Overview of my app
-Throughout I use a navigation controller, it lies in a corresponding XIB MainWindow.xib. It is set as the root vc in the app delegate.
-CategoriesVC is a table vc, it is the top VC in the root vc.
-BooksCategoryVC (all books in a category) is another table vc that is pushed after selecting something from CategoriesVC
-BookScrollVC.m is a scroll vc that displays the text after selecting a book.
Code description
1) In my app delegate I have set shouldSaveApplicationState and shouldRestoreApplicationState to return YES
2) CategoriesVC lies in the MainWindow XIB and I have set the restoration identifier in IB and in .m I have set the restorationClass to self and implemented the viewControllerWithRestoreIdentifierPath
3) BooksCategory has its own XIB and implemented the restoration protocol, pretty much same as
4) Same as 3) except the VC is instantiated in code. No XIB here.
Flow
Start the application. Navigate all the way to the last VC (BookScrollVC).
When pressing Home Button in the simulator:
encodeRestorableStateWithCoder is called in CategoriesVC and doesn't proceed to the other VCs. Shouldn't it go through all VC that have implemented the restoration protocol?
When restarting the app from Xcode, indeed only the viewControllerWithRestorationIdentifierPath in the CategoriesVC is called.
Please let me know if anything is unclear or you wish to see code
Solution
The app cannot restore if the restorationidentifier is not set. Altough I have set them in my custom initialisers it didn't seem to work. The solution was to set the restoration identifier on the next vc, before it was pushed.
I will have to dive into this and see if there's a better solution.
Related
Summary:
I'm writing a Swift iOS app with a login screen and several other views in a tab view controller. I'm transitioning from one viewcontroller to another via the "control" + left click -> "Show" method. I want to make sure I'm not designing my iOS app incorrectly with memory leaks or other flaws.
Relative Questions:
Does this mean a new view of that ViewController is created each time "Show" is called?
Could this cause a memory leak or the app to crash?
Do I need to unwind the ViewControllers at some point?
What is the best way to unwind a ViewController when launching another ViewController?
It seems what you are talking about is manually creating a Show Segue (a transition, made via the Storyboard with ctrl + click and drag to another ViewController). This is one correct way to create a Segue (transition) from one ViewController to another. To utilize this, you will need to use the left panel on the Storyboard, give this segue an identifier, and use this identifier to preform a segue from the first ViewController to the second in some sort of method or action (like a button click, etc) using the performSegue method:
self.performSegue(withIdentifier: "NameOfSegue", sender: self)
Here's more info on segues from the docs:
https://developer.apple.com/library/content/featuredarticles/ViewControllerPGforiPhoneOS/UsingSegues.html
You can read even more detail in the "Modifying a Segue’s Behavior at Runtime" section. Here's a quote: "Most of the work happens in the presenting view controller, which manages the transition to the new view controller. The configuration of the new view controller follows essentially the same process as when you create the view controller yourself and present it." Memory leaks shouldn't be an issue here. Unwind segues let you dismiss view controllers that have been presented, but they are not always needed.
Let's say we have a CustomNavigationController that subclass UINavigationController.
Let's use the following example to explain the question.
CustomNavC -> pushes on VC A. From VC A you can push on 2 different VC's. VC B and VC C. We'll say both these VC's push on various other VC's, further down the rabbit hole.
Now, let's say we want to show a UIView that acts as a banner view appearing directly underneath the navigation controller. However, we only want the banner to show on say VC A, VC C, VC E, VC J, etc.
Is there any possible way to do this from the CustomNavC itself? Or is the only way to gain this control of which VC's show the banner... is to put it on the VC's itself?
1) We put it on the CustomNavC view. When the user moves from a VC to a VC that both show the banner (A->C), we want to same banner to remain. We gain this by laying out the banner on the CustomNavC. However, how can we check whether a VC should be displaying the banner or not? Every time the NavC pushes or pops a VC, we would have to check. Likely some function on each VC like -(BOOL)allowBannerViewDisplay and VC's can opt in.
2) If we put the BannerView on individual VC's, it becomes a bit easier but the deal breaker is that if we move from VC A -> VC C, the user is going to see 2 separate banners during the transition instead of the same banner.
So, we need to solve it way 1. The CustomNavC listens for a notification and displays the banner. It would then need to check the currently displaying VC and only display the banner if the VC allowed it. However, if the user transitions to another VC, it needs to recheck the logic of whether the banner is currently displaying and if it is, check if that VC wants it to display.
All of this feels weird to me.
Suggestions?
I've never implemented something like this, but off the bat my thought is this:
Set up this custom view as a new class, and pass the reference along your VCs. Set up a method on the custom View as a class method that takes in a UIViewController and returns a bool for whether or not it should be displayed (rather than on each of your VCs). This class method can get the class and see if it exists in an array of class names or something.
Now, I've never moved a UIView from one VC to another, but I think it's possible to remove the UIView as a subview from the VC it's on, and place it on the new one as a subview.
Alternatively, perhaps you could have a datasource/delegate object for this custom view, and create a new custom UIView on each VC as needed that all reference the same datasource / delegate to set up the state appropriately.
I recently changed my app structure to include a UINavigationController as base for my hierarchy and I had its root viewController implement the UINavigationControllerDelegate protocol in order to implement custom segues.
My viewController implements navigationController:animationControllerForOperation:fromViewController:toViewController:.
My problem is two-fold:
The navigationController.delegate methods are not being called.
The navigationBar is not called in the views being pushed via storyboardSegues of type show.
The prepareForSegue:sender: function is being called.
This is my UI:
Turns out that UIStoryboardSegues I added before I added the UINavigationController to my hierarchy are still interpreted as modal segues. Probably this is set during creation.
The problem was solved by deleting and re-adding the segues in question, with the relevant information (identifier, class...) transferred to the new instance.
If you have the same problem, when you set Top Par to inferred in your segued viewController you will see no navigationBar showing.
After replacing the segues the Top Bar showed again as normal.
Edit:
I posted the question together with this answer, since there was no post on SO covering this issue. self-answer
My storyboard has: NavigationController -> UIViewController 1 -> UIViewController 2 -> UIViewController 3 -> UITabBarController -> four UIViewControllers (a,b,c,d)
I'm adding restoration support to the app. I've added it to UIViewController #1 and #3 and am now adding it to UIViewController 'c' under the UITabBarController.
When I kill my app using Xcode when it's displaying UIViewController 'c' and then start the app again the viewcontroller's methods are called correctly and the content of its UITextFields are restored correctly.
But in placing breakpoints in 'c' view controller I can see that UIViewController 3 is temporarily displayed. It's viewDidLoad and viewWillAppear: methods are called.
What would typically be the cause of UIViewController #3 being displayed?
I've pressed on implementing restoration for my UIViewControllers and there doesn't appear to be any negative effect from an end-users point of view - without the breakpoints the display doesn't get a chance to update to show the intermediate view.
That's what view controller restoration is. The app launches from scratch; to reach the view controller that was being displayed at termination, the app walks the view controller creation sequence down the chain from the root view controller to the view controller that is to be displayed to the user. It couldn't possibly happen any other way. (After all, if VC2's Back button is to lead to VC1, then VC2 must be above VC1 in the nav controller stack.) And you wouldn't want it not to happen, because you might have important work to do as the sequence is reconstructed. By setting breakpoints in the middle of the restoration process, you have revealed "the man behind the curtain". But the user doesn't see that process (it is covered by the launch image), so there's no problem.
In particular it is very important that viewDidLoad is called in correct sequence right down the chain of creation, and that this happens for all view controllers before they are sent decodeRestorableStateWithCoder:, also in order down the chain.
(However, you should not count on anything having to do with viewWillAppear: and its cousins. These may or may not be called and you can't be sure just when. I regard this inconsistency / unpredictability with regard to the timing of viewWillAppear: as a bug, but Apple, unfortunately, does not.)
My app has a simple organization, which I've configured in an Interface Builder storyboard (not in code). There is a Navigation View Controller, which has its Root View Controller set to my Main View Controller. My Main View contains a table, where cells segue to a Detail View Controller.
When I suspend the application while looking at the Detail View and then resume it, I'm returned to the Main View, rather than the Detail view. Why might this be?
Details:
I have set Restoration IDs in Interface Builder for the Navigation View Controller, the Main View Controller and the Detail View Controller. I've also tried adding a Restoration ID to the Table View and making the Main View Controller implement UIDataSourceModelAssociation.
My app is returning YES from shouldRestoreApplicationState and both the Main View and the Detail View have encode/decodeRestorableStateWithCoder methods.
I'm testing suspend/resume using the simulator: I run the app, navigate to the Detail View, hit the home button, and then click the stop button in XCode. To resume, I'm running the app again from XCode.
I see the following calls on suspend:
AppDelegate shouldSaveApplicationState
MainViewController encodeRestorableStateWithCoder
DetailViewController encodeRestorableStateWithCoder
And on resume:
AppDelegate shouldRestoreApplicationState
AppDelegate viewControllerWithRestorationIdentifierPath Navigation
AppDelegate viewControllerWithRestorationIdentifierPath Navigation/MainView
MainViewController viewDidLoad
AppDelegate viewControllerWithRestorationIdentifierPath Navigation/DetailView
MainViewController decodeRestorableStateWithCoder
In addition to the wrong view being restored, there's something else odd: Why is the Restoration Identifier Path for the Detail View "Navigation/DetailView" and not "Navigation/MainView/DetailView"? There is no direct relationship between the Navigation View Controller and the Detail View Controller. Their only connection in Interface Builder is via the segue from the Main View.
Have I misconfigured something?
I have tried assigning a Restoration Class to the Detail View. When that restoration code is invoked, it fails because the UIStateRestorationViewControllerStoryboardKey is not set in the coder.
Here's a toy version of my project which replicates the problem: https://github.com/WanderingStar/RestorationTest
I'm trying this with XCode Version 5.0 (5A1413) and iOS Simulator Version 7.0 (463.9.4), in case those are relevant.
The answer turned out to be simple: I was not calling
[super encodeRestorableStateWithCoder:coder];
in the encodeRestorableStateWithCoder:coder method in my View Controllers (and doing the same in decode...) which is what sets the storyboard in the coder.
This tutorial helped me step through each step of the process, and find out where I'd gone wrong:
http://useyourloaf.com/blog/2013/05/21/state-preservation-and-restoration.html
Also, it turns out that "Navigation/DetailView" is what's expected. The Navigation View Controller restores all of the views in its stack and then puts them back into the stack, rather than each view restoring the later views in the stack.
In the iOS App Programming Guide, section "State Preservation and Restoration" there is a convenient checklist for what you have to do to make restoration work.
After looking at your code it seems that you forgot step 3, Assign Restoration Classes. Your classes do not have these properties, and you did not implement viewControllerWithRestorationIdentifierPath in the app delegate.
Assign restoration classes to the appropriate view controllers. (If you do not do this, your app delegate is asked to provide the corresponding view controller at restore time.) See “Restoring Your View Controllers at Launch Time.”
I took a look at your sample and the applicationWillFinishLaunching is missing [self.window makeKeyAndVisible] which is a requirement for state restoration. This will make the split controller immediately collapse and then it will be restored correctly.
There is an issue that if it was preserved in landscape, i.e. separated split view , and then launched in portrait then the path will not be correct. In this case at launch it will first collapse to match the current screen, then it begin restore and first separate, then after restore has finished it will collapse again to match the current screen. During this time you need to implement viewControllerWithRestorationIdentifierPath and use the last string in the path to identify the controller and return it after having captured it from what the storyboard created initially in will finish launching. Then you can clear those properties in didFinish.