Why is my UIViewController's view nil? - ios

I'm new to presenting UIViewcontrollers programmatically, and I'm having some issues presenting its view. Here is my code:
override func willBecomeActive(with conversation: MSConversation) {
let controller = instantiateUserStickersController()
self.addChildViewController(controller)
self.view.addSubview(controller.view) //This line throws the error
}
private func instantiateUserStickersController() -> UIViewController {
guard let controller = storyboard?.instantiateViewController(withIdentifier: "UserStickersViewController") as? UserStickersViewController else { fatalError("Unable to instantiate a UserStickersViewController from the storyboard") }
return controller
}
Why can't I access the controller's view? I'm sure I set the storyboard ID correctly
Here's my storyboard:

The view should not exist before you present the view controller to kick off its lifecycle. So only access the new controller's view once it has been presented:
self.addChildViewController(controller)
self.present(controller, animated: true) {
self.view.addSubview(controller.view)
}
This worked for me.

Related

Swift: Dismissing all view controllers and then presenting a view controller

I'm looking for a way to dismiss all presented view controllers, and THEN present a view controller.
In my app, there's a main page, and the user can then click on a button that takes them to another page, and then they can click a button to submit some information. After they click to submit the evidence, I want to close all of the view controllers (so they get to the main page), and then I want to present a "Congratulations" screen. Ideally, this would be what I want to do:
self.view.window?.rootViewController?.dismiss(animated: true, completion: {
let congratsPopup = K.mainStoryBoard.instantiateViewController(withIdentifier: "congratsController") as! CongratsController
self.view.window?.rootViewController!.present(congratsPopup, animated:true, completion:nil)
})
Any ideas?
Cheers,
Josh
You can dismiss all viewcontrollers with below code block. In the completion block you can get the topViewController and you can present new viewController over topViewController. I also wrote down an extension for get the topViewController on the window.
UIApplication.shared.keyWindow?.rootViewController?.dismiss(animated: true, completion: { [weak self] in
// Get Top Controller With Extension
let topController = UIApplication.topViewController()
// Pressent New Controller over top controller
let congratsPopup = K.mainStoryBoard.instantiateViewController(withIdentifier: "congratsController") as! CongratsController
topController?.present(congratsPopup, animated: true, completion: nil)
})
Get Top View Controller Extension
extension UIApplication {
class func topViewController(controller: UIViewController? = UIApplication.shared.keyWindow?.rootViewController) -> UIViewController? {
if let navigationController = controller as? UINavigationController {
return topViewController(controller: navigationController.visibleViewController)
}
if let tabController = controller as? UITabBarController {
if let selected = tabController.selectedViewController {
return topViewController(controller: selected)
}
}
if let presented = controller?.presentedViewController {
return topViewController(controller: presented)
}
return controller
}
}

Swift - dismiss then pop VC

How would I dismiss a modal View Controller and also its parent that was pushed?
self.presentingViewController?.dismiss(animated: true, completion: {
self.parent?.navigationController?.popViewController(animated: true)
})
This only dismisses the top modal.
You can go another way.
First, you have UINavigationController in your Home view. So you can write an extension that will allow you to go to the controller, which is in the navigation stack.
I tried making an implementation like this:
extension UINavigationController {
func routingPath(for controller: UIViewController) -> [UIViewController] {
guard viewControllers.contains(controller) else {
return []
}
var result: [UIViewController] = []
for previousController in viewControllers {
result.append(previousController)
if controller === previousController {
break
}
}
return result
}
func performNavigation(toPrevious controller: UIViewController,
shouldDismissModals: Bool = true) {
let previousViewControllers = routingPath(for: controller)
guard !previousViewControllers.isEmpty else { return }
viewControllers = previousViewControllers
if shouldDismissModals, let _ = controller.presentedViewController {
controller.dismiss(animated: true, completion: nil)
}
}
}
Then you can make a special method for UIViewController:
extension UIViewController {
func returnBackIfPossible(to controller: UIViewController? = nil,
shouldDismissModals: Bool = true) {
navigationController?.performNavigation(toPrevious: controller ?? self,
shouldDismissModals: shouldDismissModals)
}
}
Then you need to pass a reference for a Home controller to all of the next controllers (or store it somewhere). Next, when needed, you can call to homeViewController?.returnBackIfPossible() method, which will close all modals and reset navigation stack.
What is non-modal parent exectly?
Is it a view controller pushed by the navigation controller?
If then, you must pop that view controller from navigation controller.

Modifying SwipeViewController Pod to run in a ContainerView

So im using this pod 'SwipeViewController' (https://github.com/fortmarek/SwipeViewController) that really gives me the effect that I want but the problem is that it only runs as the main navigation controller and I want it to work in a container view because I want to use it for this social app in the profile menu like twitter does with "My Tweets", "Likes", "Repost"...
so for example I need this...
https://camo.githubusercontent.com/f4eb2a8ba0a11e672d02a1ef600e62b5272a7843/687474703a2f2f696d6775722e636f6d2f5344496b6634622e676966
to work in here:
So just an explanation, to make the pod work you need to add this line of code to the appDelegate
let pageController = UIPageViewController(transitionStyle: .Scroll, navigationOrientation: .Horizontal, options: nil)
let navigationController = YourViewControllerName(rootViewController: pageController)
self.window?.rootViewController = navigationController
self.window?.makeKeyAndVisible()
wich creates a new window with the SwipeController, I need a way to make it work in a View controller.
I did something similar last night using the tutorial here:
https://cocoacasts.com/managing-view-controllers-with-container-view-controllers/
My associated code is below. I created an IBOutlet from the container view to my ViewController and then the code below adds the appropriate view controller to the container view depending on the setting of my Bool called buttonDefault. Make sure to add the child view to your container view and not the main view from the view controller.
#IBOutlet weak var containerView: UIView!
// MARK: Container View
// https://cocoacasts.com/managing-view-controllers-with-container-view-controllers/
lazy var remoteViewController: RemoteViewController = {
// Load Storyboard
let storyboard = UIStoryboard(name: "MainStoryboard", bundle: nil)
// Instantiate View Controller
var viewController = storyboard.instantiateViewController(withIdentifier: "RemoteViewController") as! RemoteViewController
// Add View Controller as Child View Controller
self.add(asChildViewController: viewController)
return viewController
}()
lazy var gestureRemoteViewController: GestureRemoteViewController = {
// Load Storyboard
let storyboard = UIStoryboard(name: "MainStoryboard", bundle: nil)
// Instantiate View Controller
var viewController = storyboard.instantiateViewController(withIdentifier: "GestureRemoteViewController") as! GestureRemoteViewController
// Add View Controller as Child View Controller
self.add(asChildViewController: viewController)
return viewController
}()
func add(asChildViewController viewController: UIViewController) {
// Add Child View Controller
addChildViewController(viewController)
// Add Child View as Subview
containerView.addSubview(viewController.view)
// Configure Child View
viewController.view.frame = containerView.bounds
viewController.view.autoresizingMask = [.flexibleWidth, .flexibleHeight]
// Notify Child View Controller
viewController.didMove(toParentViewController: self)
}
func remove(asChildViewController viewController: UIViewController) {
// Notify Child View Controller
viewController.willMove(toParentViewController: nil)
// Remove Child View From Superview
viewController.view.removeFromSuperview()
// Notify Child View Controller
viewController.removeFromParentViewController()
}
func updateView() {
let settings = Settings()
if settings.buttonDefault {
remove(asChildViewController: gestureRemoteViewController)
add(asChildViewController: remoteViewController)
} else {
remove(asChildViewController: remoteViewController)
add(asChildViewController: gestureRemoteViewController)
}
}
Once you add this just call updateView() in your viewDidLoad and any time the user selects a new option to view.
I hope this helps.

How to access my custom UIPresentationController from within presented controller?

This is how I perform transitioning:
extension UIViewController: UIViewControllerTransitioningDelegate {
public func presentationController(forPresented presented: UIViewController, presenting: UIViewController?, source: UIViewController) -> UIPresentationController? {
return OverlayPresentationController(presentedViewController: presented, presenting: presenting)
}
func presentOverlayController(_ controller: UIViewController) {
controller.modalPresentationStyle = .custom
controller.transitioningDelegate = self
present(controller, animated: true)
}
}
And then within my presented controller (AlertVC) at some point I need to access its presentation controller:
print(presentationController as? OverlayPresentationController) //nil
print(presentationController) //is ok, UIPresentationController
Why?
Presenting:
let controller = AlertVC.instantiate()
controller.update()
presentOverlayController(controller)
class AlertVC: UIViewController {
class func instantiate() -> AlertVC {
return UIStoryboard(name: "Alert", bundle: Bundle(for: LoginVC.classForCoder())).instantiateInitialViewController() as! AlertVC
}
func update() {
_ = view
print(presentationController as? OverlayPresentationController) //nil
}
}
You get the view controller that is presenting the current view controller by calling presentingViewController
// The view controller that presented this view controller (or its farthest ancestor.)
self.presentingViewController
If your presenting view controller is in a navigation controller that returns a UINavigationController. You can get the view controller you need like so:
let presentingNVC = self.presentingViewController as? UINavigationViewController
let neededVC = presentingNVC.viewControllers.last as? NeededViewController

Present UIAlertController in the currently active UIViewController

I've got a timer showing an alert when finished.
This alert view should be presented in the view controller which the user is currently in.
My feeling is this could be accomplished much more effective than the following:
The way I'm doing this now is give an observer for a notification to each of my 5 view controllers as well as a method to create and present that alert.
Is there a way to only set up the alert once and then present it in the view controller that is currently active?
Here's my code:
// I've got the following in each of my view controllers.
// In viewDidLoad()
override func viewDidLoad() {
super.viewDidLoad()
NSNotificationCenter.defaultCenter().addObserver(self, selector: #selector(SonglistViewController.presentSleepTimerFinishedAlert(_:)), name: "presentSleepTimerFinishedAlert", object: nil)
}
func presentTimerFinishedAlert(notification: NSNotification) {
let alertController = UIAlertController(title: "Timer finished", message: nil, preferredStyle: UIAlertControllerStyle.Alert)
alertController.addAction(UIAlertAction(title: "OK", style: UIAlertActionStyle.Default, handler: nil))
presentViewController(alertController, animated: true, completion: nil)
}
Thanks a lot for any ideas!
extension UIApplication {
/// The top most view controller
static var topMostViewController: UIViewController? {
return UIApplication.shared.keyWindow?.rootViewController?.visibleViewController
}
}
extension UIViewController {
/// The visible view controller from a given view controller
var visibleViewController: UIViewController? {
if let navigationController = self as? UINavigationController {
return navigationController.topViewController?.visibleViewController
} else if let tabBarController = self as? UITabBarController {
return tabBarController.selectedViewController?.visibleViewController
} else if let presentedViewController = presentedViewController {
return presentedViewController.visibleViewController
} else {
return self
}
}
}
With this you can easily present your alert like so
UIApplication.topMostViewController?.present(alert, animated: true, completion: nil)
One thing to note is that if there's a UIAlertController currently being displayed, UIApplication.topMostViewController will return a UIAlertController. Presenting on top of a UIAlertController has weird behavior and should be avoided. As such, you should either manually check that !(UIApplication.topMostViewController is UIAlertController) before presenting, or add an else if case to return nil if self is UIAlertController
extension UIViewController {
/// The visible view controller from a given view controller
var visibleViewController: UIViewController? {
if let navigationController = self as? UINavigationController {
return navigationController.topViewController?.visibleViewController
} else if let tabBarController = self as? UITabBarController {
return tabBarController.selectedViewController?.visibleViewController
} else if let presentedViewController = presentedViewController {
return presentedViewController.visibleViewController
} else if self is UIAlertController {
return nil
} else {
return self
}
}
}
You can find the Top ViewController on the navigation stack and directly present the AlertController from there. You can use the extension method posted here to find the Top ViewController from anywhere in your application:
https://stackoverflow.com/a/30858591/2754727
It really depends on your navigation schema.
First of all you will need current VC. If you've got root view controller as navigation controller and don't show any modals you can get current VC from rootVC. If you've got mixed navigation. i.e. tabbar and then navigation controllers inside, with possible some modals form them you can write an extension on AppDelegate which will search and return current VC.
Now you should pin somewhere this timer class - it may be a singleton or just be pinned somewhere. Than in this timer class, when the timer ends you can look for current VC (using AppDelegate's extension method or referring to your root navigation controller) an present an alert on it.

Resources