UIPresentationController with interactive presenting view - ios

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.

Related

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.

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)
}
}

iOS - Presenting a view controller from a presented view controller changes its height

I've a custom transition between two controller that works close to the iOS mail app, which one stays on top of the other with some implemented scrolling behavior.
If I present a new view controller from the Presented view controller which isn't full screen sized, and then I dismiss this new presented view controller, the previous Presented view controller changes its height and then resizes itself.
I know this might be a little confusing but check the gif example below.
As you can see, If I present this custom image picker and then dismiss it, the view controller which presented it warps to full screen and then resizes to the initial value.
How can I prevent this from happening? I want the ViewController which presents the image picker keeps its height.
After the dismiss you can see the resize happening.
Setting the presenting view controllers size
Since it's a UIViewControllerAnimatedTransitioning I create a custom presentation and the size it's set has it's own identity
class CustomPresentationController: UIPresentationController {
override init(presentedViewController: UIViewController, presenting presentingViewController: UIViewController!) {
super.init(presentedViewController: presentedViewController, presenting: presentingViewController)
}
override var frameOfPresentedViewInContainerView: CGRect {
let containerBounds = self.containerView?.bounds
let origin = CGPoint(x: 0.0, y: ((containerBounds?.size.height)! * 0.05))
let size = CGSize(width: (containerBounds?.size.width)! , height: ((containerBounds?.size.height)! * 0.95))
// Applies the attributes
let presentedViewFrame: CGRect = CGRect(origin: origin, size: size)
return presentedViewFrame
}
override func containerViewWillLayoutSubviews() {
presentedView?.frame = frameOfPresentedViewInContainerView
}
}
Any hint?
thanks
I think that is where the issue is. You are forcing the frame size which is not working out. You should use something like preferredContentSize.
You can simply add this to the viewDidLoad of your CustomPresentationController.
Alternatively you may also try modalPresentationStyle as "Over Current Context"
You can refer very good examples of how you can keep some part of VC as transparent here

How to approach to this design - iOS

My App is Navigation based Application for iPad.
First Screen is Home Screen. On Clicking button on Home Screen, it pushes to Map Screen. On the Map Screen, i have left Panel (view controller) over the map view controller which occupies 1/4 of the screen.
Left Panel is a View Controller which has Table View. On clicking cell, it should push new viewcontroller to left panel leaving the map view controller behind.
Push
Home Screen -------- Map Screen
|(Added over map screen) Push
|----- Left Panel (Table View) -------- Detail View
I can't use Split View Controller because there is a navigation in left panel as well as in Home Screen. Some times i need to animate/hide left panel. I can customise left panel.
How to implement this structure. Is it good to use Nested Navigation Controller or is there any library available. My App supports both Portrait and Landscape. I am using Swift.
I'm sorry, I don't know Swift as well. However, I think you have to declare a base layout:
you will have a MainViewController that will include a LeftPanelViewController and a FrontViewController. In the MainViewController nib, you will create the main layout using AutoLayout: add a UIView at the left of the screen and another UIView for the frontpage.
Then, link outlets and you will have the layout done! Then you have only to add/remove subviews to leftPanelView and to FrontView.
Now, I think that the right logic is that MainViewController is the NavigationController, so you have to implement the protocol of LeftPanelViewController and FrontViewController, so Main will know how and when add/remove subviews.
The important things is that no one object have to know the existence of MainViewController to preserve the logic. So you have to notify MainViewController for something, to use delegation pattern or something else as NSNotification (be aware, it could be much weight...)
I hope it will bel helpful. Bye
I would solve this by adding a ContainerView as your Left Panel. As you want to do pushes here, you can make the container view controller a navigation controller with the table view as its root view controller.
You'll still be able to animate/hide the container view just like any other view.
This extension for the UIViewController creates the side panel to the left.
private var temp_view:UIView?
private var temp_width:CGFloat?
private weak var temp_sidebar:UIViewController?
extension UIViewController {
func configureSideBar(StoryBoard:String,SideBarIdentifier:String,View:UIView){
var storyboard = UIStoryboard(name: StoryBoard, bundle: nil)
weak var sidebar = storyboard.instantiateViewControllerWithIdentifier(SideBarIdentifier) as? UIViewController
let width:CGFloat = 250//sidebar!.view.frame.size.width
let height = UIScreen.mainScreen().bounds.height
let frame = CGRectMake(0, 0, width, height)
temp_view = View
temp_width = width
temp_sidebar = sidebar
sidebar!.view.frame = frame
addChildViewController(sidebar!)
view.addSubview(sidebar!.view)
view.sendSubviewToBack(sidebar!.view)
toogleSideBarWithAnimation(0.2,Open:false)
}
func getSidebar() -> UIViewController?{
if let sdbar = temp_sidebar {
return sdbar
}
else {
println("Warning:You have tou configure sidebar first")
return nil
}
}
func toogleSideBarWithAnimation(Duration:NSTimeInterval,Open:Bool) {
if let view = temp_view {
UIView.animateWithDuration(Duration, delay: 0, options: UIViewAnimationOptions.TransitionFlipFromLeft, animations: {
var Frame = view.frame
if !Open{
Frame.origin.x = 0
}
else {
Frame.origin.x = temp_width!
}
view.frame = Frame
}, completion: { finished in
})
}
else {
println("Warning:You have tou configure sidebar first")
}
}
}
Let me explain how to use it.
First create a view controller scene and set the storyboard id. In this examples i used "sidebar"
Then add this instruction to the map view controller to the viewDidLoad like this
override func viewDidLoad() {
super.viewDidLoad()
self.configureSideBar("Main", SideBarIdentifier: "sidebar", View: Subview)
}
I suggest adding a UIView with all your view in it in the map view controller
To open and close it use this instruction
self.toogleSideBarWithAnimation(0.2,Open:OpenSidebar)

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