IOS: Programmatic rootViewController renders the view differently to Storyboard 'isInitialView' - ios

With the aim to implement a splash screen that only shows once i've modified didFinishLaunchingWithOptions in order to dynamically select the appropriate view controller. The logic seems to work fine, and the view I intended to load is the one launched
However, the UI seems to be missing elements that would otherwise be displayed should I have not altered the didFinishLaunchingWithOptions function.
func application(application: UIApplication!, didFinishLaunchingWithOptions launchOptions: NSDictionary!) -> Bool
{
window = UIWindow(frame: UIScreen.mainScreen().bounds)
let storyBoard = UIStoryboard(name: "Main", bundle: nil)
var entryViewController: UIViewController?
if NSUserDefaults.standardUserDefaults().boolForKey("hasSeenWelcomeScreen") == true
{
entryViewController = storyBoard.instantiateViewControllerWithIdentifier("NavigationController") as? UIViewController
}
else
{
entryViewController = storyBoard.instantiateViewControllerWithIdentifier("WelcomeViewController") as? UIViewController
NSUserDefaults.standardUserDefaults().setValue(true, forKey: "hasSeenWelcomeScreen")
NSUserDefaults.standardUserDefaults().synchronize()
}
self.window?.rootViewController = entryViewController
self.window?.makeKeyAndVisible()
return true
}
My WelcomeViewController is a simple view with 1 label, 1 button and a movie which plays in the background (resembling Spotify/Vine's welcome screen). Debugging the code I can see the initialization methods do get executed, but is just the frame that does not seem to be displayed when I dynamically override the initial view
import UIKit
import MediaPlayer
import QuartzCore
class WelcomeViewController: UIViewController {
var moviePlayerController: MPMoviePlayerController = MPMoviePlayerController()
#IBOutlet weak var loginButton: UIButton!
#IBOutlet weak var appNameLabel: UILabel!
override func viewDidLoad()
{
super.viewDidLoad()
buildMoviePreview()
buildButtonDesign()
}
override func viewWillAppear(animated: Bool)
{
self.view.addSubview(self.moviePlayerController.view)
self.view.addSubview(self.loginButton)
self.view.addSubview(self.appNameLabel)
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
}
override func prefersStatusBarHidden() -> Bool {
return true
}
private func buildButtonDesign()
{
loginButton.layer.borderColor = UIColor.whiteColor().CGColor
loginButton.layer.borderWidth = 2.0
loginButton.layer.cornerRadius = 7.0
}
private func buildMoviePreview()
{
let filePath = NSBundle.mainBundle().pathForResource("intro", ofType: "mov")
self.moviePlayerController.contentURL = NSURL.fileURLWithPath(filePath)
self.moviePlayerController.movieSourceType = .File
self.moviePlayerController.repeatMode = .One
self.moviePlayerController.view.frame = self.view.bounds
self.moviePlayerController.scalingMode = .AspectFill
self.moviePlayerController.controlStyle = .None
self.moviePlayerController.allowsAirPlay = false
self.moviePlayerController.shouldAutoplay = true
self.moviePlayerController.play()
}
}
For completeness, these are the discrepancies in the layout when using the XCode UI debugger. Please note that they differ even though they implement the same viewController. The only difference is that one has been programmatically set as the initial view, while the other has been set as the initial view through storyboard.
Screenshots of the rendering issue side-by-side

Your approach is... unusual. A Storyboard has a root view controller for a reason and typically at startup you would just let the application handle loading the storyboard and installing that root view controller as the window's main view controller. (The Storyboard loaded is specified in the application target's general settings as the "Main Interface")
In this case, what I would recommend is making your root view controller the "Normal" view of the application... the one you want users to see when they launch the app on a day-to-day basis.
Define your "on first launch" view controller as a separate view controller in the storyboard and add a modal segue from the root view controller to the on first launch view controller.
Then in your applicationDidFinishLaunching, if the user has never seen the first launch controller... simply ask the Storyboard to take that segue. If the user has already seen the first launch presentation that segue will be skipped.
Another issue I see with your code is in your viewWillAppear method. You should not have to add your views as subviews in viewWillAppear... those subviews should already have been set up at the time the view was loaded from the nib file.
The one exception is the view of your movie player, but your movie player is owned by a separate view controller. That separate view controller is detached from the view controller hierarchy and does not have it's own view controller methods called at the right times. (so it never receives calls like "viewWillAppear" that might tell it to get it's movie ready to play).
What you probably want to do is implement "awakeFromNib" and make sure that the movie player's view controller is a sub-controller of this view controller. (so in awakeFromNib for the WelcomeViewController use addChildViewController to make sure the movie controller is in the hierarchy).

It will be better to user 2 storyboards :
one with your welcome screen
the other one with the rest of your app
The application launching wil look like this :
func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: NSDictionary?) -> Bool {
var storyBoard : UIStoryboard!
if NSUserDefaults.standardUserDefaults().boolForKey("hasSeenWelcomeScreen") == true {
changeStoryBoard("Main")
}
else {
changeStoryBoard("Welcome")
}
return true
}
func changeStoryBoard(name :String) {
var storyBoard = UIStoryboard(name:name, bundle: nil)
var viewController: AnyObject! = storyBoard.instantiateInitialViewController() ;
self.window!.rootViewController = viewController as UIViewController
}

Related

How to keep a UIView always showing over multiple views

I wish to create and present a view over each view controller in my app allowing users to interact with it or the view controllers beneath it. I want the view to always appear on top of whatever view I may present or segue to.
I have a custom UIView that appears when users tap a table view cell. Within the didSelectRowAt tableView function I have tried:
(UIApplication.shared.delegate as! AppDelegate).window?.addSubview(self.subView!)
and
self.view.addSubview(self.subView!)
Both function similarly with the view appearing over the current View controller and allowing users to interact with the table still. But when I present a new ViewController the subView disappears as it has not been added to the new View.
Subclass UIWindow, and override addSubview() to make sure your overlay view is always on top:
weak var overlay: MyOverlayView!
override func addSubview(_ view: UIView) {
if let overlay = view as? MyOverlayView {
self.overlay = overlay
super.addSubview(overlay)
} else {
self.insertSubview(view, belowSubview: overlay)
}
}
You must make sure the app delegate uses your custom class as the app's main window, and not the superclass UIWindow. For this, remove the "Main storyboard file base name" entry from your Info.plist and instead instantiate the main window manually in your AppDelegate:
#UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
var window: MyCustomWindow?
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil) -> Bool {
self.window = MyCustomWindow(frame: UIScreen.main.bounds)
// 1. Add your overlay view to the window
// (...)
// 2. Load your initial view controller from storyboard, or
// instantiate it programmatically
// (...)
window?.makeKeyAndVisible()
return true
}
Note: I haven't tested this code. Let me know in the comments if there's a problem.

UISplitViewController as child of custom tab bar controller

I've been following this tutorial to create a custom tab bar controller for an iPad app as I would like to implement a vertical tab bar. However, I would like one of the tabs to present a UISplitViewController, whilst the others just present UIViewControllers. My questions are:
1) Will this be accepted by the app store? Apple's documentation currently states adding UISplitViews as child views is not recommended but may be implemented with certain containers. Anyone had any experience with this?
2) Here is an extract from my custom tab bar controller. If secondViewController is presenting the UISplitView, can I leave it as is? I mean, it seems to work find when I run it, but is it acceptable?
class CustomTabBarController: UIViewController {
#IBOutlet weak var tabView: UIView!
#IBOutlet var tabButtons: [UIButton]!
var firstViewController: UIViewController!
var secondViewController: UISplitViewController!
var thirdViewController: UIViewController!
var viewControllerArray: [UIViewController]!
var selectedTabIndex: Int = 0
override func viewDidLoad() {
super.viewDidLoad()
let storyboard = UIStoryboard(name: "Main", bundle: nil)
firstViewController = storyboard.instantiateViewController(withIdentifier: "firstVC")
secondViewController = storyboard.instantiateViewController(withIdentifier: "secondVC") as! UISplitViewController
thirdViewController = storyboard.instantiateViewController(withIdentifier: "thirdVC")
viewControllerArray = [firstViewController, secondViewController, thirdViewController]
tabButtons[selectedTabIndex].isSelected = true
didPressTab(tabButtons[selectedTabIndex])
}
3) I can't really get my head around what (if anything) needs to go in AppDelegate? Again seems to run fine but just wondering if its safe.
Thanks.
1) I believe Apple is simply recommending against this as potentially bad design, since they refer you to the Human Interface Guidelines. You don't always have to agree with their recommendations and very rarely will your app get rejected for design choices- the only instances off the top of my head would be mimicking the App Store or other core OS functionality.
2) If, as you say, this is working, I don't see any glaring issue.
3) Again if it's working, you may not need to do anything. But here's how Apple sets up their template for a Master-Detail app:
If your splitViewController is set up like this and you want the same functionality as this template, here's how you should be able to get it.
First add this to the very bottom of AppDelegate.swift:
extension AppDelegate: UISplitViewControllerDelegate {
func splitViewController(_ splitViewController: UISplitViewController, collapseSecondary secondaryViewController:UIViewController, onto primaryViewController:UIViewController) -> Bool {
guard let secondaryAsNavController = secondaryViewController as? UINavigationController else { return false }
guard let topAsDetailController = secondaryAsNavController.topViewController as? DetailViewController else { return false }
if topAsDetailController.detailItem == nil {
// Return true to indicate that we have handled the collapse by doing nothing; the secondary controller will be discarded.
return true
}
return false
}
}
Then, add this to the end of viewDidLoad in CustomTabBarController:
guard let appDelegate = UIApplication.shared.delegate as? AppDelegate else {return}
let navigationController = secondViewController.viewControllers[secondViewController.viewControllers.count-1] as! UINavigationController
navigationController.topViewController!.navigationItem.leftBarButtonItem = secondViewController.displayModeButtonItem
secondViewController.delegate = appDelegate

An alternative to "Storyboard Reference" for iOS 8 when handling Relationship Segue?

My App has a TabBarViewController containing 4 tabs. One of the tabs is Settings which I want to move to a separate storyboard. If I am only consider iOS 9 and above as my deployment target, then I can just refactor the SettingsTab using Storyboard Reference. However I want to target iOS 8 as well. Since Storyboard Reference doesn't support Relationship Segue, I can't rely on it in this case.
So in the main storyboard which contains the TabBarViewController, I keep a dummy SettingsTabViewController as an empty placeholder. And in the function "viewWillAppear" in its class file, I push the view to the real SettingsTabViewController in the Settings.storyboard. This works fine. But the problem is if I keep tabbing the Settings tab, the empty placeholder view controller will show up for a short time and then goes back to the real Settings view.
I tried to implement this delegate to lock the Settings tab:
func tabBarController(tabBarController: UITabBarController, shouldSelectViewController viewController: UIViewController) -> Bool {
return viewController != tabBarController.selectedViewController
}
However, the other three tabs were locked too after I implemented this delegate.
Is it possible to just lock the Settings tab without locking other three tabs? And in which view controller exactly should I implement this delegate?
Yes, it's possible. You need to check the index;
with the following code not only you can prevent locking other tabs, but also you still have tap on tab goto root view controller feature.
func tabBarController(tabBarController: UITabBarController, shouldSelectViewController viewController: UIViewController) -> Bool {
let tappedTabIndex = viewControllers?.indexOf(viewController)
let settingsTabIndex = 3 //change the index
if tappedTabIndex == settingsTabIndex && selectedIndex == settingsTabIndex {
guard let navVC = viewController as? UINavigationController else { return true }
guard navVC.viewControllers.count > 1 else { return true }
let firstRealVC = navVC.viewControllers[1]
navVC.popToViewController(firstRealVC, animated: true)
return false
}
return true
}
.
This answers your question, but still you would have the settingsVC showing up for a moment. To avoid this you simply need to turn off the animation while you're pushing it. so you need to override viewWillAppear in the following way.
override func viewWillAppear(animated: Bool) {
super.viewWillAppear(animated)
if let theVC = storyboard?.instantiateViewControllerWithIdentifier("theVC") {
navigationController?.pushViewController(theVC, animated: false)
}
}
after adding above code you still would see a back button in your real first viewController. You can hide it:
override func viewDidLoad() {
super.viewDidLoad()
navigationItem.hidesBackButton = true
}

Navigation Bar gone

How can I present a view controller from my AppDelegate and have a Navigation bar added to that view with a back button to the previous view? I need to do this programmatically from my AppDelegate. Currently I can push a controller from there, but it doesn't act like a segue. It doesn't add a nav bar with a back button. Now I know I should be able to add one myself, but when I do it gets hidden. Currently I'm using pushViewController(), but I imagine that's not the best way to do it.
I had something that I think is similar, if not the same:
HIGH LEVEL VIEW
The general composition of my App (thus far, and specific to the issue at hand - note: details about classes provided for context, not required for resolution) is as follows:
UIViewController (ViewController.swift) embedded in a UINavigationController
Buttons on UIViewController segue to a view with a custom class:
ExistingLocationViewController - subclass of:
UITableViewController
One of the buttons (Add New Location) in the UINavigationController's Toolbar segues to view with another custom class:
NewLocationViewController - subclass of:
UIViewController
CLLocationManagerDelegate
UITextFieldDelegate
There are a number of other items here, but I believe the above is sufficient as the foundation for the issue at hand
RESOLUTION
In order to preserve the navigation-bar (and tool-bar) going both forward and back - I have the following code in my custom classes (note: the following is Swift-3 code, you may have to adjust for Swift-2):
override func viewDidLoad() {
super.viewDidLoad()
//...
navigationController?.isNavigationBarHidden = false
navigationController?.isToolbarHidden = false
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated) // #=# not sure if this is needed
navigationController?.isNavigationBarHidden = false
navigationController?.isToolbarHidden = false
}
You could actually omit the last two lines in viewWillDisappear, or perhaps even omit the entire override function
The net result (for me) was as depicted below:
Hope that helps.
If you want add a NavigationController in appDelegate you can do it like this,in this way,your viewcontroller is load from storyboard
func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
// Override point for customization after application launch.
self.window = UIWindow(frame: UIScreen.mainScreen().bounds)
let vc = UIStoryboard(name: "Main", bundle: NSBundle.mainBundle()).instantiateViewControllerWithIdentifier("vc") as! ViewController
let nav = UINavigationController(rootViewController: vc)
self.window?.rootViewController = nav
self.window?.backgroundColor = UIColor.whiteColor()
self.window?.makeKeyAndVisible()
return true
}

Parse Facebook User logged in, perform segue

I am trying to skip over my login view once a user is logged in. How can I check to see if a user is logged in through Facebook while the app is starting?
I currently have the following code in a LoginViewController:
override func viewWillAppear(animated: Bool) {
var loggedIn = PFFacebookUtils.session().isOpen;
if (loggedIn) {
performSegueWithIdentifier("skipLogin", sender: self)
}
}
This does not move to my next view even after the user has clicked the "Log in with Facebook" button.
I get the following error:
Warning: Attempt to persent <_Project.HomeViewController: 0x7fa331d3af00> on
<_Project.LoginViewController: 0x7fa331f08950> whose view is not in the window hierarchy!
As discussed in chat, you have basically two options here:
Let the user "see" the animation from the login view controller to the second one. In that case you should do the push in viewDidAppear instead of viewWillAppear (where the view is not fully prepared, as the runtime warning clearly states).
If you prefer showing the final view controller immediately, without any animation, then it's better to put that logic inside your app delegate, and choose which initial view controller should be loaded from here. In that case, you're not actually performing any segue, you're just assigning one or another view controller to the main window (or your navigation controller).
Parse has the "AnyWall" sample app that implements the second logic. See here for more details: https://parse.com/tutorials/anywall#2-user-management. In particular, the chapter 2.4 is of special interest, as it explains how you can keep a user logged-in.
Simply put, here's how they did it (I've adapted their Objective-C code to Swift):
func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
...
navigationController = UINavigationController()
...
// If we have a cached user, we'll get it back here
if PFFacebookUtils.session().isOpen {
// A user was cached, so skip straight to the main view
presentWallViewController(animated: false)
} else {
// No cached user, go to the welcome screen and
// have them log in or create an account.
presentLoginViewController(animated: true)
}
...
window = UIWindow(frame: UIScreen.mainScreen().bounds)
window.rootViewController = navigationController
window.makeKeyAndVisible()
return true
}
In each of the two methods present...ViewController, they use the following skeleton:
func presentxxxViewController(#animated: Bool) {
NSLog("Presenting xxx view controller")
// Go to the welcome screen and have them log in or create an account.
let storyboard = UIStoryboard(name: "Main", bundle: nil) // Here you need to replace "Main" by the name of your storyboard as defined in interface designer
let viewController = storyboard.instantiateViewControllerWithIdentifier("xxx") as xxxViewController // Same here, replace "xxx" by the exact name of the view controller as defined in interface designer
//viewController.delegate = self
navigationController?.setViewControllers([viewController], animated: animated)
}
The navigationController and window vars should be defined like this in AppDelegate:
class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
var navigationController: UINavigationController?
...
}
If your app also uses a navigation controller as its root view controller, you can probably use the same code.

Resources