Design pattern for UI controls' state management in iOS - iphone

Similar to this question, but I am looking for a generic solution or design pattern or framework.
Q. How to add state management into all UI controls in my iOS app automatically without the need to rewrite the existing controls' class?
Example:
e.g. When I click on a UIButton, it will create a new UIWebView showing Google home page. That is easy, but problem arise when user sometimes.. click the button just too fast, so two webview will be displayed.
To solve this question, I would need to make a singleton class which contain the webview, and have a state variable isOpended and if it is true, reuse the existing webview instead of creating a new one.
But the problem is: If I want this behavior in other controls also, then I would need to create many many singleton classes..I am just thinking if there is better way to handle this without the new to re-invent the wheel.
Thanks.

I think you're solving the wrong problem here. Why don't you disable the button until the UIWebView is done processing. That way the user cannot click it twice.
- (IBAction)showMapHomepage:(UIButton*)sender
{
sender.enabled = NO;
[self taskThatTakesALongTimeWithCompletion:^{
sender.enabled = YES;
// Finish processing
}];
}

You are misinterpreting the best way to go about solving your problem. First of all, you should never find yourself in a situation where you are creating many many singletons. Singletons are a necessary evil, but you should not overuse nor abuse them. Here is a good post about singletons in objective-c.
There are numerous ways you could go about preventing a second UIWebView from being displayed when the user clicks your button.
As someone else stated, one solution would be to disable the button so that the user cannot "double-click" it. You do this using:
button.enabled = NO;
You could also hide your button using:
button.hidden = YES;
Or, in the header of the class that contains your UIButton, you could create a boolean that will handle the logic of whether or not the button has been pressed;
// declare this in your header
BOOL buttonPressed;
// this is the IBAction that your button hooks up to
- (IBAction)createWebViewButtonPressed:(id)sender {
if(!buttonPressed) {
buttonPressed = YES;
// insert code here to create your UIWebView
}
}
Again, there are numerous ways to accomplish what you are trying to do. You just have to determine which method is the best for you.

I agree with other answers that you should probably disable the control if you don't want it to be activated twice. However, if you do want an answer for your actual question about a generic pattern that you can use on all controls then you can use associated objects...
- (IBAction)buttonAction:(UIButton*)sender
{
NSString* webViewKey = #"AssociatedWebView";
// See if there is web view already
id webView = objc_getAssociatedObject(sender, webViewKey);
if(webView == nil)
{
// There is no existing web view, create it
webView = [self theWebView];
// Associate it with the button
objc_setAssociatedObject(sender, webViewKey, webView, OBJC_ASSOCIATION_RETAIN);
// Add the web view
[self.view addSubview:webView];
}
}
The above shows a generic way to associate an object to an instance of UIButton so you can check if it is already associated and re-use the existing one. I provide this answer in case you intend to use this in some other way that isn't fully described in your question, but in practice, you could use a property of your controller for the webView that lazy-loads the webView if it isn't already loaded.
If you really want to simulate the singleton style that you discuss in your question (so that you can have many UIButton instances that all share the same webView object if it already exists) then you could associate the webView onto the [UIButton class] object or even the [UIControl class] object instead of your specific instance. You would do that by replacing the sender with [UIControl class] in the code above.

One possible solution is to store a pointer to the webview in a property of the viewController. In the getter for the webview, create the webview if it doesn't exist already. The button action just need to display the webview, as it will just redisplay the webview if it already exist, and it will create the webview if it doesn't. When you are done with the webview, just set it to nil.

Related

General advice on tracking an object's changes

I have an option menu where a user can change the application's settings. I need to track the changes. There is a DONE button, I want to make changes to the data model only when the button is pressed.
My question is, I am not sure the best way to track the changes. I don't want to introduce a giant if blocks in the doneButtonPressed button. Any general advice?
Well, the generic answer is: add callback to your controls in your options screen.
For example if you are using UISlider, then you have to customize it slightly. Probably create a subclass, that would receive touch events and then you redirect them to the delegate. OR you can use this one: RCSwitch
If you are using UIButton's then it's even easier: just add action to it.
After that you can create method like:
-(void) controlDidChange:(UIView*) control {
//mark changed items here
}

access UISwitch setting from a different view?

k, I'm new to this so apologies all around, generally.
I'm trying to access the UISwitch value (on or off) from a different view and class and can't make it work.
It's a simple 2 view app. Main view and the second is a preference menu.
trying to write an if/else method to play sound when the switch (on the other view) is on and not when its off.
I cant seem to make it work. Any thoughts or some syntax examples would really help me out.
Thanks.
As Matt Wilding said "it's not good form to access UI components of one view controller from another...".
Instead of accessing the view object, when the switch state is changed by the user you save the status into NSUserDefaults as preference value. Whenever you want, you can access the switch status value through the preferences.
I'm going to take what I think you're trying to accomplish here and suggest an alternative approach. You want to have a preference in your app (assumed from "preferences menu") that allows the user to set something like whether or not you app plays background music. (May not be exact, this is just for clarification).
Typically, in a well designed app, the flow is driven by the data, with the UI reflecting the state of the data model and the controllers coordinating the two layers. What you are suggesting is to have your application play music based on the state of the UI, which is not backed by any data model. This cuts out the model level, and as you noticed, can lead to awkward attempts at communicating between the UI of different controllers for information.
Things like application preferences are typically stored in a nifty .plist file that is managed through the NSUserDefaults class. This would be a great place for the data level tracking of your preference. In this situation, the UISwitch would represent the state of the flag in the settings file, and changing the value of the switch would change the value in the file. Anywhere else in your application that you need to know if the play-sound-flag is set, you reference the data model info instead of the UI. This decouples the view controllers from each other, which is a good thing.
For this purpose add selector for swith and make NSInteger property in app delegate.Like the followed
[
yourSwitch addTarget:self action:#selector(switched:) forControlEvents:UIControlEventValueChanged];
-(IBAction) switched: (id)sender
{
int state=0;
if(yourSwitch.on)
state=1;
else
state=0;
objAppDelegate.switchState=state;
}
then you need to access this appDelegate property in second view where you are playing sound
then according to this value you can do what you want and for making object of appDelegate class you need this line
YourAppDelegateClass *objAppDelegate=(YourAppDelegateClass *)[[UIApplication sharedApplication] delegate];
ok if you have any other doubt then you can ask.

iPhone Checkboxes à la Mail

After reading the iPhone Human Interface Guidelines, I notice there's little mention of checkboxes in the style that one may encounter them on the desktop or web.
Checkboxes are generally handled by UISwitchs on the iPhone, but for an app I'm presently working on, they're really not the right control. Instead, the control you'll see in Mail is a much better fit:
Actual mail blanked out. Obviously.
How would I go about using these checkbox controls in my app? Are they standard, or will I need to imitate them with a custom control?
Cheers friends.
You'll need to create a custom control. It won't be difficult since UIControl already has 'selected', 'highlighted' and 'state' properties at your disposal. You'll just need to draw and toggle appropriately.
Don't subclass UIControl. What you want is a UIButton of "custom" type. Load it with your "unlit" image in IB (or programmatically in -viewDidLoad--you can set it appropriate to its data there too, if you came here with that property already "checked").
Point its touchUpInside event at a method called -(void)toggleCheckBox, and in that method, toggle whatever setting you're toggling (probably a BOOL property of the objects you're listing), and toggle the "lit/unlit" status of the button image by using its -setImage: forState: method. Use the control state UIControlStateNormal.
I do something similar where I let people poke a button to toggle the "favorite" status of the thing ("thisEvent"--a member of an array of local cultural/arts events) they're looking at:
- (IBAction)toggleFavorite {
if (self.thisEvent.isFavorite == YES) {
self.thisEvent.isFavorite = NO;
[self.favoriteButton setImage:[UIImage imageNamed:#"notFavorite.png"] forState:UIControlStateNormal];
}
else {
self.thisEvent.isFavorite = YES;
[self.favoriteButton setImage:[UIImage imageNamed:#"isFavorite.png"] forState:UIControlStateNormal];
}
}
I'm pretty certain there is no standard way to do this. However it's fairly simple to achieve, all you need is two images, one for each state. I would probably do something simple like subclass UIImageView and add a setState:(BOOL)theState method, which would then simply select the relevant image.
I'd rather subclass UITableViewCell then UIImageView. UITableViewCell allready comes with selected/unselected states and handlers for editmodes etc.
As said before, you'll need to subclass UIControl. The actual process was discussed here w little while ago.
I also found a description of another way to do this using the same image/method that the Mail app uses:
http://networkpx.blogspot.com/2009/07/multiple-row-selection-with-uitableview.html
but as this implements undocumented features of the iOS SDK, it may not be best for apps intended for the official App Store.

Getting a variable into another class through properties or button tags with Iphone SDK?

I have been pounding my head against a wall trying to figure out what I think is a simple thing to do, but I have, so far, been unable to figure it out with the iPhone SDK. I have a view with 4 buttons. When I push the buttons, I have another view called and come up to take over the screen. I want to have a simple variable passed from the old view to the new view. The purpose of this is to know what button was pressed out of the four identical ones. (trying to get an int value of 1 through 4) Here is what I have tried so far:
I initially tried to call the variable from the class itself. My understanding was that my new view was only sitting on top of the old view, so it should not have been released. I could not get access to the variable as the variable was said to not be declared.
I then tried to create a method that would return the int value. Again, the view class was not seen and was being declared as a "first use" of it.
I then attempted to follow another similar post on here that suggested to try and send the button tag number to some part of the new view. I attempted to do this by setting the new views button title but I still cannot figure out a way to code it without coming up with errors.
I am just looking for what method would be the best for me to pursue. I have no problem going back and reading the books on Objective-C, but I'm just trying to figure out which way I should concentrate on. Thank you for any insight.
Something like this should work:
button.tag = <whatever the tag value should be>;
[button addTarget: self selector: #selector(onTouchDown:) forControlEvents: UIControlEventTouchDown];
- (void) onTouchDown: (id) sender
{
int tag = ((UIView*)sender).tag;
UIViewController *vc = [[MyVC alloc] initWithID: tag];
// display the view for the vc
}
Incidentally, I find NSNotification's very useful for passing results back to a parent controller.

program access to iPhone volume buttons

Is there any way to subscribe to volume buttons press events?
After the recent rejections from Apple
Do not use this. Apple now uses some patch which would reject your app straightaway if it uses any of the private APIs - though should note here that quite some apps on the App Store use this already and are still there!
The only way to do this now is to have an AVAudioPlayer prepared to play but not playing ([player prepareToPlay]). This seems to take care of adjusting the app's volume according to the rocker buttons.
There's no other published way currently to handle this.
PLEASE READ THE ABOVE NOTE
Yes, Use the MPVolumeView
MPVolumeView *volume = [[[MPVolumeView alloc] initWithFrame:CGRectMake(18.0, 340.0, 284.0, 23.0)] autorelease];
[[self view] addSubview:volume];
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(volumeChanged:)
name:#"AVSystemController_SystemVolumeDidChangeNotification"
object:nil];
for (UIView *view in [volume subviews]){
if ([[[view class] description] isEqualToString:#"MPVolumeSlider"]) {
volumeViewSlider = view; //volumeViewSlider is a UIView * object
}
}
[volumeViewSlider _updateVolumeFromAVSystemController];
-(IBAction)volumeChanged:(id)sender{
[volumeViewSlider _updateVolumeFromAVSystemController];
}
This will give you a slider (same as one used in ipod) whose value will change acc to volume of the phone
You will get a compile-time warning that view may not respond to _updateVolumeFromAVSystemControl, but just ignore it.
If you just want to get the notifications, I think it is like this:
Please correct me if I am wrong, but I don't believe this uses any internal API.
[[NSNotificationCenter defaultCenter] addObserver:self
selector:#selector(volumeChanged:)
name:#"AVSystemController_SystemVolumeDidChangeNotification"
object:nil];
Details of this event are here: http://www.cocoadev.com/index.pl?AVSystemController
The other replies here seem to be based on this hack: http://blog.stormyprods.com/2008/09/proper-usage-of-mpvolumeview-class.html which was a workaround for a now-fixed bug.
But I'm pretty sure if all you want to do is GET the notification, and not SET the system volume, you can just use the notification center like with any other event!!
Be advised: since Apple added the volume-up action to the camera, this notification is not posted while a UIImagePickerController is visible.
The easiest and most functionally complete way to do this that I have found in studying all the sources mentioned above and in other threads is: JPSVolumeButtonHandler (I am not involved other than being a user. But thanks a lot to the people responsible!)
EDIT: Release 1.0.2 came with some significant changes/enhancements. I'll leave my prior answer for 1.0.1 below the fold.
I put a sample wrapper class that you can either deploy as-is, or use to learn JPSVolumeButtonHandler's hopefully proper use in a separate Github repository real quick here.
Here's how the wrapper is meant to be used (I'll add this to the repository as soon as I get to it):
The singleton class has two flags: isInUse and isOn. isInUse is meant to be set in some sort of general app settings and switches button support on and off in general.
So, no matter any other values in the class, if this is false nothing will happen when the user presses a volume button and the implementation makes sure as much as possible to keep things clean and not affect system volume level unnecessarily. (Read the issue mentioned in the README for what can happen, when button support is switched on for the first time.)
isOn is meant to be true exactly for the duration that the button is needed. You can switch it on and off without regard to the present value of isInUse.
In whichever view you initialize the action that's supposed to happen when a volume button gets pressed, set the action like so:
PhysicalButton.shared.action = { /* do something */ }
The action has type () -> Void. Until you initialize the action, nothing will break. Just nothing will happen. This defensive functionality was important to me as the view that uses volume button support would only be created after button support is set up.
For seeing things in action, you can download the app that I am using this in real quick for free. The settings manipulate "Physical button support" in general. The main Stopwatch view is the one to actually switch button handling on when entering the view, and off on leaving it. If you find the time, you'll also find an important note there in Settings > User Guide > Option: Physical Button Support:
In exceptional circumstance, the app may not get a chance to properly
switch volume button handling off outside the Stopwatch view...
I'll add the full note to the Github README.md. Feel free to adapt and reuse it, if it's relevant in your case.
The circumstances aren't actually that exceptional and I haven't fully figured out what's wrong. When the user kills the app (or you just stop your app from within Xcode) while volume buttons are on, physical button support may not properly be removed from the OS. Thus, you can end up with two internal handler instances, only one of which you have control over. So, then every button tap results in two or even more calls to the action routine. My wrapper has some guardian code to prevent too rapid an invocation of the button. But that's only a partial solution. The fix need to go into the underlying handler, which I regrettably still have too little an understand of to try to fix things myself.
OLD, FOR 1.0.1:
In particular, my interest was in a Swift solution. The code is in Objective-C. To save someone some research, this is all I did using Cocoapods (for dummies like me):
Add pod 'JPSVolumeButtonHandler' to the podfile
Run pod install on the command line
Add #import <JPSVolumeButtonHandler.h> to the bridging header file
Set up callbacks for the volume up and down buttons like so:
let volumeButtonHandler = JPSVolumeButtonHandler(
upBlock: {
log.debug("Volume up button pressed...")
// Do something when the volume up button is pressed...
}, downBlock: {
log.debug("Volume down button pressed...")
// Do something else for volume down...
})
That's it. The rest is optional.
In my case, I wanted to enable overlaying physical button pushes with virtual on-screen buttons just for select views, while making sure to block as little of the normal button functions as possible (so that the user can run music in the background and adjust its volume in the rest of the app just fine). I ended up with a mostly singleton class as follows:
class OptionalButtonHandler {
static var sharedInstance: OptionalButtonHandler?
private var volumeButtonHandler: JPSVolumeButtonHandler? = nil
private let action: () -> ()
var enabled: Bool {
set {
if !enabled && newValue {
// Switching from disabled to enabled...
assert(volumeButtonHandler == nil, "No leftover volume button handlers")
volumeButtonHandler = JPSVolumeButtonHandler(upBlock: {
log.debug("Volume up button pressed...")
self.action()
}, downBlock: {
log.debug("Volume down button pressed...")
self.action()
})
} else if enabled && !newValue {
log.debug("Disabling physical button...")
// The other way around: Switching from enabled to disabled...
volumeButtonHandler = nil
}
}
get { return (volumeButtonHandler != nil) }
}
/// For one-time initialization of this otherwise singleton class.
static func initSharedInstance(action: () -> ()) {
sharedInstance = OptionalButtonHandler(action: action)
}
private init(action: () -> ()) {
self.action = action
}
}
There is just one common action for both up and down volume buttons here. The initSharedInstance() was necessary, because my action included references to a UI element (a view) that would only be set up at some user-dependent point after app launch.
One-time set up like so:
OptionalButtonHandler.initSharedInstance({
// ...some UI action
})
Enable/disable selectively simply like so:
OptionalButtonHandler.sharedInstance!.enabled = true // (false)
(Notice that my code logic makes sure that .enabled is never accessed before initSharedInstance().)
I am running Xcode 7.3 and iOS 9.3.2 on the (required!) test device.
Looking forward to learning how Apple feels about overloading their precious volume buttons. At least my app makes sure to be minimally invasive and the button use really makes sense. It's not a camera app, but comparable apps have used physical volume buttons before (less nicely even).
If you are willing to dip into the private API, I have a patch to Wolf3d that adds exactly the functionality you are looking for. It uses the private AVSystemController class and some hidden methods on UIApplication
Okay,
So I saw your solutions and don't exactly know whether Apple is going to reject or accept using AVSystemController_SystemVolumeDidChangeNotification. But I have a work around.
Use UISlider of MPVolumeView for registering for any changes in volume by the iPhone hardware like this
MPVolumeView *volumeView = [[MPVolumeView alloc] initWithFrame:CGRectZero];
for (UIView *view in [volumeView subviews]) {
if ([view.class.description isEqualToString:#"MPVolumeSlider"]){
self.volume_slider = (UISlider*)view;
break;
}
}
[volumeView sizeToFit];
#THIS IS THE MAIN LINE. ADD YOUR CALLBACK TARGET HERE
[self.volume_slider addTarget:self action:#selector(volumeListener:) forControlEvents:UIControlEventValueChanged];
[self addSubview:volumeView];
[volumeView setAlpha:0.0f];
-(void)volumeListener:(NSNotification*)notification {
#UPDATE YOUR UI ACCORDING OR DO WHATEVER YOU WANNA DO.
#YOU CAN ALSO GET THE SOUND STEP VALUE HERE FROM NOTIFICATION.
}
Let me know if this helps anyone.