I have a viewController, in which I have a view in which I draw like in Paint by tapping. In that controller I have a method to clear screen -
- (void) clearS
{
[(TouchTrackerView *)self.view clear];
}
Now, as I don't want to occupy my screen with buttons, I have button in another screen that resets application to starting position, and I want it to clear screen. That button belongs to different view controller, and among other things I want it to call my drawing view controller and ask it to wipe screen clear. So I have setup notifications, like this in draw view contoller:
[[NSNotificationCenter defaultCenter] addObserver: self selector: #selector(clearS) name:#"clearScreen" object: nil];
And like this in my view controller from where I click a button:
[[NSNotificationCenter defaultCenter] postNotificationName:#"clearScreen" object: nil];
As I understand, when the button gets clicked, notification gets posted, and method is called. And it crashes.
[(TouchTrackerView *)self.view clear]
As I understand the problem is that at the moment of method invocation the "self" variable is my non-drawing view controller, and it tries to perform clear method on view of itself, which it lacks and crashes.
How do I send the method to correct view controller?
Is there a flaw in my thinking? Maybe this can be approached in a better way?
EDIT:
I have found the problem,
[(TouchTrackerView *)self.view clear]
calls view and it is UIScrollView, and it does not support clear method. So I have made a property containing the correct view, and changed it to
[(TouchTrackerView *)self.correctView clear]
and it works like a charm.
I have chosen notification because it is only two lines of code, and I am beginner and it is hard for me to wrap my head around delegates, so I will leave this as it is, especially that it works.
At the philosophical level, while I'm sympathetic to the observations from others that you could replace the notification with a delegate pattern (or, better, delegate protocol pattern), it strikes me that this is not an appropriate example of having two controllers communicating directly to each other at all. You probably should be employing a MVC model, where the editing view controller, A, is designed for the editing of a drawing (and thus updates the model and coordinates the view) and the reset view controller, B, should just update/reset the model. If A needs to be informed of model changes, then apply a delegate-protocol pattern there, between the model and controller A. But I don't think B should be communicating with A at all.
At a practical level, there's absolutely no reason why notifications shouldn't work just fine. Unfortunately, you haven't shared enough for us to answer the question. You should share the specifics of the error message and we might be able to help you more.
I think that notifications, which are in nature a one-to-many method of communication are not good to use here. A better approach would be to hook one as the delegate of the other -> once the button is pressed and the corresponding IBAction is invoked, the delegate (in your case, the view controller you use for drawing) should get a message and perform whatever it is it needs to do.
I would avoid notifications unless you are trying to broadcast out some info that multiple objects may be interested in.
A better approach might be to create a delegate protocol for the painting view controller --
So the PaintingViewControllerDelegate Protocol may have methods like so
(void) paintingViewControllerWillClear:(PaintingViewController*)paintingViewController;
(void) paintingViewControllerDidClear:(PaintingViewController*)paintingViewController;
Now the controller with the buttons becomes the delegate to the PaintingViewController and that object provides the methods of the PaintingViewControllerDelegate protocol as needed.
Related
I've extracted my NSFetchedResultsController's into a separate object. I'd like to monitor when the view controller appears and disappears so that I can pause and resume the FRC delegate methods to update the tableview with new content. Is this possible without any responsibility from the view controller itself? I.e. I know I could use delegates or notifications, but I am looking for a solution where I don't have to sprinkle code all over the view controllers.
It seems there isn't an official way to do this, so here's what I did.
- (void)viewWillAppear:(BOOL)animated
{
[super viewWillAppear:animated];
[self.fetchedController willAppear];
}
And then handled the necessary logic in there... pretty basic.
Maybe another time I'll post about my fetchedController. It's pretty neat: it holds a UISearchController (and delegates), 2 data sources (one for the regular view, and one for the search). There's a protocol that the view controller implements (tableView, entity name, context, sort descriptors, configureCell, etc) so I never have to create search controllers, NSFetchedResultsController's, or any of the delegates directly. It's much cleaner than having a god UIViewController superclass.
I placed my code for iAd/AdMob ads in...
-(void)viewWillAppear:(BOOL)animated{}
Ads work perfectly fine the way I have them now on all iOS devices.
When I connected my iPhone to Xcode and clicked on Product -->Analyze a message states...
The viewWillAppear:instance method in UIViewController subclass 'iPhoneSIX' is missing a [super viewWillAppear:] call
I just accidentally stumbled upon this Product-->Analyze thing. Do I really need to add [super viewWillAppear] even though everything works perfectly fine on all devices as it currently is. Will Apple reject my app if I don't pay attention to the Product-->Analyze issue navigator?
Also, what does ...
[super viewWillAppear:YES];
What does calling this do?
According to Apple: (emphasis mine)
This method is called before the receiver's view is about to be
added to a view hierarchy and before any animations are configured for
showing the view. You can override this method to perform custom tasks
associated with displaying the view. For example, you might use this
method to change the orientation or style of the status bar to
coordinate with the orientation or style of the view being presented.
If you override this method, you must call super at some point in your
implementation.
Apple doesn't gets that specific when deciding to Accept or Reject your app. It only follows the guidelines, which doesn't get that much into the weeds of your specific methods.
Calling [super viewWillAppear:YES] is a best practice, and I would recommend it. Always including super ensures that any code in the super classes get called before executing any additional code. So if you or someone else coded a super class that expected some code to be executed, you are guaranteed to still execute it, rather than just overwriting the whole method in the subclass.
Say you have a view controller of type MyViewController which is a subclass of UIViewController. Then say you have another view controller of type MyOtherViewController, which is a subclass of MyViewController. Say you're coding now some things in viewWillAppear in MyOtherViewController. If you call super first, it will call viewWillAppear in MyViewController before executing any code. If viewWillAppear in MyViewController calls super first, then it will call viewWillAppear in UIViewController before executing any code.
I'm quite certain Apple will not reject your app for failing to call super on an overridden method, primarily because there are cases where you may specifically want to avoid calling super.
That said, as Josh Gafni mentions it is definitely a best practice to do so, unless you have a very good reason for not. Also bear in mind some view controller subclasses (can't recall specifically which ones, but maybe UICollectionViewController) will only work properly if their view lifecycle methods get called appropriately, so not calling super can definitely break some classes (sometimes in subtle ways you may not realize).
Therefore my suggestion is add the call to super (generally as the first line in the method) and see if things continue to work fine. If not, spend a bit of time trying to understand what is happening differently and see if you can solve it in a different way. In general you should always (as a force of habit) provide calls to super on any view lifecycle methods you override whenever possible.
I am building a music player app -- in this app, the music player view controller will always sit on top of any other sub-viewcontroller (navigation view, table views, etc) I need actions taken in any potential subview to be sent back up to the player view controller (for example, user selects "play" on a profile page, and I send that event back up) My question is what is the best way to do this? I apologize in advance for being a bit nebulous, but I already know of three ways I can implement it. I just want to know what the "right" way is.
These are the three ways I've thought of:
1.Delegate pattern -- pass the Music Player Controller off to it's children controllers and set itself as the delegate for whenever that event is passed (messy because the first view controller is a navigation view controller, so I think I'd have to pass it down several levels meaning several delegates (correct me if I'm wrong))
2.Notification Center -- register the player view controller for a particular notification, encapsulate the data that's sent from the other viewcontrollers so that I can perform my actions.
3.Singleton-like access of the player view controller - basically allow access to the player view controller from any view controller.
Any help is appreciate to steer me in the right direction. I can do it either of these ways, but as this is a "learning" app, would love to do it right.
IMHO there is no "right way". Frankly I was thinking of all three of them when I read the subject line only.
As you are asing for opinions... I would not recommend the singleton pattern here. This is just because view controllers could stack and by their nature be instanciated multiple times. In terms of maintainable code (readability by others) I'd say no to that approach.
The delegate pattern is fine. But as you say you would have to pass a reference to this view controller from one view controller to the next. Yes, a bit messy.
Well, you could store a reference to the delegate in some singleton. That is not the same as designing a view controller as singleton.
I'd say that the notification center is most appropriate for this type of data flow. Sender and receiver of the message are totally detatched and don't need to 'know each other'. It is a perfect pattern for all kinds of multiple senders and/or mulitple receipient type of messages.
Well, as I said, this is just an opinion.
I would not recommend broadcast notifications (via NSNotificationCenter), they are hard to debug, other developers will have problems to understand, whats going on in your application (application flow), every developer must maintain a global list with all notification names (notificationSender and observer must use the same notification name and they are usually constant string variables), observer can not send back any data after receiving a notification, etc. Broadcasting is really helpful, if all controllers should be notified of the same event (for example Login/Logout events).
If possible, i think one should allways try to use a Delegate Pattern (with clearly defined protocol). Maybe something like:
id <SubViewControllerEvents>musicPlayerVC= [MyMusicAppManager delegate];
if ( [musicPlayerVC respondsToSelector:#selector(userDidSelectPlay)] ) {
[musicPlayerVC userDidSelectPlay];
}
In my app, some studies I've done show that when the user views an article, the vast majority of the time (85%+) they load the accompanying comments view controller that goes along with the article.
I'd love to load this view controller while they're reading the article, so when they tap the comments button to transition to the comments view controller the view controller is ready without any loading times.
How would I go about accomplishing something like this? At the moment when the user taps the button I call performSegueWithIdentifier: and pass it the identifier I set in the Storyboard.
And obviously, for the cases where the user decides to go back to the root view controller (say, a list of articles) I'd want to cancel the loading of that comments view controller as it would be wasteful to continue at that point.
If you keep the data model separate from the UI, you shouldn't have any trouble creating the views on the fly, and almost nothing to gain from creating them earlier.
A reasonably standard approach is to bring the view in with blank or filler data, and have the data call go out to the middle tier, with asynchronous handlers processing callbacks.
This is much easier with the block based completion handlers available in the iOS 7 flavored NSURLSession, and only slightly harder with NSURLConnection (which listens for responses on the main thread, but can be thrown into the background once you catch the response).
So my advice would be to focus on backgrounding the data calls and responses, and strongly differentiate between displaying UI and populating the UI with data. If your data manager is separate from your View Controller, nothing is stopping you from "pre-fetching" the data a little early, and then potentially having it ready when the ViewController needs it. It's a perfectly normal load balancing / customer experience technique for high value data.
The solution I'm about to describe is a bit of a hack - it doesn't really conform to the proper model-view-controller design pattern that Ryan mentioned. That being said, it might give you an idea about how to proceed. Perhaps you can improve on it to make it cleaner.
First, define a #protocol in the App Delegate. Let's call this protocol CommentQueryDelegate; it should define a method called -(void)handleCommentQuery. Also give your App Delegate a strong property to store the comment data and a weak property to store a delegate object, like so:
#property (nonatomic, strong) NSMutableArray* arrayOfComments;
#property (nonatomic, weak) id<CommentQueryDelegate> commentQueryDelegateObject;
Make sure to initialize the both of these properties to nil.
Somewhere in the article view controller, use dispatch_async() to asynchronously query your database and retrieve the comments while the user is reading the article:
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^
{
AppDelegate *appDelegate = (AppDelegate*)[[UIApplication sharedApplication] delegate];
// query your database here
appDelegate.arrayOfComments = [NSMutableArray array];
[appDelegate.arrayOfComments addObject:#"someComment"];
[appDelegate.commentQueryDelegateObject handleCommentQuery];
}
If the query is completed before the user segues into the comments view, the handleCommentQuery message will be sent to the nil object, which will have no effect.
Now, in the comments view controller's viewDidLoad method, set the commentQueryDelegateObject property of the App Delegate to self. You will need to specify that the comments view controller conforms to the comment query protocol. Next, check to see if the App Delegate's arrayOfComments property is nil. If it isn't, great - display the comments immediately. Otherwise, display a UIActivityIndicatorView.
Implement the -(void)handleCommentQuery method in your comments view controller. This method should disable the activity indicator and display the comments.
One final thing to consider - the strong pointer to the arrayOfComments object will keep it from being deallocated, so you should set this pointer to nil once you're done with your article view controller.
I am wondering if there is a good, general paradigm for setting up the refresh action in an arbitrarily deep UITableView. I'm speaking specifically of the refreshControl property of UITableViewController.
One layer is pretty easy and obvious since you just refresh your model and then call [self.tableView reloadData], but even two layers things start conflicting with my notions of good OO programming.
So if the user pulls the table view down to refresh from an arbitrarily deep table, it strikes me that it does absolutely no good to just reload the data from the parent view (which doing, it seems to me, isn't a good OO setup since a child controller isn't supposed to be able to talk directly to the parent). The refresh call needs to be passed back up through the UITableViewControllers until either the top, or some point that would conceivably be the deepest point where the model might have changed, and then push the new, refreshed, data back down through the child UITableViewControllers until it reaches the controller which originated the refresh (checking that that path even still exists at each jump).
The only way I'm coming up with doing this is to have a property in each controller called parentView which is set during prepareForSegue and a method called something like dataForChildView.
-(IBAction)refreshTableView {
[self.refreshControl beginRefreshing];
//self.parentView would be nil at the topmost TVC
if (self.parentView) {
[self.parentView refreshTableView];
self.data = [self.parentView dataForChildView];
}
//refresh data
[self.tableView reloadData];
[self.refreshControl endRefreshing];
}
But this seems to me to violate the basic rules of nested controllers, for the reasons I state above.
I haven't yet learned about NSNotifications, but this strikes me as a possible approach. However, since I'm finding nothing online about this question, I wanted to ask before I commit a bunch of time to figuring out if NSNotifications will let me do this. Or maybe I'm missing some even simpler approach.
Edit: the assignment that this is for is set up such that only the topmost UITableViewController has access to data from the model; all lower controllers are passed their data in prepareForSegue. I am starting to think that perhaps this is the problem; there needs to be a model that can be accessed from any level down in the UITableView. Would that be correct?
You could for example refresh your model (which can be passed along to the view controllers) and reload the table views on viewWillAppear.
Also the way with notifications you mentioned will work.
Register for notifications in your viewDidLoad method:
[[NSNotificationCenter defaultCenter] addObserver:self
selector:#selector(selectorThatReloadsTheTableView:)
name:#"NameOfYourNotification"
object:nil];
Don't forget to remote your view controller from the notification center by calling
[[NSNotificationCenter defaultCenter] removeObserver:self];
Now all you need to do is post the notification once the user pulls-to-refresh:
[[NSNotificationCenter defaultCenter] postNotificationName:#"TestNotification"
object:nil];