Goal: I'm trying to restore state on a tab controller-based app (with navigation controllers on each tab).
Problem: On relaunch, the selected tab seems to be restored as expected, but the navigation hierarchy inside said tab is not.
Development:
I first started with the project template "Tab based app".
Next, I added restoration IDs to both child view controllers and the tab bar controller.
In the app delegate, I implemented application(_:shouldSaveApplicationState:) and application(_:shouldRestoreApplicationState:).
I run then app, switch to the second (right) tab, hit home, terminate. o relaunch, the right tab is displayed (as expected). So far so good.
Next, I go to the storyboard and embed both child view controllers in respective navigation controllers, and assign restoration IDs to those too.
I run the app, and restoration still works. Still good.
Next, I add a "detail" view controller; its class is a custom subclass of UIViewController to the storyboard, with properties to configure the contents of a debug label and its view's background color.
I placed a "Show Detail..." button on each of the tabs' top view controllers, and create a segue from each into the (shared) detail view controller. So now my storyboard looks like a hexagon (also, both segues have identifiers set in Interface Builder). So, both left and right top view controllers share the same type of "detail" view controller. On show, it is configured so as to distinguish from where it has been pushed (see next point).
On each of the top view controllers' prepareForSegue(_:sender:) method, I configure the pushed detail view controller differently: Different text and background color ("left" and blue, and "right" and red, respectively).
I added code to the detail view controller to save and restore the state of the text and background color properties: encodeRestorableStateWithCoder(_:) and decodeRestorableStateWithCoder(_:). Also, I implemented viewDidLoad() so as to reflect those properties' values in the view. Whenever it is instantiated and pushed into the navigation through a segue, the properties are first set and then used to configre the view in viewDidLoad(). Whenever it is instantiated during restoration, the properties are set in decodeRestorableStateWithCoder(_:) and similarly used in viewDidLoad().
...but when I run this code, the last selected tab is restored but only up to the top view controller -left or right-, not the detail. Interestingly, the background color last set to the detail view controller flashes for an instant.
I placed break points in encodeRestorableStateWithCoder(_:) and decodeRestorableStateWithCoder(_:), but only the first of them is executed (encode).
Wondering what might be missing, I went ahead and implemented the app delegate's application(_:viewControllerWithRestorationIdentifierPath:coder:)(returning always nil, but logging the path components passed).
The documentation is not very clear on whether this method is needed or not, and in any case all view controllers except the detail seem to be restored perfectly even without it. I added code to instantiate each view controller based on the last path component (i.e., that controller's restoration ID) and returning it.
Now, the decodeRestorableStateWithCoder(_:) is called, but the navigation still goes back to the tab's top view controller after a split second.
So, what is going on? What am I missing to implement State Preservation and Restoration in a Tab Bar + Navigation Controller app?
FIXED: So, there were several problems with my code...
It turns out that in my case, I do not need to implement application(_:viewControllerWithRestorationIdentifierPath:coder:). (see the comments of this answer)
My implementations of encodeRestorableStateWithCoder(_:) and decodeRestorableStateWithCoder(_:)
were not calling super (as suggested in the accepted answer to the question above).
finally, I got the right view controller (detail) to appear, but its subviews' state (text label contents and main view background color) were in the initial, empty state (not being restored to their last state -i.e., text label contents and bg color). As mentioned in this question, the viewDidLoad() is not called right after decodeRestorableStateWithCoder(_:) (like I assumed), so instead I call a common method from both viewDidLoad() and decodeRestorableStateWithCoder(_:) to update the UI.
As usual, I rushed to post a question before searching or trying enough modifications in my code (my apologies...).
I hope this at least helps someone else.
As usual, I'll wait a couple of days before accepting my own answer, in case somebody sheds additional light.
Related
I am converting my single view application into one that is based on tabs. I have most of the layout done so I'm trying to wire up all the components now and I'm stuck trying to figure out how to do two things:
When I pick a bonus from my UITableView, I want it to open my 2nd tab with the info for the selected row. I'm not sure how to do that.
If I go straight to one of the other tabs, I want to have it just display the details for the first visible row of the UITableView. I'm not sure how to set such a default value.
I'm not exactly sure what code you would need to see for the above, but I am using Xcode 9 and Swift 4. I've googled and searched YouTube and Stack Overflow, and the answers are either all for Objective-C or are about the UITabBar obscuring the last row of UITableView data (which is not an issue I'm having).
EDIT: I seem to have semi-gotten it to work by changing my prepare (for segue) and then via Interface Builder, deleting the segue from the UITableView to the tab's viewController. However, this still doesn't light up the proper tab in the UITabBar. Also, this is still wrapped in the original Navigation Controller. Which allows me to move back and forth, but isn't the intent. When I tried removing the Navigation Controller, I had to use a "Show" or "Modal" type segue, which covers up the UITabBar, and offers no way to get back out of the detail view. I want to use the UITabBar to provide the back and forth that the Navigation Controller used to handle so I can gain the space at the top of the screen back.
The simplest is probably to register a Notification Center observer in your second tab's view controller. Whenever you have an item to show, simply trigger the notification from your first tab's view controller attaching the object you need to show to the notification.
I have an application with a drop down menu as the titleView of my NavigationController. When a user selects an item of the drop down menu, the entire view should switch contents, however, the NavigationBar should remain the same. The NavigationBar should not have to reload any data and the titleView should remain a drop down menu.
The original view upon opening the app:
The view upon touching the dropdown menu:
I currently see a few ways of going about this:
Set up a UIViewController for each option, perform the segue, and reload the data.
Why this is bad: I will have to set up a segue identifier for each ViewController, meaning if I have 15 options in my drop down menu, I will have 210 segue identifiers laying around. I will also have to reload all of my NavigationBar data.
Why this is good: I will have a clear area to set up each individual view.
Programmatically add and remove UIButtons, UILabels, and UIWhatevers as I need them.
Why this is bad: This will create a lot of code inside just one ViewController and things could get difficult to debug.
Why this is good: The NavigationBar never gets reloaded.
Add a container and embed a unique ViewController for each item as I need it.
Why this is bad: All of my work would still be in the main ViewController and I'd have to manage the logic of the embedded ViewController inside one Controller.
Why this is good: The NavigationBar never gets reloaded.
A completely different method suggested by someone else because I don't know the most efficient way of doing this.
So, in conclusion, what is the most efficient way to maintain state of my NavigationBar when switching my main content in my View?
Option 3 is the best out of the three you listed. Options 1 and 2 will get more and more complicated the more view controllers you want to add. Compare that to UINavigationController, UITabBarController, or UIPageViewController which do not need to be more complicated in order to handle 10 screens vs. 100 screens.
I would suggest creating a custom container view controller (Apple's Reference)
I see 2 immediate approaches to implementing this:
Subclassing UIViewController - this is how Apple's container view controllers are implemented
Subclass UITabBarController - I have done this successfully, subclassing UITabBarController to show a custom tab bar at the top instead of along the bottom.
I am answering my own question in this post.
Requirement: I want to have a tab view controller as the top parent. This will have 5 tabs. In the first tab, I want to have a segment control at the top which I want to switch the views within this first tab with information based off of which segment index is selected.
Reading a few suggestions online were to-
Either use containment view controllers where the first tab holds strong references to the child view controllers and switches them based off of which index is selected in the segment control. Problem with this is that each of the view controller will be eating up memory as we are keeping them in strong reference.
Second idea I read online was to put everything on a single view controller in the first tab - based on which index segment control is selected, just hide everything else. Problem with this was super messy code with too much stuff on the same view controller plus storyboard would get messy with things on top of each other.
Solution I came up was to embed another tabbarcontroller inside the first tab's view controller. Hide this second tab bar. This second tab bar will have the 3 child view controllers - each for each segmentcontrol's index. Whenever the segment is changed, I change the tab.
So in the second tabbarcontroller (self is the second tabbarcontroller)
-(void)segmentChanged:(UISegmentedControl*)sender{
NSLog(#"New value: %d",sender.selectedSegmentIndex);
[self setSelectedIndex:sender.selectedSegmentIndex];
}
This way iOS will put the view controllers in memory only when the segments are switched and not from the very beginning. Also iOS UIKit will handle the memory management for the tabs as mentioned here.
Plus we don't have to deal with messy code and storyboard shenanigans. Each segment control's index logic is separate in it's own view controller from the second tab.
i have implemented a tabbarcontroller with 5 tabs each connected to a view.
the tabBarcontroller is created and default view allocation happens in another view.
everythine looks fine but i have a bug.
each view in the tab have buttons that trigger other views. these views don't have tabs so to get back to a tab view i use a back button.
when i press the back button i don't want the view ( with the tab ) to be created from scratch. so i have put the relevant code in viewdidload rather than viewwillappear.
( this choice is because this view downloads data from server and it becomes time consuming to put the code at this point in viewwillappear )
However if i am returning from current tab to a tab that i had previously touched and viewed. I want view to be loaded from scratch because data should be downloaded again at this point. but since i am using viewdidload rather than viewwillappear the old view is not refreshed.
how can i achieve this conditional refreshing of view depending upon whether i am coming from another tab or a from a view with back button
One way to do this is the isMovingToParentViewController method in your view controller. This will enable you to detect if your current stack has been popped from navigation controller.
There's an answer here that explains how to use it, and links to Apple's documentation.
As you can see from that thread, you can also use NSNotificationCenter to do this.
Let's say I have 4 items in my UITabBar: A B C D.
Via the delegate methods, D pushes a new viewController from the UITabBar's navigationController, (removing the tabBar for that one view).
This works fine, but how can I keep the UITabBar from showing a blank view for D when going back from the new view?
I tried setting the selectedIndex to the previous index on push, but that just hangs the app (seems to work fine for modals, just not when pushing.)
If I interpret this correctly, when you're pushing a view as a result of selecting a tab, you're simultaneously changing the tab that's selected. This is probably trying to then change the view hierarchy that you're currently pushing a view on (that would be the result of selecting the other tab that you're trying to set it to). So it's probably crashing because it's replacing a view that's in the process of being displayed.
Also, from the sound of it you're using a UITabBarController inside of a UINavigationController. Apple says that you're not supposed to place a UITabBarController inside another view controller. The following is taken from Apple's docs on UITabBarController:
When deploying a tab bar interface, you must install this view as the root of your window. Unlike other view controllers, a tab bar interface should never be installed as a child of another view controller.
If this is the case, you should redesign your app so that you're not containing the tab bar controller in any other view controllers, or your app may behave oddly or stop working at some point.
If you wanted to make things behave exactly how you want them to, you could use a UITabBar directly, and implement your own UITabBarDelegate.