How UIViewControllers created - by alloc/init vs. performSegueWithIdentifier - objective-c

My iOS 8 project uses a Storyboard, wherein I have a UINavigationController, with a root UIViewController; let's call this A. I also have another UIViewController, connected via a Segue to this root controller; let's call this B. Pretty standard configuration.
Now, A is a subclassed UITableViewController, and in the table I have a list of URLs. The app user taps a table cell, the URL is extracted and passed to B. B is a subclassed UIViewController that contains a WKWebView.
Now, because B is on the UINavigationController's stack, users can tap the Back button while in B, returning to the table with the URLs (A). The user can then tap another cell, to load another URL.
Because I need to pass some data (the URL) to B, I'm invoking it with performSegueWithIdentifier, and in the corresponding prepareForSegue, I assign the URL to a property in B.
This all works fine. My questions reflect my still-nOOb status with respect to iOS and Objective-C.
The first question, when B is created via the performSegueWithIdentifier / prepareForSegue combo, when the user taps the Back button, is this controller deallocated by the OS? In other words, there's no longer an instance of this subclassed UIViewController floating around?
If it is, then presumably it's safe to have a new instance of it created if the user taps a table cell to view another URL. In other words, repeating the call to performSegueWithIdentifier, assigning the newly chosen URL in B's property, etc. Is it safe?
Or, if it's not deallocated automatically, then does repeating the call to performSegueWithIdentifier, etc. just keep creating more and more instances of B?
And, in either case, does it make sense to, when B is first created, capture a reference to it, and keep re-using it? In other words, before calling performSegueWithIdentifier, check if a reference to B already exists, and if it does, do a pushViewController with that reference to B?
So the basic question boils down to this: In a situation where a UIViewController needs to be made visible many times - through a user action - is an instance being newly created every time it's needed? If it is, then does this also imply that every instance is also being deallocated internally when it's no longer needed? And therefore I don't need to worry about the internals?
Or, if I keep using performSegueWithIdentifier every time, am I just creating a pile of new instances of the controller, and thus being wasteful and inefficient, when I could just capture a reference to the first new instance and keep re-using it?
I've tried to step through this process in the debugger and keep track of references, but I'm unsure whether I'm seeing what I should be seeing, and so I'm asking more knowledgeable developers.
Thank you for reading through this long question, and for your time in answering it.

First of all, when you push view and pop view and if you are not retaining that view controller anywhere else then it will be released completely and it doesn't really matter how many time you call performSegueWithIdentifier to create this view if you are not retaining it or manage it reference well enough.
However, in this case, I would create your 'B' view controller once using
self.bController = [self.storyboard instantiateViewControllerWithIdentifier:#"BController"];
And then you call pushViewController using self.bController, function would be looking something like this..
- (void)presentURL:(NSString*)url
{
if ( self.bController == nil ) {
self.bController = [self.storyboard instantiateViewControllerWithIdentifier:#"BController"];
}
[self.bController setURL:url];
[viewController.navigationController pushViewController:self.bController animated:YES];
}
And refresh the WKWebView with below code when it sets the URL
-(void)setUrlString:(NSString *)urlString {
// set url string
_urlString = urlString;
// clear
[self.webView loadHTMLString:#"" baseURL:nil];
// load url with string
if( _urlString != nil ) {
// get address for hosting view
NSURL* url = [NSURL URLWithString:_urlString];
NSURLRequest* urlRequest = [NSURLRequest requestWithURL:url];
[self.webView loadRequest:urlRequest];
}
}
it will make bit simpler than prepareForSegue, however, again, repeating the call to performSegueWithIdentifier, assigning the newly chosen URL in B's property, etc. It is totally safe as long as you don't retaining anywhere else.. but performance wise it might be bit expensive than keeping one instance of B viewController and showing it multiple time because it has to read and initiate the view controller and the WKWebView instance so arguably it is bit expansive..

Related

How do I prevent NSMutableArray from losing data?

The first view of my app is a UITableView.
The user will choose an option and the next view will be another UITableView. Now the user can tap on an "add" button to be taken to another UIViewController to enter text in a UITextField. That text will then appear in the previous UITableViewCell when saved.
The issue I am having: if I back out to the main view and then go back to where I previously was, that inputed text is now gone.
How can I make sure that text is not being released or disappears like this?
You might want to store this array somewhere else in your project, like in an MVC (data model). You could create a new class for it that passes the information through the classes and stores the array in one place. Then once you add to the array, you could reference that class and call a method in that class to store the text in the array and whenever you load the table view it loads with that array in the class.
In my case, I would do this, but I would make everything class methods (where you cannot access properties or ivars) and just store the array in the user defaults / web service or wherever you need and retrieve and add/return it like this:
+ (NSMutableArray *)arrayOfSavedData {
return [[NSUserDefaults standardUserDefaults] objectForKey: #"savedData"];
}
+ (void)addStringToArray: (NSString *)stringFromTextField {
[[[[NSUserDefaults standardUserDefaults] objectForKey: #"savedData"] mutableCopy] addObject: stringFromTextField];
}
The mutableCopy part is important because arrays don't stay mutable after you store them into the user defaults
The reason the text is gone, is probably because you're instantiating new controllers when you go back to where you were. You can keep a strong reference to your controllers, and only instantiate one if it doesn't exist yet. Exactly how to do this depends on how you're moving between controllers -- whether you're doing it in code, or using a storyboard for instance.
This kind of issue is very frequent. When you move around multiple controllers and views.
Each time you load a new view and controllers are alloc+init, new set of values are assigned and previous values are not used!!!.
You can use SharedInstance/SingletonClass so that it is allocated & assigned once and does not get re created with new set of values.

MagicalRecord: Create now, (possibly) save later

I've been using MagicalRecord quite a bit lately - and blimey it's been making my Core Data life much, much easier, so full credit to the author of what is a quality library!
Anyway, the scenario is I have navigation controller stack of 3 controllers, referred here from bottom to top as A-B-C:
View controller A is a form where I would like to fill in the details about the entity (i.e., its properties).
View controller B shows the result of a calculation from the entity (e.g., via an instance function).
For the sake of simplicity, view controller C is just a confirmation & is where I'd like to save the entity.
Is there a way in MagicalRecord I can call [MyEntity createEntity] in view controller A with its properties set, pass it through to C via B & only save it in C? This would also include the possibility of not saving it at all should the user decide to go back to A from B or C.
I fully appreciate I may well be getting the wrong end of the stick with Core Data & it may not be possible. As a workaround, already I know I can create a class method that does the same calculation given the relevant parameters & pass all the parameters through the stack from A to C.
Edit: Just to make it clear, I want to call [[NSManagedObjectContext defaultContext] save] in View Controller C.
yes sure.. just dont save the managedContext.
the VCs should run all on the main thread anyways.. so all can use
[NSManagedObjectContext defaultContext]
OR
pass the MOC between the three classes using a property on the controllers.
#property NSManagedObjectContext *context;
After some testing, I knew #Daij-Djan had the right idea in that I don't call [[NSManagedObjectContext defaultContext] save] if I don't want to save my entity. As a result I leave that call until View Controller C to do the saving.
However, my testing showed I need to do a little more to avoid saving any unwanted entities. I've noticed if I go to A from B via the back button, I want to discard the entity there. In my use-case, it doesn't matter if I just create another new entity going from A to B, and I never pass back through View Controller B if I have successfully saved the entity.
So basically I need to delete the unsaved entity if the back button is pressed on View Controller B. This answer helps me massively there, resulting in this code in View Controller B:
-(void) viewWillDisappear:(BOOL)animated {
if ([self.navigationController.viewControllers indexOfObject:self] == NSNotFound) {
// self.entity is the instance of my entity
[self.entity deleteEntity];
self.entity = nil;
}
[super viewWillDisappear:animated];
}

Memory management for reusing UIAlertView

Lets say I have multiple View controller classes using the same UIAlertView *alertView. *alertView's delegate is set to a centralized delegate.
I do this because would like to use the .tag to do different things based on it.
The question is every time I invoke an alert view or dismiss it, what do i have to do to prevent a memory leak?
Should I not release every time? Or is this a very bad idea?
Thanks.
A UIAlertView may be "shown" from anywhere in your app. I have an app that the main UIViewController has a timer that every so often brings up a UIAlertView. When that timer goes off, even if my main view being shown is from a completely different UIViewController (and thus view) the Alert will come to front.
If you really want to "actively" bring up the UIAlertView from any of your UIViewControllers (lets say based upon a user action), then I would do one of two things.
1) setup my Application Delegate Object with the UIAlertView implemented there, with accessor methods for invoking (showing) the Alert view, and thus freeing it from there also, or
2) Generate a singleton like object with the AlertView implemented there!!!
In either case then you can simply dealloc your UIAlertView once within the dealloc routine you write for either of those placements, and alloc it only once when the object is initialized.
Just treat it like you would any other object. If you want to keep it around, assign it to a retained property like: self.myAlert. You still need to release it like you normally would when creating it. The retained property will take care of keeping it around for you.
Always keep your retains(alloc's, copy's, etc...) and releases balanced.

Duplicate NSLog entries

I don't know if it's possible for me to include code here that's relevant as my project is so large but are there any typical reasons why NSLog would repeat some warnings and calls to it at occasions where only one call/error is occuring?
As an example, I have a subclass of NSBox that inits an instance of another class on awakeFromNib:
- (void) awakeFromNib {
burbControllerInstance = [[BurbController alloc] init];
if (burbControllerInstance) {
NSLog(#"init ok");
}
}
I get NSLog printing "init ok" twice. I don't see why this subclass would be 'awoken' twice anywhere in my project. This is part of a larger problem where I can't get variables to return anything but nil from the class I'm creating an instance of. I'm wondering if perhaps the double values are something to do with it.
This post could be helpful, i. e. one comment:
Also important: awakeFromNib can be
called multiple times on the
controller if you use the same
controller for several nibs – say,
you’re using the app delegate as the
owner of both the app’s About Box and
preferences dialog. So you’ll need an
extra guard test if you use
awakeFromNib for anything but
initializing the nib objects
Update: Much more interesting could also be this, where the author mentions that awakeFromNib gets called twice. Unfortunately there is no real answer for this particular problem but maybe some basic ideas.
Update #2: Another potential solution from stackoverflow.com: View Controller calls awakeFromNib twice.

How can I load data for an NSTableView without blocking the interface?

I'm initializing a simple interface, with an NSTableView bound to an array controller (which manages an array of dictionaries). I want to load the content for the array in the background (it's a very time-consuming process), updating the table view every 100 or 1000 elements. The idea is that the interface is available and responsive. I can't figure out how to also trigger an update / refresh afterwards. The table remains empty. Can anyone offer pointers?
My current approach is:
// In init for my app controller. This seems to work well, but I've tried other methods here.
[self performSelectorInBackground:#selector(loadTable) withObject:nil];
- (void)loadTable {
tracks = [[NSMutableArray alloc] initWithCapacity:[masters count]];
// ... create each object one-by-one. Add it to tracks.
for (... in ...) {
[tracks addObject:newObject];
}
// Now I don't know what to do next. The table remains empty.
// Things I've tried (though possibly not in all combinations with the
// method above):
// 1. With a suitably-defined reloadData method, which just reloads
// the table view and sets needs display.
[self performSelectorOnMainThread:#selector(reloadData) withObject:nil waitUntilDone:YES];
// 2. Reload directly.
[tv reloadData];
[tv setNeedsDisplay];
}
If I just load the data directly, and don't try to do that in the background, everything works fine, but it takes almost 30s.
You have the table columns (I assume you meant) bound to an array controller, so that's where the table view gets its data from. The table view may very well be asking for updated arrays, but it's asking the array controller, which doesn't know anything has changed.
The array controller won't simply turn around and ask you for fresh data; that would imply it exists solely to make it harder for you to bind the table view to your array, and that isn't the case. It's a controller; its job is to own (a copy of) the array and maintain its order and the user's selection of some subset of its objects.
Therefore, you need the array controller to find out when you add items to your array. The best way to make this happen is to bind the array controller's contentArray to a property of your controller, and update that property in a KVO-compliant manner.
That means:
Create the mutable array in your init method. (And, of course, release it in dealloc.)
Implement the array accessor methods, plus addTracksObject: and removeTracksObject: (which are technically set accessor methods, so KVO will ignore them for an array property) for your convenience.
To add a track, send yourself an addTracksObject: message. You should respond to that by sending yourself an insertObject:inTracksAtIndex: message (with [self countOfTracks] for the index, unless you want to do an insort), and you should respond to insertObject:inTracksAtIndex: by sending your tracks array an insertObject:atIndex: message.
As I mentioned, KVO will ignore addFooObject: and removeFooObject: when foo is an NSArray property, considering those only NSSet-property accessors, so you need to implement them on top of insertObject:inFooAtIndex: and removeObjectFromFooAtIndex: because those are array accessors, which means KVO will react to them.
Step 3, as I just described it, will be pretty slow, because it will cause the array controller to re-fetch your property and the table view to re-fetch the array controller's arrangedObjects at least once each for every row you add.
So, you should maintain your batch-adding behavior with this alternate step 3:
Implement insertTracks:atIndexes:, and pass it an array of one batch of (e.g., 100 or 1000) tracks and an index set formed by [NSIndexSet indexSetWithRange:(NSRange){ [self countOfTracks], countOfBatch }]. You'll also need to implement removeTracksAtIndexes:, only because KVO will ignore each insert method if you don't also have its counterpart.
You probably should have the array controller set to attempt to preserve the selection, so as not to frustrate the user too much while you're still bringing in rows.
Also, you may want to create the objects on a background thread, periodically sending yourself another batch to add using a main-thread perform. I'm ordinarily an advocate of doing things on the main thread run loop whenever possible, but this sort of thing could easily make your interface laggy while your periodic load builds up another batch.
You need to call setNeedsDisplay:YES on your table view on the main thread. Don't call it from a background thread. All Cocoa UI calls must be done on the main thread or weird things happen.