Being still new to iPhone development, I am slowly getting there however sometimes the simple things seem to stump me.
My application consists of 7 very different views. It is implemented as a window based app without a navigation controller. Navigation is managed by each view controller individually and I use a central data repository to make data available through the app.
I use :-
DetailView *viewDetail = [[DetailView alloc] initWithNibName:nil bundle:nil];
[self presentModalViewController:viewDetail animated:YES];
to change views. Obviously, in all cases, this is executed within the controller of the current view.
HOWEVER, I have now added Push Notification to the app. Upon receipt of a Push, the userInfo data is made available within the main AppDelegate and didReceiveRemoteNotification is executed. I wish to force the application into the Detailview described above, passing it one of the values within userInfo.
Of course, I get a nice SIGABRT as soon as it executes the second line. I assume this is because the currently active view is not self. How do I surrender the current view in favour of the DetailView from within the app delegate? The current view could be ANY of the 7 views, including DetailView, which I wil want to refresh with the new data.
Thanks in advance
Chris Hardaker
This is a small side note, but the naming convention that you are using is non-standard. Rather than,
DetailView *viewDetail = [[DetailView alloc] initWithNibName:nil bundle:nil];
conventionally, an iOS developer would expect to see the following:
DetailViewController *viewDetail = [[DetailViewController alloc] initWithNibName:nil bundle:nil];
since viewDetail is a subclass of UIViewController rather than UIView.
To answer you main question though, I would use NSNotificationCenter. Basically, this allows any class to post a notification, which more or less just throws out the fact that an event occurred to any class that happens to be listening for it.
So in didReceiveRemoveNotification: you can call:
[[NSNotificationCenter defaultCenter] postNotificationName:#"pushNotification" object:nil userInfo:userInfo];
When you allocate your view controllers, run this line:
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(pushNotificationReceived:) name:#"pushNotification" object:nil];
You will need to put a method called pushNotificationReceived: (NSNotification*)aNotification in each of the view controllers to handle the notification. The userInfo dictionary that you passed will be a property of the notification.
Now, when ever the #"pushNotification" notification is sent, your controllers will receive a notification and run the given selector. So you can dismiss the controller or show the detail view or whatever you want.
When you dealloc your view controller, make sure to call
[[NSNotificationCenter defaultCenter] removeObserver:self];
Related
I'd like to go back to my root view from within a class method of view 1. When in an instance method of view 1, i would just say
[self.navigationController popViewControllerAnimated:YES];
but since self doesn't apply in a class method, I am wondering how to accomplish this. Pertaining to the illustration below, I am currently in a class method of View1Controller.m and I'd like to get back to Rootview. Thanks.
You can declare another method:
-(void)closeThisViewController
{
[self.navigationController popViewControllerAnimated:YES];
}
Then use NotificationCenter:
[[NSNotificationCenter defaultCenter] postNotificationName:#"notif_closeThisVC" selector:#selector(closeThisViewController) object:nil];
Although as jonkroll said, you're dealing with view controller stuff, we don't understand why you would put view controller related code inside a class method.
Edit
Sorry bad code above.
I meant to say you can use NSNotificationCenter to post a notification:
-(void)postNotificationName:(NSString *)notificationName object:(id)notificationSender
Then in the same view controller declare a NSNotificationCenter observer:
- (void)addObserver:(id)notificationObserver selector:(SEL)notificationSelector name:(NSString *)notificationName object:(id)notificationSender
My brain was quicker than my fingers, so I kinda combined the two into one when I tried to explain the solution :P
It should more like this:
// posting a notification with NSNotificationCenter
[[NSNotificationCenter defaultCenter] postNotificationName:#"notif_closeThisVC" object:nil];
In your viewDidLoad method somewhere (I recommend at the top), add this:
-(void)viewDidLoad
{
// adding an observer with NSNotificationCenter
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(closeThisViewController) name:#"notif_closeThisVC" object:nil];
...
}
Hope that helps.
If you don't want to refactor this from a class method to an instance method (and there are certainly cases where you wouldn't want to do that), I'd suggest you add a completion block parameter to your class method:
+ (void)doSomethingWithCompletion:(void(^)())completion {
/* Do your thing... */
completion();
}
- (IBAction)doSomething:(id)sender {
[self.class doSomethingWithCompletion:^{
[self.navigationController popViewControllerAnimated:YES];
}];
}
This would allow you to cleanly separate the instance-less operation the class method performs from instance-specific dismissing of the view controller. You could also make the completion block accept an error object if the operation can fail.
You could do something similar with a delegate object or even by passing in the view controller to dismiss, but this design seems to offer the cleanest separation with the most modern feel.
There are going to be legitimate arguments that encourage you to refactor so that you do have access to the current view controller and can access the navigation controller via currentVC.navigationController. Remember, it can still be a class method, just give it an extra argument when you call it (or start the call chain that calls it) from the VC.
However, I also had to solve this in one of my apps, so I just made sure that the navigation controller was globally accessible to everyone, always via pointer ("weak ref")
If you declare a global variable like this (say, in "Navigation.h")
extern UINavigationController *gNavController;
and define it in your AppDelegate.m (pays to review distinction between declaration/definition if you're rusty on that):
UINavigationController* gNavController;
and then assign it when you start up in application:didFinishLaunchingWithOptions::
(assuming the delegate has a property called viewController that is your navigation controller):
gNavController = viewController;
Then as long as you #import Navigation.h, you'll always have access to the navigation controller. This also makes getting a handle to its view for popups/popovers much simpler.
This also assumes your nav controller is never released for the lifetime of the app (probably true unless you're doing something unusual).
I have an app with a navigation controller as the root view. There are many views that can be pushed in.
The user has to create an account to use the app. The user can then log into this account from other devices, but only one device can be logged onto the same account at a time. So if multiple devices try to log into an account, only the latest device will be logged in and the other devices are logged off (a push is sent to the devices).
Since there are multiple views that the device could be showing before it was logged off, I call popToRootViewControllerAnimated: to get back to the root view. This is because when the user logs in the next time I only want the root view to be shown (the new account might not have access to the previously shown view).
If the user has an alert view or action sheet presented (which uses the current view as its delegate) before the push is received, the view will still be shown after the popToRootViewControllerAnimate: method is called. If the user then taps on a button for the alert view or action sheet, it will send a message to the dealloc'd view and crash the app.
An example:
myViewController is being shown to the user.
myViewController create an action sheet prompting the user for a decision.
The push is received for the device to log out.
The navigation controller pops all the views controllers and now shows myRootViewController.
Since the view controllers are popped, myViewController is now dealloc'd.
The action sheet from myViewController is still shown.
When the user selects an option form the action sheet, a message is sent to myViewController, and since it is already dealloc'd, a crash will occur.
Is there any way to prevent this?
One solution I have considered would be to keep track of all the objects that uses a specific view controller as its delegate. Then when that view controller dealloc's it will also set all the object's delegates to nil. This requires me to manually take care of every view controller when they create an object that uses itself as the delegate, since I cannot think of a way to automatically create and update this list.
Any better solution (or improvement to mine) would be appreciated!
Edit: The alert view and action sheet are only examples of some objects that I would use myViewController as a delegate. I am also using a number of other classes (and third-party libraries) that implements this delegate pattern.
A few ideas:
you can encapsulate the alert/action sheet view and delegate in a single class. Then when you need an alert view, create MyAlertView instead, which will also be its own delegate and will do [self release] after the user taps a button.
make your App Delegate the only delegate for all your alert views and action sheets. App Delegate is always around while the application is running, so there won't be a problem with a released delegate.
The problem with both solutions is that if you need your application to know what happened in the alert view/action sheet, you somehow need to tell the interested class of the user's choice.
You can do that by either using delegates of your own - which would mean you're back to square one - or use notifications: when the alert view/action sheet delegate is called, it would post a notification ([[NSNotificationCenter defaultCenter] postNotificationName:NotificationName object:self userInfo:userInfo];), while the interested object would look for that notification ([[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(onNotification:) name:NotificationName object:nil];) and perform whatever tasks necessary in onNotification:(NSNotification*)aNotification method.
You'll be able to agree with yourself on what type of information is passed in those notifications (I would think the button number in a NSNumber class would be enough, or perhaps pass the button text, too). And you won't have to keep track of all alert views - just don't forget to remove the observer ([[NSNotificationCenter defaultCenter] removeObserver:self name:postNotificationName object:nil];) in the views' dealloc.
Edit:
"This requires me to manually take care of every view controller when they create an object that uses itself as the delegate, since I cannot think of a way to automatically create and update this list."
Actually you probably can do this in a semi-automated way: make a singleton object with a method like
-(id)delegate:(id)delegate for:(id)forWhom
And then instead of
someThingy.delegate = self;
you'd do
someThingy.delegate = [[DelegateLocker defaultLocker] delegate:self for:someThingy];
Inside the DelegateLocker you'd have a MutableDictionary with delegate class as a key and a MutableArray of someThingies as a value. Then in your view controllers' deallocs you'd call
[[DelegateLocker defaultLocker] delegateIsDying:self];
which would go through the thingies and assign delegate = nil for each
The drawback of course is that you'll be retaining all the thingies for an indefinite period of time instead of releasing them immediately.
So the ViewController that presented the action sheet iand set itself as the delegate right? So why dont you keep a reference to the ActionSheet in the ViewController, in the dealloc method of the view controller, you can check if the action sheet is visible, if it is then set the delegate of the action sheet to nil,and dismiss it...
so
-(void)dealloc
{
if(myActionSheet && [myActionSheet visible])
{
[myActionSheet setDelegate: nil];
//dismiss
}
}
Hope that helps
If you want automated solution, I think you can make a function to iterate through Ivars of your view controller to see if any Ivar has delegate property and set it to nil.
What are the possible ways to send data to previous view in iphone. Without using Appdelegate. Because there are chances for my view class to be instantiated again.
I believe the best approach is using the NSNotificationCenter class.
Basically what you do is register an object (as an observer) with a notification center.
So for example if you have objects A and B. A registers as an observer. Now lets say A is the "previous" object you are talking about, you can have B send a notification (data or message) to the notification center which then notifies object A (and any other registered observers).
Example:
In file ClassA.m register as shown below:
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(didSomething:) name:#"SomethingHappened" object:nil];
didSomething is the method which receives the notification sent by object B. This will look something like
- (void) didSomething: (NSNotification *) notify {
...
}
Finally you send the message below from whatever method in ClassB.m to notify/send data to object A
[[NSNotificationCenter defaultCenter] postNotificationName:#"SomethingHappened" object:self userInfo:your_data];
Seems convoluted but it's the best approach in my opinion (and quite simple once you understand it :)).
There are several ways to achieve data sharing, with Singleton Objetcs being one of the most popular:
Objective C Singleton
If the view you want to communicate with is a parent view (e.g. the previous view's view controller is where you created this view) then you probably want to handle dismissing the view in the previous view controller. When you do that, you can read the data that has changed and update the previous view controller with the new data.
Then in the viewWillAppear: method of the previous view controller, update the actual views to reflect the current state of the view controller.
Edit: I've just noticed that your newView is transparent. If this is the case, then you certainly want to route all logic through your view controller. You should only have one view controller with visible views at a time.
I am trying to solve a problem considering update of an view.
First I switch to another view by using:
- (void)viewSettings {
settingsViewController = [[SettingsViewController alloc] initWithNibName:nil bundle:nil];
[[self viewController] presentModalViewController:settingsViewController animated:YES];}
Which is a delegate Method called by
ivaskAppDelegate *mainDelegate = (ivaskAppDelegate *)[[UIApplication sharedApplication] delegate];
[mainDelegate viewSettings];
I switch back by calling another dellegate method
- (void)settingsDone {
[[self viewController] dismissModalViewControllerAnimated:YES];}
When I return to my view I now want to update a label, can you explain how to do it?
I use NIB-files which have a controller class and a view class connected in the identity inspector.
/N
Although I heavily suggest delegation in this case there are two other options, that come to my mind: Notification and KVO.
Notification
Whenever settings are changed the settings view controller could post a Notification, to let other parts of the app know about this change. Posting a notification is easy as:
[[NSNotificationCenter defaultCenter]
postNotificationName:#"SettingsChangedNotification" object:theNewSettings];
Every object that somehow want to know about a settings change can subscribe to that notification via:
//Subscribe (in viewDidLoad or init)
[[NSNotificationCenter defaultCenter]
addObserver:self selector:#selector(settingsChanged:)
name:#"SettingsChangedNotification" object:nil];
// Called when a "SettingsChangedNotification" is posted
- (void)settingsChanged:(NSNotification*)settingsChangedNotification {
id newSettings = [settingsChangedNotification object];
}
//Unsubscribe (in viewDidUnload or dealloc)
[[NSNotificationCenter defaultCenter]
removeObserver:self name:#"SettingsChangedNotification" object:nil];
See Notification Programming Topics
If you are trying to manages UserDefaults with your settingsViewController there's an even better way. Just set the values on the sharedUserDefaults and the app will post a NSUserDefaultsDidChangeNotification notification all on it's own. All objects that depend on user settings could subscribe to that notification, and after it's posted reread the userDefaults.
See
NSUserDefaults Class Reference
User Defaults Programming Topics
Key-Value Observing (KVO)
Your rootViewController could observe changes of an object, which it needs to synchronize with, by Key-Value Observing.
One object registers itself as observer for keyPaths on other objects by sending them a addObserver:forKeyPath:options:context: message. The object is informed about changes via the observeValueForKeyPath:ofObject:change:context: callback method. KVO could sometimes be difficult the get right, because you have to ensure, that you register and unregister the same number of times, before an object gets deallocated.
See Key-Value Observing Programming Guide
Conclusion
Please refrain from using "global" variables on your app-delegate. There's enough possibilities to do better. The sooner you dive into them, the better code you will write.
You will need to set up a delegate that is implemented in your main view controller (where you need to have the label updated), and that is called from your settings view controller. I have a blog post that describes how to do this:
http://www.dosomethinghere.com/2009/07/18/setting-up-a-delegate-in-the-iphone-sdk/
You would make your view controller conform to a protocol that your create. In this case it may be named SettingsDelegate. That protocol contains one message - (void)didFinishSettingsWithSomeNewValue:(id)newValue;
The Settings Controller has a delegate instance variable and a property of type id<SettingsDelegate>
Before you present the settings viewController you assign the parent view controller to the delegate property. In the settings view controller you send a didFinishSettingsWithSomeNewValue: message to your delegate (the parent view controller) along with some new value. The parent view controller implements that method and inside the implementation can dissmiss the modal controller and update any views on itself.
I am using a navigation based application for iPhone. I want to reload a view when i come back to it after pressing the back button. ANy solution?
Regards
Add a method viewWillAppear: to your controller class. In that method you can then update the view with current data.
The viewWillAppear: method will execute whenever the view is about to be displayed (after navigating to a different view using UINavigationController)
There is more than one, but I usually use NSNotificationCenters. You attach "listeners" for some kind of event, like this:
[[NSNotificationCenter defaultCenter]
addObserver:self
selector:#selector(onSomethingChanged:)
name: #"somethingChangedEvent"
object: nil];
So, if some other view changes a setting, it notifies all the listeners like this:
[[NSNotificationCenter defaultCenter] postNotificationName: #"somethingChangedEvent" object: Nil];
Pretty simple and intuitive.