iOS 10 Swift 3 UIViewController present doesn't work - ios

I'm currently developing an application using iOS 10 and Swift 3
I think that I may have destroy the navigation between my controllers.
Indeed, when I try to present a new view controller, I have this warning on Xcode debugger.
Warning: Attempt to present FirstViewController on Project.StoryBoardManager whose view is not in the window hierarchy!
I have made some research but I'm not able to fix my bug.
I have this on my AppDelegate.swift
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
let storyboard = UIStoryboard(name:"Authentication", bundle: nil)
let vc = storyboard.instantiateInitialViewController() as UIViewController!
self.window?.rootViewController = vc
self.window?.makeKeyAndVisible()
return true
}
And this on my class to present news views
class StoryBoardManager: UIViewController{
fileprivate var appD = UIApplication.shared.delegate as! AppDelegate
func changeView(storyboardName: String){
let storyboard = UIStoryboard(name: storyboardName, bundle: nil)
if let vc = storyboard.instantiateInitialViewController() {
vc.modalTransitionStyle = UIModalTransitionStyle.flipHorizontal
vc.modalPresentationStyle = UIModalPresentationStyle.fullScreen
//appD.window?.rootViewController = vc
present(vc, animated: true, completion: nil)
} else {
print("Unable to instantiate VC from \(storyboardName) storyboard")
}
}
override func viewDidLoad(){
super.viewDidLoad()
}
If I comment the update of rootViewController the new controller is not presented.
EDIT for #Zac Kwan
import Foundation
import UIKit
class CustomNavBar: UIView {
fileprivate let _storyBoardManager : StoryBoardManager = StoryBoardManager()
fileprivate var _currentUIViewController : UIViewController = UIViewController()
init() {
super.init(frame: CGRect(x: 0, y: 0, width:0, height:0))
}
func changeViewStoryboard(sender: UIButton!){
if (sender.tag == 0){
self._storyBoardManager.changeView(storyboardName: "View1")
} else if (sender.tag == 1) {
self._storyBoardManager.changeView(storyboardName: "View2")
} else if (sender.tag == 2) {
self._storyBoardManager.changeView(storyboardName: "View3")
} else {
self._storyBoardManager.changeView(storyboardName: "View4")
}
}
override init(frame: CGRect) {
super.init(frame: frame)
}
func createButton(title: String, posX: Double, witdh: Double, tag: Int, font: UIFont) -> UIButton {
let buttonCreated = UIButton(frame: CGRect(x: posX, y: 0, width: witdh, height: 60))
buttonCreated.setTitle(title, for: UIControlState())
buttonCreated.setTitleColor(CommonVariable.darkGrey, for: UIControlState())
buttonCreated.titleLabel!.font = font
buttonCreated.tag = tag
buttonCreated.addTarget(self, action:#selector(self.changeViewStoryboard(sender:)), for: UIControlEvents.touchUpInside)
buttonCreated.backgroundColor = UIColor.white
return buttonCreated
}
required init?(coder aDecoder: NSCoder) {
Super. Inuit (coder: decoder)
addSubview(self.createButton(title: « ChangeView », posX: 256.0, witdh: Double(self._sizeButton) - 1, tag: 1, font: UIFont(name: « Arial », size: 15)!))
addSubview(self.createButton(title: « ChangeView 2 », posX: 512.0, witdh: Double(self._sizeButton) - 1, tag: 2, font: UIFont(name: « Arial », size: 15)!))
}
}

Please try changing the code like below :
class StoryBoardManager: UIViewController{
fileprivate var appD = UIApplication.shared.delegate as! AppDelegate
func changeView(storyboardName: String){
let storyboard = UIStoryboard(name: storyboardName, bundle: nil)
if let vc = storyboard.instantiateInitialViewController() {
vc.modalTransitionStyle = UIModalTransitionStyle.flipHorizontal
vc.modalPresentationStyle = UIModalPresentationStyle.fullScreen
//appD.window?.rootViewController = vc
appD.present(vc, animated: true, completion: nil)
} else {
print("Unable to instantiate VC from \(storyboardName) storyboard")
}
}
override func viewDidLoad(){
super.viewDidLoad()
}

Related

Reload UITabBarController on demand

I have the following TabBarController with 2 items. It is showing correctly.
I'm calling the setupItems() function from another controller when something changes its value.
The function is called correctly, the problem is that the navFirstController.tabBarItem.image is not being updated.
class TabBarController: UITabBarController {
override func viewDidLoad() {
super.viewDidLoad()
setupItems()
}
func setupItems() {
let scale: CGFloat = 0.35
let navFirstController = UINavigationController(rootViewController: FirstController())
let navSecondController = UINavigationController(rootViewController: SecondController())
navSecondController.tabBarItem.image = UIImage.scale(image: UIImage(named: "image2")!, by: scale)
navSecondController.tabBarItem.imageInsets = UIEdgeInsets(top: 8, left: 0, bottom: -8, right: 0)
if something == true {
navFirstController.tabBarItem.image = UIImage.scale(image: UIImage(named: "image1")!, by: scale)
} else {
navFirstController.tabBarItem.image = UIImage.scale(image: UIImage(named: "image3")!, by: scale)
}
navFirstController.tabBarItem.imageInsets = UIEdgeInsets(top: 8, left: 0, bottom: -8, right: 0)
viewControllers = [navSecondController, navFirstController]
}
}
I'ved tried with:
1) viewControllers?.remove(at: 1) at the beginning of setupItems()
2) navFirstController.removeFromParent() at the beginning of setupItems()
3) self.viewWillLayoutSubviews() at the end of setupItems()
4) self.view.setNeedsLayout(), self.view.setNeedsDisplay() at the end of setupItems()
I don't feel we need to create viewControllers object again just to change tab bar image.
Just we need to get viewController object from viewControllers array and change image.
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
}
}
class SecondViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
}
#IBAction func btnClicked(_ sender: Any) {
//change image of tab bar item on button clicked
if let tabVC = self.tabBarController as? TabBarController {
tabVC.changeImage()
}
}
}
class TabBarController: UITabBarController {
override func viewDidLoad() {
super.viewDidLoad()
setupItems()
}
func setupItems() {
let storyboard = UIStoryboard(name: "Main", bundle: Bundle.main)
let firstVC = storyboard.instantiateViewController(withIdentifier: "First")
let navFirstController = UINavigationController(rootViewController: firstVC)
navFirstController.tabBarItem.image = UIImage(named: "Image1")
let secondVC = storyboard.instantiateViewController(withIdentifier: "Second")
let navSecondController = UINavigationController(rootViewController: secondVC)
navSecondController.tabBarItem.image = UIImage(named: "Image2")
viewControllers = [navSecondController, navFirstController]
}
func changeImage() {
if let second = viewControllers?[1] as? UINavigationController {
second.tabBarItem.selectedImage = UIImage(named: "Image3")
second.tabBarItem.image = UIImage(named: "Image3")
}
}
}
Note if you want to change selected tab bar item image then change "selectedImage" value otherwise change "image" value.
You probably need to set the image's rendering mode to UIImageRenderingModeAlwaysOriginal.
Try changing this:
navFirstController.tabBarItem.image = UIImage.scale(image: UIImage(named: "image1")!, by: scale)
With this:
navFirstController.tabBarItem.image = UIImage.scale(image: UIImage(named: "image1")!, by: scale).withRenderingMode(.alwaysOriginal)
EDIT - Sample Code
Consider this setup:
The initial view controller is a custom class TabBarViewController
The red background view controller is a UIViewController with storyboard ID "First"
The orange background view controller is a custom class SecondViewController with an IBAction and storyboard ID "Second"
The Assets.xcassets file has three images (40x40 png):
TabBarViewController
import UIKit
class TabBarViewController: UITabBarController {
var something: Bool = false
override func viewDidLoad() {
super.viewDidLoad()
setupItems()
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
}
func setupItems() {
let storyboard = UIStoryboard(name: "Main", bundle: Bundle.main)
let firstVC = storyboard.instantiateViewController(withIdentifier: "First")
let navFirstController = UINavigationController(rootViewController: firstVC)
navFirstController.tabBarItem.image = UIImage(named: "image1")!.withRenderingMode(.alwaysOriginal)
let secondVC = storyboard.instantiateViewController(withIdentifier: "Second")
let navSecondController = UINavigationController(rootViewController: secondVC)
navSecondController.tabBarItem.image = UIImage(named: "image2")!.withRenderingMode(.alwaysOriginal)
navSecondController.tabBarItem.imageInsets = UIEdgeInsets(top: 8, left: 0, bottom: -8, right: 0)
if something == true {
navFirstController.tabBarItem.image = UIImage(named: "image3")!.withRenderingMode(.alwaysOriginal)
} else {
navFirstController.tabBarItem.image = UIImage(named: "image1")!.withRenderingMode(.alwaysOriginal)
}
navFirstController.tabBarItem.imageInsets = UIEdgeInsets(top: 8, left: 0, bottom: -8, right: 0)
viewControllers = [navSecondController, navFirstController]
}
}
SecondViewController
import Foundation
import UIKit
class SecondViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
}
#IBAction func btnClicked(_ sender: Any) {
//change image of tab bar item on button clicked
if let tabVC = self.tabBarController as? TabBarViewController {
tabVC.something = !tabVC.something
tabVC.setupItems()
}
}
}
OUTPUT

Switch another navigation programmatically swift

I ignore storyboard and create UINavigationController in AppDelegate.swift
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
// Override point for customization after application launch.
window = UIWindow(frame: UIScreen.main.bounds)
window?.makeKeyAndVisible()
let layout = UICollectionViewFlowLayout()
layout.scrollDirection = .horizontal
window?.rootViewController = UINavigationController(rootViewController: HomeController(collectionViewLayout: layout))//make the ViewController class to be the root
return true
}
I have leftBarButton which switches to another UINavigationController or UICollectionViewController(according to your advice)
override func viewDidLoad() {
super.viewDidLoad()
let parentMenuButton = UIButton(frame: CGRect(x: 0, y: 0, width: 34, height: 34))
parentMenuButton.addTarget(self, action: #selector(self.menuButtonOnClicked), for: .touchUpInside)
navigationItem.leftBarButtonItem = UIBarButtonItem(customView: parentMenuButton)
}
#objc func menuButtonOnClicked(){
print("menuButtonOnClicked button is pressed")
}
How can I achieve this programmatically?(switch another navigation area by pressing menu button)
let storyboard = UIStoryboard(name: "Main", bundle: nil)
let secondViewController = storyboard.instantiateViewController(withIdentifier: "SecondViewController") as! SecondViewController
self.navigationController!.pushViewController(secondViewController, animated: true)
Error:
Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: 'Storyboard () doesn't contain a view controller with identifier 'SecondViewController''
I create SecondViewController:
import UIKit
class SecondViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
}
}
Is there a way to do it without messing with storyboard?(only programatically)
You need to call push method in your menuButtonOnClicked()
#objc func menuButtonOnClicked(){
print("menuButtonOnClicked button is pressed")
let storyboard = UIStoryboard(name: "Main", bundle: nil)
let secondViewController = storyboard.instantiateViewControllerWithIdentifier("SecondViewController") as SecondViewController
self.navigationController.pushViewController(secondViewController, animated: true)
}

UITabBar Controller Present ViewController of Second tab Without Segue

I'm eliminating the storyboard from my app completely. How do I present the VC that is linked to the second tab of the TabBarController.
Setup: mainVC --- myTabBar -- tab1 - navCntrl - VC1
tab2 - navCntrl - VC2
When using a segues I used the following code:
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if (segue.identifier == myTabBar) {
let tabVC = segue.destination as? UITabBarController {
tabVC.selectedIndex = myTabBarIndex ==> 1 to reach VC2
}
// other other stuff
}
To eliminating the segues I rewrote the above but although I set the selectedIndex VC2 is not presented. Any suggestions?
func vc2Btn() {
let tabVC = MyTabBar()
tabVC.selectedIndex = 1 // ==>> Index set but can not reach VC2
present(tabVC, animated: true, completion: nil)
}
The full code of my test system:
class MyTabBar: UITabBarController, UITabBarControllerDelegate {
override func viewDidLoad() {
super.viewDidLoad()
self.delegate = self
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
// Create Tab 1
let navCtrlTab1 = UINavigationController(rootViewController: VC1())
let tabOne = navCtrlTab1
let tabOneBarItem = UITabBarItem(title: "", image: StyleKit.imageOfIconTabRecent, selectedImage: StyleKit.imageOfIconTabRecentRev)
tabOne.tabBarItem = tabOneBarItem
// Create Tab 2
let navCtrlTab2 = UINavigationController(rootViewController: VC2())
let tabTwo = navCtrlTab2
let tabTwoBarItem = UITabBarItem(title: "", image: StyleKit.imageOfIconTabNote, selectedImage: StyleKit.imageOfIconTabNoteRev)
tabTwo.tabBarItem = tabTwoBarItem
self.viewControllers = [tabOne, tabTwo]
}
}
class mainVC: UIViewController {
let btn0: UIButton = {
let button = UIButton()
button.setBackgroundImage(StyleKit.imageOfBtnBlue(btnText: "VC1"), for: UIControlState.normal)
button.addTarget(self, action:#selector(vc1Btn), for: .touchUpInside)
return button
}()
let btn1: UIButton = {
let button = UIButton()
button.setBackgroundImage(StyleKit.imageOfBtnBlue(btnText: "VC2"), for: UIControlState.normal)
button.addTarget(self, action:#selector(vc2Btn), for: .touchUpInside)
return button
}()
override func viewDidLoad() {
super.viewDidLoad()
self.view.addSubview(btn0)
self.view.addSubview(btn1)
addConstraintsWithFormat("H:|-100-[v0]", views: btn0)
addConstraintsWithFormat("H:|-100-[v0]", views: btn1)
addConstraintsWithFormat("V:|-300-[v0]-20-[v1]", views: btn0, btn1)
}
func addConstraintsWithFormat(_ format: String, views: UIView...) {
var viewsDictionary = [String: UIView]()
for (index, view) in views.enumerated() {
let key = "v\(index)"
viewsDictionary[key] = view
view.translatesAutoresizingMaskIntoConstraints = false
}
self.view.addConstraints(NSLayoutConstraint.constraints(withVisualFormat: format, options: NSLayoutFormatOptions(), metrics: nil, views: viewsDictionary))
}
func vc1Btn() {
let tabVC = MyTabBar()
tabVC.selectedIndex = 0 // ==>> this is working
present(tabVC, animated: true, completion: nil)
}
func vc2Btn() {
let tabVC = MyTabBar()
tabVC.selectedIndex = 1 // ==>> Index set but can not reach VC2
present(tabVC, animated: true, completion: nil)
}
}
class VC1: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
navigationItem.title = "VC1"
print ("VC1")
}
}
class VC2: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
navigationItem.title = "VC2"
print ("VC2")
}
}
#UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
window = UIWindow(frame: UIScreen.main.bounds)
window?.makeKeyAndVisible()
window?.rootViewController = mainVC()
return true
}
}

Present Popover View Controller Swift

When I try to display a popover view controller programmatically it won't work and I don't know why. I've copied from multiple sources on the web and nothing seems to work, I get the same error in the console every time showing Warning: Attempt to present <AddFriendsPopoverViewController> on <MainPageViewController> whose view is not in the window hierarchy! I am lost and can't seem to figure out what the problem is, thanks in advance!
Here is my swift code in my viewDidLoad() function:
let addFriendsPopoverViewController = AddFriendsPopoverViewController()
override func viewDidLoad() {
super.viewDidLoad()
if (PFUser.currentUser()?["numberOfFriends"])! as! NSObject == 0 {
print(PFUser.currentUser()?["numberOfFriends"])
let storyboard = UIStoryboard(name: "Main", bundle: nil)
let vc = storyboard.instantiateViewControllerWithIdentifier("AddFriendsPopoverViewController") as! UIViewController
vc.modalPresentationStyle = UIModalPresentationStyle.Popover
vc.preferredContentSize = CGSizeMake(50, 50)
let popoverMenuViewController = vc.popoverPresentationController
popoverMenuViewController!.permittedArrowDirections = .Any
popoverMenuViewController!.delegate = self
popoverMenuViewController!.sourceView = self.view
popoverMenuViewController!.sourceRect = CGRectMake(
100,
100,
0,
0)
self.presentViewController(vc, animated: true, completion: nil)
}
}
EDIT
I figured out that for a popover to work with iPhone the following code is required.
func adaptivePresentationStyleForPresentationController(controller: UIPresentationController!) -> UIModalPresentationStyle {
// Return no adaptive presentation style, use default presentation behaviour
return .None
}
Your view is not in the view hierarchy until it has been presented and not during viewDidLoad:
Move your code to viewDidAppear:
if (PFUser.currentUser()?["numberOfFriends"])! as! NSObject == 0 {
addFriendsPopoverViewController.modalPresentationStyle = UIModalPresentationStyle.Popover
addFriendsPopoverViewController.preferredContentSize = CGSizeMake(200, 200)
let popoverMenuViewController = addFriendsPopoverViewController.popoverPresentationController
popoverMenuViewController!.permittedArrowDirections = .Any
popoverMenuViewController!.delegate = self
popoverMenuViewController!.sourceView = self.view
popoverMenuViewController!.sourceRect = CGRect(
x: 50,
y: 50,
width: 1,
height: 1)
presentViewController(
addFriendsPopoverViewController,
animated: true,
completion: nil)
}
Your code is working right but u can not write that presentViewController code in ViewDidLoad method because viewdidLoad call till that time controller itself it not presented thats why it's not allow to presentViewController .
Write that same code in..
override func viewDidAppear(animated: Bool)
{
var controller = UIViewController()
controller.view.backgroundColor = UIColor .greenColor()
presentViewController(controller, animated: true, completion: nil)
}
I have made it simple for multiple use and its ready to use and go. just copy and paste this extension.
extension UIViewController: UIPopoverPresentationControllerDelegate{
#discardableResult func presentPopOver(_ vcIdentifier: String, _ isAnimate: Bool = true,sender:UIView,contentSize:CGSize = .init(width: 100, height: 100)) -> (UIViewController){
let popoverContentController = storyboard?.instantiateViewController(withIdentifier: vcIdentifier)
popoverContentController?.modalPresentationStyle = .popover
if let popoverPresentationController = popoverContentController?.popoverPresentationController {
popoverPresentationController.permittedArrowDirections = .up
popoverPresentationController.sourceView = sender
popoverPresentationController.sourceRect = sender.bounds
popoverContentController?.preferredContentSize = contentSize
popoverPresentationController.delegate = self
if let popoverController = popoverContentController {
present(popoverController, animated: isAnimate, completion: nil)
}
}
return popoverContentController ?? UIViewController()
}
public func adaptivePresentationStyle(for controller: UIPresentationController) -> UIModalPresentationStyle {
return .none
}
public func popoverPresentationControllerDidDismissPopover(_ popoverPresentationController: UIPopoverPresentationController) {
}
public func popoverPresentationControllerShouldDismissPopover(_ popoverPresentationController: UIPopoverPresentationController) -> Bool{
return true
}}
**how to use for presenting pop over on button click:-**
#IBAction func sortClicked(_ sender: UIButton) {
presentPopOver("PopOverVC", sender: sender)
}
***presenting pop over and to get and pass data:-***
#IBAction func sortClicked(_ sender: UIButton) {
let vc = presentPopOver("PopOverVC", sender: sender) as? PopOverVC
vc?.arrayNames = ["name1","name2"]
vc?.callBack = {name in
print(name)
vc?.dismiss(animated: true)
}
}

Present modal view controller in half size parent controller

I am trying to present modal view controller on other viewcontroller sized to half parent view controller. But it always present in full screen view.
I have created freeform sized View controller in my storyboard with fixed frame size. 320 X 250.
var storyboard = UIStoryboard(name: "Main", bundle: nil)
var pvc = storyboard.instantiateViewControllerWithIdentifier("CustomTableViewController") as ProductsTableViewController
self.presentViewController(pvc, animated: true, completion: nil)
I have tried to set frame.superview and it doesn't help.
Please advice.
You can use a UIPresentationController to achieve this.
For this you let the presenting ViewController implement the UIViewControllerTransitioningDelegate and return your PresentationController for the half sized presentation:
func presentationController(forPresented presented: UIViewController, presenting: UIViewController?, source: UIViewController) -> UIPresentationController? {
return HalfSizePresentationController(presentedViewController: presented, presenting: presentingViewController)
}
When presenting you set the presentation style to .Custom and set your transitioning delegate:
pvc.modalPresentationStyle = .custom
pvc.transitioningDelegate = self
The presentation controller only returns the frame for your presented view controller:
class HalfSizePresentationController: UIPresentationController {
override var frameOfPresentedViewInContainerView: CGRect {
guard let bounds = containerView?.bounds else { return .zero }
return CGRect(x: 0, y: bounds.height / 2, width: bounds.width, height: bounds.height / 2)
}
}
Here is the working code in its entirety:
class ViewController: UIViewController, UIViewControllerTransitioningDelegate {
#IBAction func tap(sender: AnyObject) {
let storyboard = UIStoryboard(name: "Main", bundle: nil)
let pvc = storyboard.instantiateViewController(withIdentifier: "CustomTableViewController") as! UITableViewController
pvc.modalPresentationStyle = .custom
pvc.transitioningDelegate = self
pvc.view.backgroundColor = .red
present(pvc, animated: true)
}
func presentationController(forPresented presented: UIViewController, presenting: UIViewController?, source: UIViewController) -> UIPresentationController? {
return HalfSizePresentationController(presentedViewController: presented, presenting: presentingViewController)
}
}
class HalfSizePresentationController: UIPresentationController {
override var frameOfPresentedViewInContainerView: CGRect {
guard let bounds = containerView?.bounds else { return .zero }
return CGRect(x: 0, y: bounds.height / 2, width: bounds.width, height: bounds.height / 2)
}
}
It would be a clean architect if you push some delegate methods of UIViewControllerTransitioningDelegate in your ViewController that want to be presented as half modal.
Assuming we have ViewControllerA present ViewControllerB with half modal.
in ViewControllerA just present ViewControllerB with custom modalPresentationStyle
func gotoVCB(_ sender: UIButton) {
let vc = ViewControllerB()
vc.modalPresentationStyle = .custom
present(vc, animated: true, completion: nil)
}
And in ViewControllerB:
import UIKit
final class ViewControllerB: UIViewController {
lazy var backdropView: UIView = {
let bdView = UIView(frame: self.view.bounds)
bdView.backgroundColor = UIColor.black.withAlphaComponent(0.5)
return bdView
}()
let menuView = UIView()
let menuHeight = UIScreen.main.bounds.height / 2
var isPresenting = false
init() {
super.init(nibName: nil, bundle: nil)
modalPresentationStyle = .custom
transitioningDelegate = self
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .clear
view.addSubview(backdropView)
view.addSubview(menuView)
menuView.backgroundColor = .red
menuView.translatesAutoresizingMaskIntoConstraints = false
menuView.heightAnchor.constraint(equalToConstant: menuHeight).isActive = true
menuView.bottomAnchor.constraint(equalTo: view.bottomAnchor).isActive = true
menuView.leadingAnchor.constraint(equalTo: view.leadingAnchor).isActive = true
menuView.trailingAnchor.constraint(equalTo: view.trailingAnchor).isActive = true
let tapGesture = UITapGestureRecognizer(target: self, action: #selector(ViewControllerB.handleTap(_:)))
backdropView.addGestureRecognizer(tapGesture)
}
#objc func handleTap(_ sender: UITapGestureRecognizer) {
dismiss(animated: true, completion: nil)
}
}
extension ViewControllerB: UIViewControllerTransitioningDelegate, UIViewControllerAnimatedTransitioning {
func animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController) -> UIViewControllerAnimatedTransitioning? {
return self
}
func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {
return self
}
func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
return 1
}
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
let containerView = transitionContext.containerView
let toViewController = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.to)
guard let toVC = toViewController else { return }
isPresenting = !isPresenting
if isPresenting == true {
containerView.addSubview(toVC.view)
menuView.frame.origin.y += menuHeight
backdropView.alpha = 0
UIView.animate(withDuration: 0.4, delay: 0, options: [.curveEaseOut], animations: {
self.menuView.frame.origin.y -= self.menuHeight
self.backdropView.alpha = 1
}, completion: { (finished) in
transitionContext.completeTransition(true)
})
} else {
UIView.animate(withDuration: 0.4, delay: 0, options: [.curveEaseOut], animations: {
self.menuView.frame.origin.y += self.menuHeight
self.backdropView.alpha = 0
}, completion: { (finished) in
transitionContext.completeTransition(true)
})
}
}
}
The result:
All code is published on my Github
Just in case someone is looking to do this with Swift 4, as I was.
class MyViewController : UIViewController {
...
#IBAction func dictionaryButtonTouchUp(_ sender: UIButton) {
let modalViewController = ...
modalViewController.transitioningDelegate = self
modalViewController.modalPresentationStyle = .custom
self.present(modalViewController, animated: true, completion: nil)
}
}
extension MyViewController : UIViewControllerTransitioningDelegate {
func presentationController(forPresented presented: UIViewController, presenting: UIViewController?, source: UIViewController) -> UIPresentationController? {
return HalfSizePresentationController(presentedViewController: presented, presenting: presenting)
}
}
Where the HalfSizePresentationController class is composed of:
class HalfSizePresentationController : UIPresentationController {
override var frameOfPresentedViewInContainerView: CGRect {
get {
guard let theView = containerView else {
return CGRect.zero
}
return CGRect(x: 0, y: theView.bounds.height/2, width: theView.bounds.width, height: theView.bounds.height/2)
}
}
}
Cheers!
Jannis captured the overall strategy well. It didn't work for me in iOS 9.x with swift 3. On the presenting VC, the action to launch the presented VC is similar to what was presented above with some very minor changes as below:
let storyboard = UIStoryboard(name: "Main", bundle: nil)
let pvc = storyboard.instantiateViewController(withIdentifier: "SomeScreen") as SomeViewController
pvc.modalPresentationStyle = .custom
pvc.transitioningDelegate = self
present(pvc, animated: true, completion: nil)
To implement UIViewControllerTransitioningDelegate on the same presenting VC, the syntax is quite different as highlighted in SO answer in https://stackoverflow.com/a/39513247/2886158. This is was the most tricky part for me. Here is the protocol implementation:
func presentationController(forPresented presented: UIViewController, presenting: UIViewController?, source: UIViewController) -> UIPresentationController? {
return HalfSizePresentationController(presentedViewController:presented, presenting: presenting)
}
For the UIPresentationController class, I had to override the variable frameOfPresentedViewInContainerView, not method, as below:
class HalfSizePresentationController: UIPresentationController {
override var frameOfPresentedViewInContainerView: CGRect {
return CGRect(x: 0, y: 0, width: containerView!.bounds.width, height: containerView!.bounds.height/2)
}
}
There were some questions about how to dismiss the view after presentation. You can implement all the usual logic on your presented VC like any other VC. I implementation an action to dismiss the view in SomeViewController when a user tabs outside the presented VC.
Details
Xcode 12.2 (12B45b)
Swift 5.3
Solution 1. Default transition
Idea:
Hide root view of the ChildViewController and add new view that will be used as the root view.
Main logic:
class ChildViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .clear
let contentView = UIView()
contentView.backgroundColor = .lightGray
view.addSubview(contentView)
//...
}
}
Solution 1. Full sample
import UIKit
// MARK: ParentViewController
class ParentViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let button = UIButton(frame: CGRect(x: 50, y: 50, width: 200, height: 60))
button.setTitle("Present VC", for: .normal)
button.setTitleColor(.blue, for: .normal)
button.addTarget(self, action: #selector(touchedUpInside), for: .touchUpInside)
view.addSubview(button)
}
#objc func touchedUpInside(source: UIButton) {
let viewController = ChildViewController()
present(viewController, animated: true, completion: nil)
}
}
// MARK: ChildViewController
class ChildViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .clear
let contentView = UIView()
contentView.backgroundColor = .lightGray
view.addSubview(contentView)
contentView.translatesAutoresizingMaskIntoConstraints = false
contentView.heightAnchor.constraint(equalTo: view.heightAnchor, multiplier: 0.5).isActive = true
contentView.leftAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leftAnchor).isActive = true
contentView.rightAnchor.constraint(equalTo: view.safeAreaLayoutGuide.rightAnchor).isActive = true
contentView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor).isActive = true
}
}
Solution 2. Custom transition
Idea:
Change size of the root view of the ChildViewController.
Main logic:
ModalPresentationController
protocol ModalPresentationControllerDelegate: class {
func updateFrameOfPresentedViewInContainerView(frame: CGRect) -> CGRect
}
class ModalPresentationController: UIPresentationController {
private weak var modalPresentationDelegate: ModalPresentationControllerDelegate!
convenience
init(delegate: ModalPresentationControllerDelegate,
presentedViewController: UIViewController,
presenting presentingViewController: UIViewController?) {
self.init(presentedViewController: presentedViewController,
presenting: presentingViewController)
self.modalPresentationDelegate = delegate
}
override var frameOfPresentedViewInContainerView: CGRect {
get { modalPresentationDelegate.updateFrameOfPresentedViewInContainerView(frame: super.frameOfPresentedViewInContainerView) }
}
}
Update root view size
class ChildViewController: UIViewController {
init() {
//...
transitioningDelegate = self
modalPresentationStyle = .custom
}
}
extension ChildViewController: UIViewControllerTransitioningDelegate {
func presentationController(forPresented presented: UIViewController,
presenting: UIViewController?,
source: UIViewController) -> UIPresentationController? {
ModalPresentationController(delegate: self, presentedViewController: presented, presenting: presenting)
}
}
extension ChildViewController: ModalPresentationControllerDelegate {
func updateFrameOfPresentedViewInContainerView(frame: CGRect) -> CGRect {
CGRect(x: 0, y: frame.height/2, width: frame.width, height: frame.height/2)
}
}
Solution 2. Full sample
Do not forget to paste here ModalPresentationController that defined above
import UIKit
// MARK: ParentViewController
class ParentViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let button = UIButton(frame: CGRect(x: 50, y: 50, width: 200, height: 60))
button.setTitle("Present VC", for: .normal)
button.setTitleColor(.blue, for: .normal)
button.addTarget(self, action: #selector(touchedUpInside), for: .touchUpInside)
view.addSubview(button)
}
#objc func touchedUpInside(source: UIButton) {
let viewController = ChildViewController()
present(viewController, animated: true, completion: nil)
}
}
// MARK: ChildViewController
class ChildViewController: UIViewController {
init() {
super.init(nibName: nil, bundle: nil)
transitioningDelegate = self
modalPresentationStyle = .custom
view.backgroundColor = .lightGray
}
required init?(coder: NSCoder) { super.init(coder: coder) }
}
extension ChildViewController: UIViewControllerTransitioningDelegate {
func presentationController(forPresented presented: UIViewController,
presenting: UIViewController?,
source: UIViewController) -> UIPresentationController? {
ModalPresentationController(delegate: self, presentedViewController: presented, presenting: presenting)
}
}
extension ChildViewController: ModalPresentationControllerDelegate {
func updateFrameOfPresentedViewInContainerView(frame: CGRect) -> CGRect {
CGRect(x: 0, y: frame.height/2, width: frame.width, height: frame.height/2)
}
}
Starting with iOS 15, UISheetPresentationController now has a medium appearance that presents the view controller for half of the screen.
Here is Swift 4.0 some class name is change frameOfPresentedViewInContainerView get method
Step 1: Set Delegate
class ViewController: UIViewController, UIViewControllerTransitioningDelegate
Step 2: Set Delegate Method
func presentationController(forPresented presented: UIViewController, presenting: UIViewController?, source: UIViewController) -> UIPresentationController? {
return SetSizePresentationController(presentedViewController: presented, presenting: presenting)
}
Step 3: Here you can create your own Class for set size (CGRect)
class SetSizePresentationController : UIPresentationController {
override var frameOfPresentedViewInContainerView: CGRect {
get {
return CGRect(x: 0, y: (containerView?.bounds.height ?? 0)/2, width: containerView?.bounds.width ?? 0, height: (containerView?.bounds.height ?? 0)/2)
}
}
}
Step 4: here 2 lines important transitioningdelegate & UIModalPresentationStyle.custom
let storyboard = UIStoryboard(name: "User", bundle: nil)
let vc = storyboard.instantiateViewController(withIdentifier: "LicenceViewController") as! LicenceViewController
vc.modalPresentationStyle = UIModalPresentationStyle.custom
vc.transitioningDelegate = self
present(vc, animated: true)
To add to Jannis' answer:
In case your pop-view is a UIViewController to which you ADD a Table on load/setup, you will need to ensure that the table frame you create matches the desired width of the actual view.
For example:
let tableFrame: CGRect = CGRectMake(0, 0, chosenWidth, CGFloat(numOfRows) * rowHeight)
where chosenWidth is the width you set in your custom class (in the above: containerView.bounds.width)
You do not need to enforce anything on the Cell itself as the table container (at least in theory) should force the cell to the right width.
I use below logic to present half screen ViewController
let storyboard = UIStoryboard(name: "Main", bundle: nil)
let expVC = storyboard.instantiateViewController(withIdentifier: "AddExperinceVC") as! AddExperinceVC
expVC.modalPresentationStyle = UIModalPresentationStyle.overCurrentContext
self.present(expVC, animated: true, completion: nil)
Present normally, then use systemLayoutSizeFitting in viewDidLayoutSubviews to adjust the frame to the minimum required size. This retains the visuals and physics provided by Apple –which you will lose using a custom presentation.
See the sample code on this answer.

Resources