UITabBarController seems to open one view before closing another - ios

I have a method that is called when a Settings button is tapped in my root view, that subclasses two UIViewControllers, attaches them to a UITabBarController and pushes the UITabBarController onto a navigation stack:
-(IBAction)onSettings:(id)sender {
// Create the Settings Views
SettingsViewController *vcSettings1 = [[Settings1ViewController alloc] initWithNibName:#"Settings1ViewController" bundle:nil];
Settings2ViewController *vcSettings2 = [[Settings2ViewController alloc] initWithNibName:#"Settings2ViewController" bundle:nil];
// Create the Tab View
UITabBarController *tabController= [[UITabBarController alloc] init];
tabController.viewControllers = #[vcSettings1,vcSettings2];
// Pass the Index of the database on to the views so they can pull the record from the database
vcSettings.recordIndex = recordIndex;
vcSettings2.recordIndex = recordIndex;
// Add the tab bar controller to the navigation stack
[self.navigationController pushViewController:tabController animated:YES];
}
In each of the Settings views, I override the viewWillAppear method to load a row of data from an sqlite database at primary key recordIndex. (Both views pull the same record and display different data from the record, except one field is the same on both.)
I also override viewWillDisappear in each view to save the controls data back to the database.
I can verify that every time I switch views using the tab bar, the viewWillDisappear method is called on one closing view and the viewWillAppear is called on the opening view.
The problem is that when I change data on the first view and switch to the second view, the data is not changed on the second view unless I return to the first view and then back to the second. As best I can tell, here is what seems to be happening:
View 1 is open. I change the data in the field.
I tap on the tab for View 2
viewWillAppear is called for View 2, populating the field in View 2 with the old data in the database.
viewWillDisappear is then called for View 1, saving the changed data to the database.
It seems that the opening view is calling viewWillAppear BEFORE the closing view is calling viewWillDisappear.
I have tried this other ways, such as using a singleton, and simply trying to modify the recordIndex from both views and in all cases it seems that the data is loaded from the opening view before it is saved from the closing view.
Is this a bug in the way UITabBarController works, or am I abusing viewWillAppear and Disappear in a way that I'm not supposed to? Has anyone else run across this behavior?

This is not a good design. There is no guarantee that viewWillAppear of new view should be called after viewWillDisappear from the previous one. Even if that were to work, there would be no guarantee that this would keep working in future iOS versions.
If performance is not impacted, a quick fix could be to save the changes to the database as they occur, you would always have an up to date database that could be accessed from any view in any circumstance.
Best design is to have some model classes, with your model objects accessible through a singleton for example. Those model objects are updated real time as you interact with the UI, they are the ones being accessed by the different views, and they are periodically saved using the method of your choice.

as a cheap solution you could load data in viewDidAppear but in general I agree with JP's answer

Related

Code in viewDidLoad runs every time it is called

Hi all I am doing a course in Udemy, and the code calls for placing code in the viewDidLoad function as shown below:
override func viewDidLoad() {
super.viewDidLoad()
placesArray.append(["name":"Taj Mahal", "lat":"27.175607", "lon":"78.042112"])
}
The array append should only run once, however, when I segue to another viewController and come back, it runs the code to append again. So I now have an array with 2 rows, both of which are Taj Mahal.
I thought that the viewDidLoad function only runs code once?
Is there a way around this?
Thanks.
Addendum:
I am using Swift, so I don't see any alloc and init while creating and launching the viewController. And weird as it sounds, the video tutorial has it working in the viewDidLoad and the trainer is using the storyboard to segue from the initial table view controller to a map view on a view controller and just has a back button on the map view that segue's back to the table view controller via the storyboard as well. - Could be because I have the latest version of the Swift language and the trainer was using an earlier version, (cause I noticed some slight differences in coding earlier) but you never know. Either way whenever he touches the back button it does not run the append code anymore.
I am trying to get in contact with the trainer as some of the suggestions here, though they are good don't seem to work.
I will put the solution in here once I get in contact with the trainer.
The viewDidLoad method is called when your view controller's view finishes loading. The view will load when a view controller's view property is nil and something attempts to access it.
UIViewController *myVC = [[UIViewController alloc] init];
UIView *aView = myVC.view; // this loads myVC's view; viewDidLoad is called when it completes loading.
If the view has unloaded (usually due to memory limitations), it will be called when the view property is next accessed.
Manipulation of data sets should generally not be done within view methods. Consider moving this to the init of the view controller (or to a different "datasource" class).
I suppose you are trying to do data initialisation in viewDidLoad. If there is no other operation on placesArray before viewDidLoad, then instead of append, what about setting the placesArray directly:
placesArray = ["name":"Taj Mahal", "lat":"27.175607", "lon":"78.042112"]
Then even if your view is unloaded for some reasons. Taj Mahal will still be added once only.
viewDidLoad is called whenever the view controller's view property is set. When does this happen? It depends on how the view controller is contained:
UINavigationController
- View Controller views are loaded as they are added to the navigation stack and "unloaded" (although the viewDidUnload method is deprecated) as they are removed.
UITabBarController
- View Controller views are loaded as they are added to the tab bar regardless of whether they are on screen or not. They stay loaded as you change from tab to tab.
Depending on your needs and use case, you can create your own view controller container that does what you need. Checkout the Apple docs on the proper way to do this:
https://developer.apple.com/library/ios/featuredarticles/ViewControllerPGforiPhoneOS/CreatingCustomContainerViewControllers/CreatingCustomContainerViewControllers.html

Keep UI data when navigating between views

In my test project I have a ViewController and a TableViewController controller embedded in a Navigation Controller. The ViewController is the main view, and the user can navigate to the TableViewController and then return back to the ViewController.
I am using a 'push' segue when going from ViewContoller>TableViewController, and the TableViewController is dismissed using [self dismissViewControllerAnimated:YES completion:nil]; when the user wishes to go back to ViewController.
In ViewController, I have a button that changes the text on a label:
-(IBAction)onButtonPress:(id)sender {
_myLabel.text = #"New Label Text";
}
When navigating to TableView, and then back to ViewController, the change in the _myLabel.text has been lost and the original text is restored. What is the best way to ensure UI data is retained when navigating between views? I might only have one label in this project, but at some point I will have many UI elements, for example, WebViews that need to keep a page loaded when the user navigates away and then comes back.
What would you suggest is the best method to implementing this?
Thanks for your time.
First of all, View is your data representation, any data is your Model. So you may store Model with its entities and values. Your controllers manage data to represent it on View or to change Model according to user actions in View.
So add some Model-object for storing _myLabel.text. And use this object for both controllers in your app.
Try to remove setting the text from the ViewWillAppear or ViewDidAppear method, put it in ViewDidLoad.

Making a popover segue´s View Controller stay persistent (only allocate one instance)

I programmed my app initially for iPhone using a tab bar controller were the view controllers are initialized once and stays persistent - it does not initialize a new instance of the view controller when I tap the tab bar.
on the iPad I am using a different GUI were instead I have one main view that always stays on the screen, and the rest are popovers segueing from the main view.
I want the popovers to stay persistent (only initialize once) what is the best way of archiving this. If I had been using *.xib files I could have initialized the popover´s view controllers in the main view and then sent a copy of them when segueing, and that way only ever have one instance of them. But I am using Storyboards.
You can't use segues if you want your controllers to be persistent, because segues always instantiate new controllers. You can still use the storyboard, but you have to leave the controllers unconnected, and instantiate them in code, and assign them to a strong property. So, something like:
-(void)presentPopover {
if (! self.vc) {
self.vc = [self.storyboard instantiateViewControllerWithIdentifier:#"MyController"];
}
// do what you want here to put vc on screen
}
I found a solution and actually it´s easy, just use a UIPopoverController and initialize it with the view controller you want to present. In this way it will not instantiate a new instance each time a popover is requested.
if (!popoverController)
popoverController = [[UIPopoverController alloc]initWithContentViewController:bellViewController];
[popoverController presentPopoverFromBarButtonItem:sender permittedArrowDirections:UIPopoverArrowDirectionAny animated:YES];
popoverController.delegate=self;

Conventional way to implement master-detail view controllers in iOS

I just have a quick question about recommended ways to implement a master-detail view hierarchy in iOS--the kind where selecting a row in a table on one screen pushes a details view for that item onto the navigation stack.
Specifically, should I reuse the same instance of the details view controller and just change its target and reload it each time, or should I instantiate a new instance of the view controller each time?
I'd prefer the first method, as it just seems generally more efficient, but I'm having trouble figuring out how to set the target and do the reload (especially the first time, when the view controller has not yet even been initialized--I'm using storyboards and that pretty much handles all of the initialization itself).
Or perhaps instead of setting the target on the child view controller, I could set it on the parent, such that each time the child view controller is shown, it reloads itself based on the parent selection? That actually sounds like the best bet so far, just looking for tips/warnings from anyone who's run into this before.
First, there's nothing wrong with creating a new view controller each time. If you use segues, that's what you'll get, since segues always instantiate new controllers. The detail controller will be deallocated when you pop or dismiss it anyway, so it won't persist.
If you want to use the same controller, you have to do your push or presentViewController in code. You can still setup the controller in the storyboard. Give it an identifier, but don't connect it up with a segue. In code, you check for the existence of your controller (you'll need a property for it), and if it doesn't exist, create it.
if (! self.detailController) {
DetailController *dvc = [self.storyboard instantiateViewControllerWithIdentifier:#"MyIdentifier"];
}
self.dvc.whateverProperty = self.somePropertyIWantToPass; // pass some date to it
[self.navigationController pushViewController:dvc animated:YES completion:nil];

Specifying a Delegate before ViewDidLoad in Tabs

So, I followed this tutorial: http://enroyed.com/ios/how-to-pass-data-between-ios-tab-bars-using-delegate/
And the most important part of the tutorial:
- (void)viewDidLoad
{
[super viewDidLoad];
SecondViewController *svc = [self.tabBarController.viewControllers objectAtIndex:1];
svc.delegate = self; //important !! otherwise delegation will not work !
}
The problem is that even if I put it in "viewWillLoad", it still forces me to click on my tab before it initializes. How can I specify this before I click on the tab?
Edit
I have a three tab project. I used that tutorial in the link pass data from tab 1 to tab 2. The data passed is a url from a webview on tab 1 to a url on tab 2. The url gets pass when I click a link on the 1st tab.
The data does get passed, but only if I physically click on the 2nd tab first and then click back to the 1st and click on the link.
So, it appears to me that my code above only runs if I physically click on the 2nd tab.
Your problem is that - until you actually go to tab item 2, secondViewController is not fully initialised, and so there is no data to transfer from vc2 to vc1. In particular, secondViewController's view has not yet loaded, so there is no value to be had from it's slider yet, and no slider, so also no IBAction method to call to trigger the delegate method. Indeed, as the data transfer is only triggered on moving the slider in VC2, it should be fairly obvious that until you go to vc2 and move the slider, nothing is going to happen.
The example you link to uses the delegation pattern, which seems a fairly poor way to deal with your problem. The delegation pattern most comfortably fits with the scenario where there is a hierarchical relationship between delegator (owned) and delegatee (owner) ... not always, but most commonly. In a tab bar controller, the relationships are more like kindred child relationships to the tab bar controller itself.
You haven't offered enough detail in your question as to what you want to achieve, but you need to consider this:
When a tabBarController loads, all of it's child viewControllers are initialised but their views are not loaded.
This means that these methods do get called:
//if loading from a xib or in code
- (id) initWithNibName:bundle
//if loading from a storyboard
- (id) initWithCoder:
- (void) awakeFromNib
But the view loading methods (viewDidLoad, viewWillAppear etc) do not get called as the view does not get loaded unless you actually open the relevant tab.
You could solve this by putting an initialised variable into your viewController2 (in one of the init methods) and accessing that variable via property syntax from vc1. But then, you might as well just put an initialised value directly into vc1. You need to think closely about how each vc is dependent on the other, how you can decouple that dependency, and perhaps how to set up an independent data source that both vcs can access as needed. This could be a model class, or NSUserDefaults, or a property in your appDelegate... just a few of the many possible solutions.

Resources