Presenting view controllers on detached view controllers - ios

I have sideViewController with a button and Action, which present new view controller by clicking this button.
class sideViewController: UIViewController {
#IBOutlet var buttonVC1 : UIButton!
#IBAction func goToVC1 () {
var VC1 = self.storyboard.instantiateViewControllerWithIdentifier("ViewController") as ViewController
presentViewController(VC1, animated:true, completion: nil)
}
}
I use this in main view controller:
class ViewController: UIViewController {
var menu : sideViewController!
override func viewDidLoad() {
super.viewDidLoad()
menu = self.storyboard.instantiateViewControllerWithIdentifier("menu") as sideViewController
menu.view.frame = CGRect(x: 0, y: 0, width: 160, height: 480)
view.addSubview(menu.view)
}
when I click this button, the problem is: "Presenting view controllers on detached view controllers is discouraged"
What should I do to fix this?

I just ran into this same warning myself, and realized that I'm getting it because when I was calling
self.presentViewController
I was calling it on a view controller that wasn't attached to the UIWindow through the view hierarchy. You need to change what your doing to delay calling presentViewController until you know the view is on the view stack. This would be done in ViewDidLoad or ViewDidAppear, or if your coming from a background state, waiting until your app is in the active state

Use this to make sure you are on the main thread
dispatch_async(dispatch_get_main_queue(), { () -> Void in
self.presentViewController(VC1, animated: true, completion: nil)
})

Problem
iOS is complaining that some other view(the detached view) which came after the main view is presenting something. It can present it, which it does apparently, but it's discouraged as it's not a good practice to do so.
Solution
Delegate/protocol pattern is suitable to solve this issue. By using this pattern, the action will be triggered inside the SideVC although this trigger will be sent to the MainVC and be performed there.
Therefore, since the action will be triggered by the MainVC, from iOS's perspective, it will all be safe and sound.
Code
SideVC:
protocol SideVCDelegate: class {
func sideVCGoToVC1()
}
class sideVC: UIViewController {
weak var delegate: SideVCDelegate?
#IBOutlet var buttonVC1: UIButton!
#IBAction func goToVC1 () {
delegate.sideVCGoToVC1()
}
MainVC
class MainVC: UIViewController, SideVCDelegate {
var menu: sideVC!
override func viewDidLoad() {
super.viewDidLoad()
menu = self.storyboard?.instantiateViewControllerWithIdentifier("menu") as sideViewController
menu.delegate = self
menu.view.frame = CGRect(x: 0, y: 0, width: 160, height: 480)
view.addSubview(menu.view)
}
// MARK: - SideViewControllerDelegate
func sideViewControllerGoToVC1() {
menu.view.removeFromSuperview()
var VC1 = self.storyboard?.instantiateViewControllerWithIdentifier("ViewController") as ViewController
presentViewController(VC1, animated:true, completion: nil)
}
}
Note
Apart from the question you've asked, the below lines seems somewhat vague.
var VC1 = self.storyboard?.instantiateViewControllerWithIdentifier("ViewController") as ViewController
menu.view.frame = CGRect(x: 0, y: 0, width: 160, height: 480)
You're obtaining a view controller from your storyboard which has a frame when you designed it inside Interface Builder but you're changing it afterwards. It's not a good practice to play with the frames of views once they're created.
Maybe you've intended to do something else but most likely, it's a problematic piece of code.

Swift 5
In the UIKit view hierarchy, view controllers can either be "attached" or "detached", which I put in quotes because they're never explained in documentation. From what I've observed, attached view controllers are simply view controllers that are directly chained to the key window.
Therefore, the nearest attached view controller would obviously be the root view controller itself, since it's directly owned by the key window. This is why presenting from the root view controller remedies warnings about presenting on detached view controllers.
To present a subsequent view controller (a second one), you must find the next nearest and available attached view controller (I say available because the root view controller is currently occupied presenting the current view controller; it cannot present any more view controllers). If the root is presenting a plain view controller (meaning, not a container view controller like a navigation controller), then the next nearest attached view controller is that view controller. You can present from self without any warnings, since it's directly chained to the root, which is directly chained to the key window. However, if the root presented a container view controller, like a navigation controller, then you could not present from any of its children, because they are not directly chained to the root—the parent/container is. Therefore, you would have to present from the parent/container.
To make this easier, you can subclass UIViewController and add a convenience method for finding the nearest available attached view controller.
class XViewController: UIViewController {
var rootViewController: UIViewController? {
return UIApplication.shared.keyWindow?.rootViewController
}
/* Returns the nearest available attached view controller
(for objects that seek to present view controllers). */
var nearestAvailablePresenter: UIViewController? {
guard let root = rootViewController else {
return nil
}
if root.presentedViewController == nil {
return root // the root is not presenting anything, use the root
} else if let parent = parent {
return parent // the root is currently presenting, find nearest parent
} else {
return self // no parent found, present from self
}
}
}
Usage
class SomeViewController: XViewController {
let modal = AnotherViewController()
nearestAvailablePresenter?.present(modal, animated: true, completion: nil)
}

Here this might help you. I got my error fixed with this
let time = dispatch_time(DISPATCH_TIME_NOW, Int64(0.001 * Double(NSEC_PER_SEC)))
dispatch_after(time, dispatch_get_main_queue(), { () -> Void in
self.performSegueWithIdentifier("SegueName", sender: self)
})
Good luck..

Related

UIPresentationController with interactive presenting view

I looked at this question, but it does not help: Interacting with presenting view and UIPresentationController
I am trying to implement a sheet presentation controller, similar to the UISheetPresentationController for iOS 15, except I need it to run on iOS 14 as well. And I am also wanting to make it so that it has a small detent, similar to how it is done in the Maps app.
So I have a custom UIPresentationController class and I don't have much in it yet, but is what I have so far:
- (CGRect)frameOfPresentedViewInContainerView {
[super frameOfPresentedViewInContainerView];
CGRect presentedViewFrame = CGRectZero;
CGRect containerBounds = self.containerView.bounds;
presentedViewFrame.size = CGSizeMake(containerBounds.size.width, floor(containerBounds.size.height * 0.5));
presentedViewFrame.origin = CGPointMake(0, containerBounds.size.height - presentedViewFrame.size.height);
return presentedViewFrame;
}
- (BOOL)shouldPresentInFullscreen {
return NO;
}
- (BOOL)shouldRemovePresentersView {
return NO;
}
And this does work. It does display the view controller at half of the height of the presenting view controller. The problem is that the presenting view is no longer interactive because there is a view that gets added by the presentation controller class apparently.
So my question is how do I get the presenting view to be interactive, where I can scroll it and interact with buttons and the other controls? I want to be able to use a presentation controller to present the view controller.
The following allows you to present a shorter modal view controller while still allowing interaction with the presenting view controller. This doesn't attempt to implement what you get with the newer UISheetPresentationController. This only solves the issue of being able to interact with both view controllers while the shorter second controller is in view.
This approach makes use of a custom UIPresentationController. This avoids the need to deal with custom container views and animating the display of the presented view.
Start with the following custom UIPresentationController class:
import UIKit
class ShortPresentationController: UIPresentationController {
override var shouldPresentInFullscreen: Bool {
// We don't want full screen
return false
}
override var frameOfPresentedViewInContainerView: CGRect {
let size = containerView?.frame.size ?? presentingViewController.view.frame.size
// Since the containerView's frame has been resized already, we just need to return a frame of the same
// size with a 0,0 origin.
return CGRect(origin: .zero, size: size)
}
override func presentationTransitionWillBegin() {
super.presentationTransitionWillBegin()
guard let containerView = containerView else { return }
// By default the containerView's frame covers the screen which prevents interacting with the presenting view controller.
// Update the containerView's frame to match the area needed by the presented view controller. This allows
// interection with the presenting view controller even while the presented view controller is in view.
//
// This code assumes we want the presented view controller to use the full width of the presenting view controller
// while honoring the preferredContentSize height. It also assumes we want the bottom of the presented view
// controller to appear at the bottom of the presenting view controller. Adjust as needed.
let containerSize = containerView.bounds.size
let preferredSize = presentedViewController.preferredContentSize
containerView.frame = CGRect(x: 0, y: containerSize.height - preferredSize.height,
width: containerSize.width, height: preferredSize.height)
}
}
In the presenting view controller you need to create and present the short view controller. This is fairly typical code for presenting a modal view controller with the important differences of setting the style to custom and assigning the transitioningDelegate.
FirstViewController.swift:
let vc = SecondViewController()
let nc = UINavigationController(rootViewController: vc)
nc.modalPresentationStyle = .custom
nc.transitioningDelegate = self
present(nc, animated: true)
You need to implement one method of the transition delegate in FirstViewController to return the custom presentation controller:
extension FirstViewController: UIViewControllerTransitioningDelegate {
func presentationController(forPresented presented: UIViewController, presenting: UIViewController?, source: UIViewController) -> UIPresentationController? {
return ShortPresentationController(presentedViewController: presented, presenting: presenting)
}
}
And lastly, make sure you set the preferredContentSize property of the second view controller. One typical place is in the viewDidLoad of SecondViewController:
preferredContentSize = CGSize(width: 320, height: 300)
That does not include the navigation controller bars (if any). If you want the presenting view controller to set the final size, including the bars, you could set the preferredContentSize on nc just before presenting it. It depends on who you want to dictate the preferred size.

Coordinator pattern with UINavigationControllers and a UITabBarController

I'm trying to learn how to integrate coordinator pattern into iOS development.
I have an app which like this. In the storyboard, it looks like this. The navigation controllers and tabbars are not added in the storyboard because according to coordinator pattern, they will be added programatically.
The first view controller is PhoneViewController which takes user's phone number. This view controller is embedded in a navigation controller. After entering the phone number, it moves to the VerifyPhoneViewController. After verification, it moves to MainViewController a tabbarcontroller which contains three tabs. Each of these view controller will have a separate navigation controller of their own.
I have a protocol which contains all the necessary properties and functions each coordinator needs to implement.
protocol Coordinator {
var childCoordinators: [Coordinator] { get set }
var navigationController: UINavigationController { get set }
func start()
}
I created a separate coordinator called AuthCoordinator for the authentication flow part of the app.
class AuthCoordinator: Coordinator {
var childCoordinators = [Coordinator]()
var navigationController: UINavigationController
init(navigationController: UINavigationController) {
self.navigationController = navigationController
}
// The initial view
func start() {
let phoneViewController = PhoneViewController.instantiate()
phoneViewController.coordinator = self
navigationController.pushViewController(phoneViewController, animated: false)
}
func submit(phoneNo: String) {
let verifyPhoneViewController = VerifyPhoneViewController.instantiate()
verifyPhoneViewController.coordinator = self
verifyPhoneViewController.phoneNo = phoneNo
navigationController.pushViewController(verifyPhoneViewController, animated: true)
}
// Move to the tabbarcontroller
func main() {
let mainViewController = MainViewController.instantiate()
navigationController.pushViewController(mainViewController, animated: true)
}
}
The navigation works fine. However there's a small issue.
Notice after moving to the tabbarcontroller, the titles don't show in the navigationbar when I switch between view controllers (I do set them in viewDidLoad method of each view controller). Plus the back button to VerifyPhoneViewController is still there too.
The issue is obvious. The navigationcontroller I initialized for the AuthCoordinator is still there at the top. I'm literally pushing the MainViewController on to that stack.
func main() {
let mainViewController = MainViewController.instantiate()
navigationController.pushViewController(mainViewController, animated: true)
}
What I can't figure out is a way to not do it like this. I can hide the navigationbar in the start method but then it's not ideal because well, it hides the navigationbar and I don't want that.
func start() {
let phoneViewController = PhoneViewController.instantiate()
phoneViewController.coordinator = self
navigationController.navigationBar.isHidden = true
navigationController.pushViewController(phoneViewController, animated: false)
}
Is there a different way to keep the navigationcontroller for the duration of the auth flow and then discard it when/soon after showing the MainViewController?
The demo project is uploaded here.

Animate root view controller transition

I want to animate the transition between one root view controller and another. I could hypothetically perform a segue to the other view controller instead of switch roots, but if possible I would like to maintain the root view controller transition. Here's what I have to do this without animation.
let initialViewController = UIStoryboard.initialViewController(for: .main)
self.view.window?.rootViewController = initialViewController
self.view.window?.makeKeyAndVisible()
How would I do this with, say, an animation where the first controller slides up and away and reveals the second one?
An approach could be:
1. Set 2nd ViewController as root view controller.
2. Add 1st ViewController's view to 2nd Controller.
3. Remove 1st Controller's view with animation.
Code:
class View2Controller: UIViewController {
var viewToAnimate:UIView?
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
if let view1Controller = self.storyboard?.instantiateViewController(withIdentifier: "View1Controller") {
self.addChildViewController(view1Controller)
self.view.addSubview(view1Controller.view)
self.viewToAnimate = view1Controller.view
}
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 1.5) {
let frame = self.view.frame
UIView.animate(withDuration: 1.0, animations: {
self.viewToAnimate?.frame = CGRect(x: 0, y: -frame.height, width: frame.width, height: frame.height)
}, completion: { (finished) in
if finished {
self.viewToAnimate?.removeFromSuperview()
}
})
}
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
self.viewToAnimate?.frame = self.view.bounds
}
}
Effect:
Source code on Github:
SlideUp Demo
You really shouldn't ever change your root view controller.
The way I deal with this, therefore, is that my root view controller is not my root view controller. Instead, I have a "secret" root view controller which is the real root view controller. It effectively does nothing; its view contains no interface. Its only job is to act as the parent to every other "root" view controller; it is a custom parent view controller (container view controller), and it always has one child view controller.
A moment's thought will show that now the problem is solved, because the business of replacing a child view controller and its view with another child view controller and its view, while transitioning with animation between the views, is straightforward and well-documented.

Pass data to View Controller embedded inside a Container View Controller

My view controller hierarchy is the following:
The entry point is a UINavigationController, whose root view controller is a usual UITableViewController. The Table View presents a list of letters.
When the user taps on a cell, a push segue is triggered, and the view transitions to ContainerViewController. It contains an embedded ContentViewController, whose role is to present the selected letter on screen.
The Content View Controller stores the letter to be shown as a property letter: String, which should be set before its view is pushed on screen.
class ContentViewController: UIViewController {
var letter = "-"
#IBOutlet private weak var label: UILabel!
override func viewWillAppear(animated: Bool) {
super.viewWillAppear(animated)
label.text = letter
}
}
On the contrary, the Container View Controller should not know anything about the letter (content-unaware), since I'm trying to build it as reusable as possible.
class ContainerViewController: UIViewController {
var contentViewController: ContentViewController? {
return childViewControllers.first as? ContentViewController
}
}
I tried to write prepareForSegue() in my Table View Controller accordingly :
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
if let containerViewController = segue.destinationViewController as? ContainerViewController {
let indexPath = tableView.indexPathForCell(sender as! UITableViewCell)!
let letter = letterForIndexPath(indexPath)
containerViewController.navigationItem.title = "Introducing \(letter)"
// Not executed:
containerViewController.contentViewController?.letter = letter
}
}
but contentViewController is not yet created by the time this method is called, and the letter property is never set.
It is worth mentioning that this does work when the segue's destination view controller is set directly on the Content View Controller -- after updating prepareForSegue() accordingly.
Do you have any idea how to achieve this?
Actually I feel like the correct solution is to rely on programmatic instantiation of the content view, and this is what I chose after careful and thorough thoughts.
Here are the steps that I followed:
The Table View Controller has a push segue set to ContainerViewController in the storyboard. It still gets performed when the user taps on a cell.
I removed the embed segue from the Container View to the ContentViewController in the storyboard, and I added an IB Outlet to that Container View in my class.
I set a storyboard ID to the Content View Controller, say… ContentViewController, so that we can instantiate it programmatically in due time.
I implemented a custom Container View Controller, as described in Apple's View Controller Programming Guide. Now my ContainerViewController.swift looks like (most of the code install and removes the layout constraints):
class ContainerViewController: UIViewController {
var contentViewController: UIViewController? {
willSet {
setContentViewController(newValue)
}
}
#IBOutlet private weak var containerView: UIView!
private var constraints = [NSLayoutConstraint]()
override func viewDidLoad() {
super.viewDidLoad()
setContentViewController(contentViewController)
}
private func setContentViewController(newContentViewController: UIViewController?) {
guard isViewLoaded() else { return }
if let previousContentViewController = contentViewController {
previousContentViewController.willMoveToParentViewController(nil)
containerView.removeConstraints(constraints)
previousContentViewController.view.removeFromSuperview()
previousContentViewController.removeFromParentViewController()
}
if let newContentViewController = newContentViewController {
let newView = newContentViewController.view
addChildViewController(newContentViewController)
containerView.addSubview(newView)
newView.frame = containerView.bounds
constraints.append(newView.leadingAnchor.constraintEqualToAnchor(containerView.leadingAnchor))
constraints.append(newView.topAnchor.constraintEqualToAnchor(containerView.topAnchor))
constraints.append(newView.trailingAnchor.constraintEqualToAnchor(containerView.trailingAnchor))
constraints.append(newView.bottomAnchor.constraintEqualToAnchor(containerView.bottomAnchor))
constraints.forEach { $0.active = true }
newContentViewController.didMoveToParentViewController(self)
}
} }
In my LetterTableViewController class, I instantiate and setup my Content View Controller, which is added to the Container's child view controllers. Here is the code:
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
if let containerViewController = segue.destinationViewController as? ContainerViewController {
let indexPath = tableView.indexPathForCell(sender as! UITableViewCell)!
let letter = letterForIndexPath(indexPath)
containerViewController.navigationItem.title = "Introducing \(letter)"
if let viewController = storyboard?.instantiateViewControllerWithIdentifier("ContentViewController"),
let contentViewController = viewController as? ContentViewController {
contentViewController.letter = letter
containerViewController.contentViewController = contentViewController
}
}
}
This works perfectly, with an entirely content-agnostic container view controller. By the way, it used to be the way one instantiated a UITabBarController or a UINavigationController along with its children, in the appDidFinishLaunching:withOptions: delegate method.
The only downside of this I can see: the UI flow ne longer appears explicitly on the storyboard.
The only way I can think of is to add delegation so that your tableViewController implements a protocol with one method to return the letter; then you have containerViewController setting its childViewController (the contentViewController) delegate to its parent. And the contentViewController can finally ask its delegate for the letter.
At your current solution the presenting object itself is responsible for working both with the "container" and the "content", it doesn't have to be changed, but such solution not only has the issues like the one you described, but also makes the purpose of the "container" not very clear.
Look at the UIAlertController: you are not configuring its child view controller directly, you are not even supposed to know it exists when using the alert controller. Instead of configuring the "content", you are configuring the "container" which is aware of the content interfaces, lifecycle and behavior and doesn't expose it. Following this approach you achieve a properly divided responsibility of the container and content, minimal exposure of the "content" allows you to update the "container" without a need to update the way it is used.
In short, instead of trying to configure everything from a single place, make it so you configure only the "container" and let it configure the "content" when and where it is needed. E.g. in the scenario you described the "container" would set data for the "content" whenever it initializes the child controllers. I'm using "container" and "content" instead of ContainerViewController and ContentViewController because the solution is not strictly based on the controllers because you might as well replace it wth NSObject + UIView or UIWindow.

Performing a segue on a view controller that is nested inside of another view controller in swift

I have a view controller which is nested inside of another view controller using a container view. Is it possible for me to segue from the view which is currently in the container view and replace it with another view controller in the same container view. I.e. the content that is around the container view is not removed by another view controller taking up the entire view.
Yes it is. You can read about that in the Apple Docs.
Considering your containerView currently only has one viewcontroller, here is a very basic example:
func loadVCWithId(idToLoad: String){
childViewControllers[0].willMoveToParentViewController(nil)
childViewControllers[0].view.removeFromSuperview()
childViewControllers[0].removeFromParentViewController()
let secondViewController = self.storyboard?.instantiateViewControllerWithIdentifier(idToLoad)
UIView.transitionWithView(yourContainer, duration: 0.5, options: UIViewAnimationOptions.TransitionFlipFromRight, animations: {self.yourContainer.addSubview((secondViewController?.view)!)}, completion: nil)
secondViewController!.view.frame = firstContainer.bounds
// do initialization of secondViewController here
secondViewController?.didMoveToParentViewController(self)
}
loadVCWithId(idToLoad:String)is a method within your host viewcontroller.
In this code fragment I delete the current content of the container (probably not the best way to just access index 0, but for the sake of this example, this should be enough), instantiate a new ViewController by ID (this one is present in my storyboard but not accessbile yet), animate the transition and actually add the new VC to the container.
Hope this helps.
this my solution maybe helpful for
first i create a protocol on childViewController
protocol ChildViewControllerDelaget
{
func performForSegue(SegueIdentifier:String)
}
class ChildViewController: UIViewController {
var delaget:ChildViewControllerDelaget?
override func viewDidLoad() {
super.viewDidLoad()
}
init()
{
}
#IBAction func myAction(sender: AnyObject) {
if delaget != nil {
deleget.performForSegue("mySegueIdentifier")
}
}
and on MainViewController
class ViewController: UIViewController,ChildViewControllerDelaget {
override func viewDidLoad()
{
super.viewDidLoad()
let child = ChildViewController()
child.delaget = self
}
func performForSegue(segueIdentifier:String)
{
self.performSegueWithIdentifier(segueIdentifier, sender: nil)
}
}

Resources