Animate root view controller transition - ios

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.

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.

Adding a child view controller to a subview but make it modal to entire view without a navigation controller

Using Xcode 10+, Swift 4, iOS 11.4+
First let me say that I'm not using a Navigation Controller -
I'm adding a ViewController to another as a child using this basic code:
topController.addChildViewController(childVC)
topController.view.addSubview(childVC.view)
childVC.didMove(toParentViewController: topController)
The child is smaller than the parent and has a few buttons, one of which will animate it out of view.
I'm not using present/dismiss as it always covers the entire screen.
I'd like it to be modal - once it's animated into place, nothing else on screen (behind it) should be usable until it is animated out of view.
How can I make the childVC be modal?
You could try adding the controller to a UIWindow which has windowLevel = UIWindowLevelAlert + 1 instead. Then after the dismiss animation finishes you could remove the window. Here is a sample code snippet that seems to work:
func presentChildVC() {
modalWindow = UIWindow(frame: UIScreen.main.bounds)
let rootController = UIViewController()
rootController.view.backgroundColor = .clear
rootController.addChild(childController)
rootController.view.addSubview(childController.view)
childController.didMove(toParent: rootController)
modalWindow?.rootViewController = rootController
modalWindow?.windowLevel = .alert + 1
modalWindow?.makeKeyAndVisible()
modalWindow?.backgroundColor = .clear
UIView.animate(withDuration: 2, animations: {
self.childController.view.alpha = 1
})
}
func dismissChildVC() {
UIView.animate(withDuration: 2, animations: {
self.childController.view.alpha = 0
}, completion: { _ in
self.modalWindow?.isHidden = true
self.modalWindow = nil
})
}
1) The child is smaller than the parent:-
You just need to update your child view frame same like parent view.
topController.addChildViewController(childVC)
topController.view.addSubview(childVC.view)
**childVC.view.frame.size.height = self.view.frame.size.height**
childVC.didMove(toParentViewController: topController)
2) has a few buttons, one of which will animate it out of view :-
Set Click Event on buttons like this to remove child view from parent
self.willMove(toParentViewController: nil)
self.view.removeFromSuperview()
self.removeFromParentViewController()

viewController.view not getting added to custom view inside Parent View Controller

I would like to add a childViewController into a custom UIView part of a parentViewController. However, if I am doing self.customView.addSubview(childViewController.view) I cannot see the childViewController.view as it doesn't get added. In contrast, if I do self.view.addSubview(childViewController.view) it all works well. Can someone explain why this is happening? I really need to add childViewController.view as subview of the customView and not as part of the self.view.
if let childViewController = self.storyboard?.instantiateViewController(withIdentifier: "ChildVC") as UIViewController? {
self.addChildViewController(childViewController)
childViewController.view.frame = customView.bounds
self.customView.addSubview(childViewController.view)
childViewController.view.autoresizingMask = [.flexibleWidth, .flexibleHeight]
childViewController.didMove(toParentViewController: self)
childViewController.view.frame = CGRect(x: customView.frame.origin.x,
y: customView.frame.origin.y,
width: customView.frame.width,
height: customView.frame.height)
}
How about using a Container view from Object Library (place from where we drag table view, textView and all UI components onto our storyboard). which is
Container view define a region of view controller that include
a child view controller
When you take Container view from object library in your desired
ViewController on storyBoard.
it automatically gives you a another view controller attached to your view controller with a segue.
you just need to override this segue code and that dragged container view will work as a child view controller for you it did load will call automatically.
just Override this is your parent view Controller
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if let identifier = segue.identifier {
switch identifier {
case "SegueIndetifierName":
(segue.destination as? YourChildViewController)?.parentViewControllerObject = self
default:
break
}
}
}
NOTE: declare parentView controller object in your child view controller like this
weak var parentViewControllerObject: ParentViewController!
to uniquely identify the relation ship between child and parent. and rest your work will be done automatically.
You can check the Sample Working project to add Subviews
Link https://github.com/RockinGarg/Container_Views.git
Required Code:
Class Object to be added as Subview
private lazy var FirstObject: firstVC =
{
// Instantiate View Controller
let viewController = self.storyboard?.instantiateViewController(withIdentifier: "firstVC") as! firstVC
// Add View Controller as Child View Controller
self.addChildViewController(viewController)
return viewController
}()
Add in ParentView as Subview
private func add(asChildViewController viewController: UIViewController)
{
// Configure Child View
viewController.view.frame = CGRect(x: 0, y: 0, width: self.firstContainer.frame.size.width, height: self.firstContainer.frame.size.height)
// Add Child View Controller
addChildViewController(viewController)
viewController.view.translatesAutoresizingMaskIntoConstraints = true
// Add Child View as Subview
firstContainer.addSubview(viewController.view)
// Notify Child View Controller
viewController.didMove(toParentViewController: self)
}
Where : -
firstContainer is the parent view In which subview is to be added
viewController class object whose view is to be added as Subview
Note- This can be used in containerViews as well as Normal UIView too for adding a Controller as Subview

How do I keep UI elements above a UIViewController and its presented ViewController?

I want to display some UI elements, like a search bar, on top of my app's first VC, and also on top of a second VC that it presents.
My solution for this was to create a ContainerViewController, which calls addChildViewController(firstViewController), and view.addSubview(firstViewController.view). And then view.addSubview(searchBarView), and similar for each of the UI elements.
At some point later, FirstViewController may call present(secondViewController), and ideally that slides up onto screen with my search bar and other elements still appearing on top of both view controllers.
Instead, secondViewController is presented on top of ContainerViewController, thus hiding the search bar.
I also want, when a user taps on the search bar, for ContainerViewController to present SearchVC, on top of everything. For that, it's straightforward - containerVC.present(searchVC).
How can I get this hierarchy to work properly?
If I understand correctly, your question is how to present a view controller on top (and within the bounds) of a child view controller which may have a different frame than the bounds of the parent view. That is possible by setting modalPresentationStyle property of the view controller you want to present to .overCurrentContext and setting definesPresentationContext of your child view controller to true.
Here's a quick example showing how it would work in practice:
override func viewDidLoad() {
super.viewDidLoad()
let childViewController = UIViewController()
childViewController.view.backgroundColor = .yellow
childViewController.view.translatesAutoresizingMaskIntoConstraints = true
childViewController.view.frame = view.bounds.insetBy(dx: 60, dy: 60)
view.addSubview(childViewController.view)
addChildViewController(childViewController)
childViewController.didMove(toParentViewController: self)
// Wait a bit...
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 2) {
let viewControllerToPresent = UIViewController()
viewControllerToPresent.modalPresentationStyle = .overCurrentContext // sets to show itself over current context
viewControllerToPresent.view.backgroundColor = .red
childViewController.definesPresentationContext = true // defines itself as current context
childViewController.present(viewControllerToPresent, animated: true, completion: nil)
}
}

Presenting view controllers on detached view controllers

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..

Resources