I have two storyboards one of them is default called Main and the other one I have just added is called Admin.
Main is used for customer, Admin is used for owner of the app.
I wonder how do you set initial/main interface storyboard programmatically.
P.S. I know how to change it via xcode but do not know programatically.
You do not set an initial storyboard programmatically.
Here's how it works. Either you have a main storyboard listed in Xcode under Main Interface or you don't:
If you do, that is the initial storyboard, period. That storyboard is loaded automatically at launch time, and its initial view controller becomes the window's root view controller (and the interface is then displayed automatically).
If you don't (that is, if the Main Interface field is empty), then nothing happens automatically. It is up to your code to obtain a view controller, somehow (possibly from a storyboard), and make its the window's root view controller (and to show the interface).
So, to sum up, either everything happens automatically or nothing happens automatically. There is no intermediate state, as you seem to imagine, in which you can programmatically change things so that a different storyboard is loaded automatically.
There is, however, a kind of intermediate state where you permit the Main Interface storyboard to load but then you ignore it. In your implementation of application:didFinishLoading..., you would then sometimes do the thing I said in the second bullet point, i.e. load a different view controller and make it the root view controller. This works because the automatic stuff I mentioned in the first bullet point has already happened by the time application:didFinishLoading... is called. So, in effect, your code sometimes permits that automatic stuff and sometimes overrides it.
In this example from my own code, we either use the Main Interface storyboard to load the initial view controller or we load a different view controller from the storyboard ourselves, depending on a value in User Defaults:
func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
if let rvc = self.window?.rootViewController {
if NSUserDefaults.standardUserDefaults().objectForKey("username") as? String != nil {
self.window!.rootViewController = rvc.storyboard!.instantiateViewControllerWithIdentifier("root")
}
}
return true
}
You can set your storyboard programmatically like this in the app delegate:
window = UIWindow(frame: UIScreen.main.bounds)
// Or "Admin"
window!.rootViewController = UIStoryboard(name: "Main", bundle: nil).instantiateInitialViewController()!
window!.makeKeyAndVisible()
You could actually set any view controller. That's how no-storyboard approach is wired up.
Oh, another advantage of doing it in code is it delays the storyboard initialization. If you use the "Main Interface" settings in Xcode, the storyboard is actually initialized BEFORE the application:didFinishLaunchingWithOptions method.
Related
If I have an Xcode project that already uses a Storyboard, and now I want to switch to loading the view controller programmatically, how do I do that?
Here is an in-depth article about doing this. The TLDR is:
In the project settings, clear the Main Interface:
In your didFinishLaunching use code such as the following:
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
window = UIWindow(frame: UIScreen.main.bounds)
let homeViewController = UIViewController()
homeViewController.view.backgroundColor = UIColor.red
window!.rootViewController = homeViewController
window!.makeKeyAndVisible()
return true
}
Slowly. One step at a time.
Storyboards are not all-or-nothing. You can continue to use a Storyboard for places where it is helpful, and you can use programmatic loading for places where it isn't, and you can transition between them one step at a time.
A first step if you wanted to unwind a piece from the Storyboard is to remove the segues leading in and out of it, and then programmatically load that view controller from the Storyboard with instantiateViewController(withIdentifier:).
You could then pull a given scene into its own separate Storyboard (select the scene, Editor, Refactor to Storyboard...) so that it's completely independent of all the other scenes. Then you can rework that one view controller as programmatic code without interrupting anything else in the project. You can also mix-and-match here; loading the view controller from the Storyboard, but handling initialization by hand in init(nibName:bundle:).
You can also cut and paste a storyboard view into its own XIB file, which sometimes is easier to work with, without giving up the GUI layout system. Select the view, copy, create a new XIB file (it's called "View" in the templates), and paste it. This is a bit easier to use with init(nibName:bundle:).
But the key point is to not try to rework your entire project all at once. If there's some portion where Storyboards are causing trouble, refactor them out. If you just like programmatic code better, then keep refactoring one step at a time until you're done or until you've done enough to be happy.
As Senseful's answer notes, the top-level scene is slightly special. You can change it with or without changing the other scenes.
I'm a Swift beginner, so take it easy on me. I'll be specific in my question.
I deleted the storyboard (I want to learn how to build UI programmatically using Swift).
The below code is placed in the AppDelegate.swift
I also have ViewController.swift in the project explorer.
Firstly, is there a way to use anything other then UINavigationController..? Or having a UINavigationController is a must requirement..?
If not a must, how can I just refer it to a ScrollView for example..?
2ndly, with further research into Apple's own guide, they stated I can also use window?.isHidden = false... Is there a difference between using the former line, and window?.makeKeyAndVisible()..?
Sorry if my question doesn't make sense programmatically, like I said, I'm a beginner, but I'm determined to understand why I write the code I write or copy.
Thank you.
class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
// Override point for customization after application launch.
window = UIWindow(frame: UIScreen.main.bounds)
window?.makeKeyAndVisible()
window?.rootViewController = UINavigationController (rootViewController: ViewController())
return true
}
...
}
A window needs a view controller. It doesn't have to be a UINavigation Controller, but it does need to be a view controller. Not all apps start with a navigation controller. In fact, you can see this yourself by looking at the storyboard that Xcode provides for you in new projects. It's just a ViewController. Can you use a ScrollView instead? If you put the scrollview (which is a UIView) into a UIViewController, sure you can.
Hiding a window just makes it appear and disappear. Making it the key window means that it will be the window that receives events. You might not think that it is important in iOS apps, but iOS apps can have more than one window. In the case of starting an application, it's a good idea to define the window that will be visible and be key, not just the one that will be visible.
Firstly, is there a way to use anything other then UINavigationController..? Or having a UINavigationController is a must requirement..?
You can use any UIViewController subclass as the root view controller. e.g. UITabBarController, UIPageViewController, UIViewController, UITableViewController...
how can I just refer it to a ScrollView for example..?
You can't set a UIScrollView as the root view controller. You can, however, add the UIScrollView as a subview of the UIWindow. I don't recommend you to do this though, because using VCs will make your code more manageable, with different classes managing different views.
Is there a difference between using the former line, and window?.makeKeyAndVisible()..?
Yes, if you look at the docs of makeKeyAndVisible:
This is a convenience method to show the current window and position it in front of all other windows at the same level or lower. If you only want to show the window, change its
isHidden property to false.
So yeah, calling makeKeyAndVisible will make the window the "key window".
According to here, key window behaves like this:
The key window receives keyboard and other non-touch related events. Only one window at a time may be the key window.
I have a standard split view controller that I want to always show it's master view on iPads.
In the master view's viewDidLoad, I'm calling:
self.splitViewController.preferredDisplayMode = UISplitViewControllerDisplayModeAllVisible;
With this one line in place, I'm getting my desired effect (always shows the master). However, a strange thing also happens: The top layout guide appears to move to the top of the master view, under the navigation bar.
You can see the effect in this picture, where the first cell in the table view is partially hidden behind the navigation bar. In fact, there is a green view above it (it's a placeholder for something that will be there soon) that is totally hidden.
If I rotate the device to landscape then back to portrait, autolayout works as expected, and the views appear in the correct place:
I've tried the following in both viewDidLoad and viewDidAppear to try to force the views to lay out properly, but it's had no effect:
[self.splitViewController.view setNeedsLayout];
[self.splitViewController.view layoutIfNeeded];
I'm looking for any solutions/suggestions.
I want to answer my own question with a hindsight point of view.
Apple mentioned in its documentation:
When building your app’s user interface, the split view controller is typically the root view controller of your app’s window.
And later warns:
You cannot push a split view controller onto a navigation stack. Although it is possible to install a split view controller as a child in some other container view controllers, doing is not recommended in most cases. Split view controllers are normally installed at the root of your app’s window. For tips and guidance about ways to implement your interface, see iOS Human Interface Guidelines.
Although they're not outright saying never to use UISplitViewController as a non-root view controller, I have found that UISplitViewController behaves unreliably when used in a non-root manner. The bug mentioned in the question is just one of many other problems you will encounter, the worse of which (in my experience) is not propagating-viewWillAppear or -viewDidAppear calls to child view controllers.
If you want a non-root split view, I suggest roll your own custom split view.
The only thing that's worked for me is this:
https://stackoverflow.com/a/22084634/1919412
In my case, the issue only shows up the first time the user rotates from landscape to portrait mode (and without manually revealing the master pane first). So even though Rivera's workaround causes a jarring visual blip, it only has to happen at most once per launch.
I recently ran into the same issue and solved it by setting the preferredDisplayMode on launch like so:
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
...
if let splitController = self.window?.rootViewController as? UISplitViewController {
splitController.preferredDisplayMode = .allVisible
}
...
return true
}
Well:
I was facing the same problem (I guess).
But If I call:
.preferredDisplayMode=UISplitViewControllerDisplayModePrimaryOverlay
It worked like a charm.
I have a class MasterViewController that loads on app startup. Inside viewDidLoad() it checks if a user is logged in, and presents one view controller or another based on the outcome. If you aren't logged in and then proceed to do so, the app loads up a new MasterViewController. My goal is to essentially replace the existing MasterViewController with a new instance so it performs the check in viewDidLoad once again. I've tried the following, and they both work:
// changing the root view controller
let appDelegate = UIApplication.sharedApplication().delegate as! AppDelegate
appDelegate.window = UIWindow(frame: UIScreen.mainScreen().bounds)
appDelegate.window!.rootViewController = MasterViewController()
appDelegate.window!.makeKeyAndVisible()
// using presentViewControllerAnimated
someViewController.presentViewControllerAnimated(MasterViewController(), animated: true, completion: nil)
...but while presentViewControllerAnimated has a nice animation, changing root doesn't. More importantly, changing the root view controller doesn't destroy the existing one (at least deinit is never called..), and obviously presentViewControllerAnimated doesn't do that either, so in both cases I have this view controller floating around that I don't want anymore.
I can just imagine some scenario where a user logs out and back in repeatedly and suddenly I have 10 MasterViewControllers on top of one another. Any way to completely purge a view controller? Or is this just totally unnecessary?
EDIT
Just remembered presentViewControllerAnimated is for presenting a vc modally, so that's definitely not what I want. Would be nice to change the root view controller with a similar animation though. All the animations I've seen with root vc changes were pretty wonky.
The following Stackoverflow thread touches on your concern about the old UIViewController sticking around in memory:
Stackoverflow: Changing root view controller of a iOS Window
serge-k's answer comes from the UIWindow Class Reference:
var rootViewController: UIViewController?
The root view controller for the window.
The root view controller provides the content view of the window.
Assigning a view controller to this property (either programmatically
or using Interface Builder) installs the view controller’s view as the
content view of the window. If the window has an existing view
hierarchy, the old views are removed before the new ones are
installed. The default value of this property is nil.
The Resource Management in UIViewController section of the UIViewController Programming Guide also contains a note on this:
The default behavior for a view controller is to load its view hierarchy
when the view property is first accessed and thereafter keep it in memory
until the view controller is disposed of. The memory used by a view to
draw itself onscreen is potentially quite large. However, the system
automatically releases these expensive resources when the view is not
attached to a window. The remaining memory used by most views is small
enough that it is not worth it for the system to automatically purge and
recreate the view hierarchy.
You can explicitly release the view hierarchy if that additional memory
is necessary for your app. Listing 4-3 overrides the
didReceiveMemoryWarning method to accomplish this. First, is calls the
superclass’s implementation to get any required default behavior. Then,
it cleans up the view controller’s resources. Finally, it tests to see if
the view controller’s view is not onscreen. If the view is associated
with a window, then it cleans up any of the view controller’s strong
references to the view and its subviews. If the views stored data that
needs to be recreated, the implementation of this method should save that
data before releasing any of the references to those views.
Following code and concept is helpful for you
var window: UIWindow?
var viewController: MasterViewController?
var navigationVC:UINavigationController?
func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool
{
window = UIWindow(frame: UIScreen.mainScreen().bounds)
if let window = window {
window.backgroundColor = UIColor.whiteColor()
viewController = MasterViewController(nibName: "MasterViewController", bundle: nil);
//If you want NavigtionController
navigationVC = UINavigationController(rootViewController: viewController!)
navigationVC?.navigationBar.hidden = true
window.rootViewController = navigationVC
//Or If you don't want navigation controller
window.rootViewController = viewController
window.makeKeyAndVisible()
}
return true
}
Here
1.The root view controller provides the content view of the window. Assigning a view controller to this property (either programmatically or using Interface Builder) installs the view controller’s view as the content view of the window.
2.If the window has an existing view hierarchy, the old views are removed before the new ones are installed.
3.The default value of this property is nil.
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.