I have an iMessage extension and I'm having some issues with the top layout guide. I have an MSMessagesAppViewController that handles changes between presentation styles. In my extension I have a button. When it is clicked I transition to expanded presentation style and then present a view controller modally. Here's the problem: my UI in the second VC is getting hidden behind the top navigation bar. I thought this was strange as I set my constraints to the top layout guide. So I dug through my code and started debugging the top layout guide. I noticed that after I transition to expanded presentation style, topLayoutGuide.length = 86. That's how it should be. But when I present the second view controller modally, the top layout guide is reset to 0. Why isn't it 86 as it should be? Here is my code:
In my main viewController:
#IBAction func addStickerButtonPressed(_ sender: AnyObject) {
shouldPerformCreateSegue = true
theSender = sender
requestPresentationStyle(.expanded)
}
override func didTransition(to presentationStyle: MSMessagesAppPresentationStyle) {
if presentationStyle == .expanded {
if shouldPerformCreateSegue == true {
shouldPerformCreateSegue = false
performSegue(withIdentifier: "CreateStickerSegue", sender: theSender)//here is where I present the new viewController
} else {
searchBar.becomeFirstResponder()
searchBar.placeholder = nil
searchBar.showsCancelButton = true
searchBar.tintColor = UIColor.white
}
} else {
searchBar.showsCancelButton = false
}
print(topLayoutGuide.length) //This prints out 86
}
In the other modally presented view controller:
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
self.view.addConstraint(navBar.topAnchor.constraint(equalTo: topLayoutGuide.bottomAnchor))
print(topLayoutGuide.length) //This prints out 0
}
As a workaround I use UIPresentationController, which shifts the modal view controller by topLayoutGuide.length points:
class MyViewController: MSMessagesAppViewController {
private func presentModalViewController() {
let imagePicker = UIImagePickerController()
imagePicker.delegate = self
imagePicker.sourceType = .savedPhotosAlbum
imagePicker.modalPresentationStyle = .custom
imagePicker.transitioningDelegate = self
present(imagePicker, animated: true, completion: nil)
}
}
// MARK: - UIViewControllerTransitioningDelegate
extension MyViewController: UIViewControllerTransitioningDelegate {
func presentationController(forPresented presented: UIViewController, presenting: UIViewController?, source: UIViewController) -> UIPresentationController? {
let vc = PresentationController(presentedViewController: presented, presenting: presenting)
// I really don't want to hardcode the value of topLayoutGuideLength here, but when the extension is in compact mode, topLayoutGuide.length returns 172.0.
vc.topLayoutGuideLength = topLayoutGuide.length > 100 ? 86.0 : topLayoutGuide.length
return vc
}
}
class PresentationController: UIPresentationController {
var topLayoutGuideLength: CGFloat = 0.0
override var frameOfPresentedViewInContainerView: CGRect {
guard let containerView = containerView else {
return super.frameOfPresentedViewInContainerView
}
return CGRect(x: 0, y: topLayoutGuideLength, width: containerView.bounds.width, height: containerView.bounds.height - topLayoutGuideLength)
}
}
The only problem is when you're calling presentModalViewController from compact mode, topLayoutGuide.length is 172.0 for unknown reason. So I had to hardcode a value for that case.
I believe this was known bug on previous iOS 10 beta. I had same issue and top and bottom layout guide works as I expect after I upgraded iOS version to latest.
I used a slightly varied version of Andrey's
class MyViewController: MSMessagesAppViewController {
private func presentModalViewController() {
let imagePicker = UIImagePickerController()
imagePicker.delegate = self
imagePicker.sourceType = .savedPhotosAlbum
imagePicker.modalPresentationStyle = .custom
imagePicker.transitioningDelegate = self
present(
imagePicker,
animated: true,
completion: nil
)
}
}
extension MyViewController: UIViewControllerTransitioningDelegate {
func presentationController(
forPresented presented: UIViewController,
presenting: UIViewController?,
source: UIViewController
) -> UIPresentationController? {
let vc = PresentationController(
presentedViewController: presented,
presenting: presenting
)
vc.framePresented = modalBoundaries.frame
return vc
}
}
class PresentationController: UIPresentationController {
var framePresented = CGRect.zero
override var frameOfPresentedViewInContainerView: CGRect {
return framePresented
}
}
modalBoundaries being a dummy UIView constrained (via XIB in my case) to respect any TopLayoutGuide length.
Related
I want all of the functionality of a pageSheet UIModalPresentationStyle segue but I only want the presented ViewController to show half the screen (see the example in the image below).
I am presenting it modally using the pageSheet modalPresentationStyle but it always presents it at 100% height.
I haven't been able to figure out how to limit or modify a ViewController's height. I tried the following in my SecondViewController but it didn't work:
import UIKit
class SecondViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
self.preferredContentSize = CGSize(width: self.view.frame.width, height: 400)
}
}
I'm initiating the segue with Storyboard Segues, and a button that presents it modally:
I figured out a way to do it, which I find to be pretty simple:
import UIKit
class SecondViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let newView = UIView(frame: CGRect(x: 0, y: 500, width: self.view.frame.width, height: 400))
newView.backgroundColor = .yellow
newView.layer.cornerRadius = 20
self.view = UIView(frame: CGRect(x: 0, y: 0, width: self.view.frame.width, height: self.view.frame.height))
// self.view is now a transparent view, so now I add newView to it and can size it however, I like.
self.view.addSubview(newView)
// works without the tap gesture just fine (only dragging), but I also wanted to be able to tap anywhere and dismiss it, so I added the gesture below
let tap = UITapGestureRecognizer(target: self, action: #selector(self.handleTap(_:)))
self.view.addGestureRecognizer(tap)
}
#objc func handleTap(_ sender: UITapGestureRecognizer? = nil) {
dismiss(animated: true, completion: nil)
}
}
In order to achieve you will need to subclass UIPresentationController and implement the protocol UIViewControllerTransitioningDelegate in the presenting controller and set transitioningDelegate and modalPresentationStyle of presented view controller as self(presenting view controller) and .custom respectively. Implement an optional function of UIViewControllerTransitioningDelegate:
func presentationController(forPresented presented: UIViewController, presenting: UIViewController?, source _: UIViewController) -> UIPresentationController?
and return the custom presentationController which sets the height of presented controller as per your requirement.
Basic code that might help:
class CustomPresentationController: UIPresentationController {
var presentedViewHeight: CGFloat
init(presentedViewController: UIViewController, presenting presentingViewController: UIViewController?, presentedViewHeight: CGFloat) {
self.presentedViewHeight = presentedViewHeight
super.init(presentedViewController: presentedViewController, presenting: presentingViewController)
}
override var frameOfPresentedViewInContainerView: CGRect {
var frame: CGRect = .zero
frame.size = CGSize(width: containerView!.bounds.width, height: presentedViewHeight)
frame.origin.y = containerView!.frame.height - presentedViewHeight
return frame
}
}
Implementation of optional function:
func presentationController(forPresented presented: UIViewController, presenting: UIViewController?, source _: UIViewController) -> UIPresentationController? {
let presentationController = CustomPresentationController(presentedViewController: presented, presenting: presenting, presentedViewHeight: 100)
return presentationController
}
You can also play with other optional functions and adding some other functionalities to CustomPresentationController like adding blur background, adding tap functionality and swipe gesture.
We can add our view in UIActivityController and remove UIActivityController's default view and if you add navigation controller so you will get navigation also, so you can do half your controller by this:
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
}
#IBAction func actionPresent(_ sender: UIBarButtonItem) {
let vc1 = storyboard?.instantiateViewController(withIdentifier: "ViewControllerCopy")
let vc = ActivityViewController(controller: vc1!)
self.present(vc, animated: true, completion: nil)
}
}
class ActivityViewController: UIActivityViewController {
private let controller: UIViewController!
required init(controller: UIViewController) {
self.controller = controller
super.init(activityItems: [], applicationActivities: nil)
}
override func viewDidLoad() {
super.viewDidLoad()
let subViews = self.view.subviews
for view in subViews {
view.removeFromSuperview()
}
self.addChild(controller)
self.view.addSubview(controller.view)
}
}
for example you can check this repo:
https://github.com/SomuYadav/HalfViewControllerTransition
Once, the user taps a button, I want my modalViewController to appear as a small square in the middle of the screen (where you can still see the original view controller in the background).
Almost every answer on stackoverflow I find uses the storyboard to create a modal view controller, but I've gotten this far with everything I've found.
When you tap the button that is supposed to bring up the modal view, this function is called:
func didTapButton() {
let modalViewController = ModalViewController()
modalViewController.definesPresentationContext = true
modalViewController.modalPresentationStyle = .overCurrentContext
navigationController?.present(modalViewController, animated: true, completion: nil)
}
And the modalViewController contains:
import UIKit
class ModalViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .blue
view.isOpaque = false
self.preferredContentSize = CGSize(width: 100, height: 100)
}
}
Based on the answers I found, I was under the impression that if I set preferredContentSize = CGSize(width: 100, height: 100), then it would make the modal view controller 100px x 100px.
However, the view controller takes up the entire screen (except for the tab bar because I set modalViewController.modalPresentationStyle = .overCurrentContext
I'm obviously missing a step here, but I want to do everything programmatically as I'm not using the Storyboard at all in my project (except for setting the opening controller)
Thanks in advance for you help!!
The modalPresentationStyle documentation tells us
In a horizontally compact environment, modal view controllers are always presented full-screen.
So, if you want to do this in a iPhone in portrait mode, you have to specify a .custom presentation style and have your transitioning delegate vend a custom presentation controller.
I’d personally let my second view controller manage its own presentation parameters, so my first view controller might only:
class FirstViewController: UIViewController {
#IBAction func didTapButton(_ sender: Any) {
let controller = storyboard!.instantiateViewController(withIdentifier: "SecondViewController")
present(controller, animated: true)
}
}
And then my second view controller would specify a custom transition and specify a custom transitioning delegate:
class SecondViewController: UIViewController {
private var customTransitioningDelegate = TransitioningDelegate()
override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) {
super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil)
configure()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
configure()
}
}
private extension SecondViewController {
func configure() {
modalPresentationStyle = .custom
modalTransitionStyle = .crossDissolve // use whatever transition you want
transitioningDelegate = customTransitioningDelegate
}
}
Then that transitioning delegate would vend the custom presentation controller:
class TransitioningDelegate: NSObject, UIViewControllerTransitioningDelegate {
func presentationController(forPresented presented: UIViewController, presenting: UIViewController?, source: UIViewController) -> UIPresentationController? {
return PresentationController(presentedViewController: presented, presenting: presenting)
}
}
And that presentation controller would specify its size:
class PresentationController: UIPresentationController {
override var frameOfPresentedViewInContainerView: CGRect {
let bounds = presentingViewController.view.bounds
let size = CGSize(width: 200, height: 100)
let origin = CGPoint(x: bounds.midX - size.width / 2, y: bounds.midY - size.height / 2)
return CGRect(origin: origin, size: size)
}
override init(presentedViewController: UIViewController, presenting presentingViewController: UIViewController?) {
super.init(presentedViewController: presentedViewController, presenting: presentingViewController)
presentedView?.autoresizingMask = [
.flexibleTopMargin,
.flexibleBottomMargin,
.flexibleLeftMargin,
.flexibleRightMargin
]
presentedView?.translatesAutoresizingMaskIntoConstraints = true
}
}
This is just the tip of the iceberg with custom transitions. You can specify the animation controller (for custom animations), dim/blur the background, etc. See WWDC 2013 Custom Transitions Using View Controllers video for a primer on custom transitions, and WWDC 2014 videos View Controller Advancements in iOS 8 and A Look Inside Presentation Controllers dive into the details of presentation controllers.
For example, you might want to dim and blur the background when you present your modal view. So you might add presentationTransitionWillBegin and dismissalTransitionWillBegin to animate the presentation of this “dimming" view:
class PresentationController: UIPresentationController {
...
let dimmingView: UIView = {
let dimmingView = UIVisualEffectView(effect: UIBlurEffect(style: .dark))
dimmingView.translatesAutoresizingMaskIntoConstraints = false
return dimmingView
}()
override func presentationTransitionWillBegin() {
super.presentationTransitionWillBegin()
let superview = presentingViewController.view!
superview.addSubview(dimmingView)
NSLayoutConstraint.activate([
dimmingView.leadingAnchor.constraint(equalTo: superview.leadingAnchor),
dimmingView.trailingAnchor.constraint(equalTo: superview.trailingAnchor),
dimmingView.bottomAnchor.constraint(equalTo: superview.bottomAnchor),
dimmingView.topAnchor.constraint(equalTo: superview.topAnchor)
])
dimmingView.alpha = 0
presentingViewController.transitionCoordinator?.animate(alongsideTransition: { _ in
self.dimmingView.alpha = 1
}, completion: nil)
}
override func dismissalTransitionWillBegin() {
super.dismissalTransitionWillBegin()
presentingViewController.transitionCoordinator?.animate(alongsideTransition: { _ in
self.dimmingView.alpha = 0
}, completion: { _ in
self.dimmingView.removeFromSuperview()
})
}
}
That yields:
You can set view controller's background color to clear, and then create a view in the middle of the view controller, and set the modal presentation style to .overCurrentContext, and this way you will see the view controller from behind.
Here is the edited example:
func didTapButton() {
let modalViewController = storyboard?.instantiateViewController(withIdentifier: "ModalViewController") as! ModalViewController
modalViewController.modalPresentationStyle = .overCurrentContext
modalViewController.modalTransitionStyle = .crossDissolve // this will look more natural for this situation
navigationController?.present(modalViewController, animated: true, completion: nil)
}
Here is your presented view controller class:
import UIKit
class ModalViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .clear
createTheView()
}
private func createTheView() {
let xCoord = self.view.bounds.width / 2 - 50
let yCoord = self.view.bounds.height / 2 - 50
let centeredView = UIView(frame: CGRect(x: xCoord, y: yCoord, width: 100, height: 100))
centeredView.backgroundColor = .blue
self.view.addSubview(centeredView)
}
}
You can already build from here: add your desired look for the "smaller" view controller :)
I currently working on an iOS app and I want to use a bottom navigation drawer from material-io. So I did it like it is explained in the examples on the site. But when I present the navigation Drawer the ViewController only gets a bit darker and the contentView of the drawer isn't shown.
Here is my Code:
import Foundation
import UIKit
import MaterialComponents
class CreateSubjectView: UIViewController, UITextFieldDelegate {
...
override func viewDidLoad() {
...
let bottomDrawerViewController = MDCBottomDrawerViewController()
self.modalPresentationStyle = .popover
let newViewController = self.storyboard?.instantiateViewController(withIdentifier: "TEST")
bottomDrawerViewController.contentViewController = newViewController
present(bottomDrawerViewController, animated: true, completion: nil)
...
}
...
}
Your view controller to be shown in drawer must have specified preferred content size.
Here is a demo of minimal controller. (Note: modalPresentationStyle = .popover has no effect on MDCBottomDrawerViewController)
Tested with Xcode 12
// button action in parent controller
#objc private func presentNavigationDrawer() {
let bottomDrawerViewController = MDCBottomDrawerViewController()
bottomDrawerViewController.contentViewController = DemoViewController()
present(bottomDrawerViewController, animated: true, completion: nil)
}
}
class DemoViewController: UIViewController {
override func loadView() {
super.loadView()
let view = UIView()
view.backgroundColor = .red
self.view = view
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
// specify your content preferred height explicitly
self.preferredContentSize = CGSize(width: 0, height: 400) // required !!
}
#available(iOS 11.0, *)
override func viewSafeAreaInsetsDidChange() {
super.viewSafeAreaInsetsDidChange()
// specify your content preferred height explicitly
self.preferredContentSize = CGSize(width: 0, height: 400) // required !!
}
}
Move this to viewWillAppear/ viewDidAppear once as it's too early for viewDidLoad to present a vc
class CreateSubjectView: UIViewController, UITextFieldDelegate {
let bottomDrawerViewController = MDCBottomDrawerViewController()
var once = true
override func viewDidLoad() {
super.viewDidLoad()
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
if once {
let newViewController = self.storyboard?.instantiateViewController(withIdentifier: "TEST")
bottomDrawerViewController.contentViewController = newViewController
present(bottomDrawerViewController, animated: true, completion: nil)
once = false
}
}
}
How can I get a presenting modal to be of custom size? Tried lots of different solutions, many which seem obsolete
This is how I instantiate the modal view from the parent view controller:
self.definesPresentationContext = true
let vc = (storyboard?.instantiateViewController(withIdentifier: "modalViewController"))!
vc.modalPresentationStyle = .overCurrentContext
vc.preferredContentSize = CGSize(width: 100, height: 100)
present(vc, animated: true, completion: nil)
But, the modal view covers the full screen instead of just occupying 100 * 100.
You need to implement UIViewControllerTransitioningDelegate methods and UIViewControllerAnimatedTransitioning methods for customizing the presented UIViewController size.
To know how to implement custom animator,
Refer to: https://github.com/pgpt10/Custom-Animator
Edit:
class ViewController: UIViewController
{
//MARK: Private Properties
fileprivate let animator = Animator()
//MARK: View Lifecycle Methods
override func viewDidLoad()
{
super.viewDidLoad()
}
override func awakeFromNib()
{
super.awakeFromNib()
self.transitioningDelegate = self
self.modalPresentationStyle = .custom
}
//MARK: Button Action Methods
#IBAction func dismissController(_ sender: UIButton)
{
self.dismiss(animated: true, completion: nil)
}
}
// MARK: - UIViewControllerTransitioningDelegate Methods
extension ViewController : UIViewControllerTransitioningDelegate
{
func animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController) -> UIViewControllerAnimatedTransitioning?
{
self.animator.transitionType = .zoom
self.animator.size = CGSize(width: 100, height: 100)
return self.animator
}
func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning?
{
return self.animator
}
}
I encountered a strange bug. I am just using iOS's custom transitioning method for UIViewControllers using UIViewControllerTransitioningDelegate together with an implementation of UIViewControllerAnimatedTransitioning. It all seems to work fine, until I do exactly the following:
open the app
present another view controller with my custom transition
rotate to landscape
dismiss the just presented view controller
That's all! What happens now is the following: I see a large black bar on the right side of the initial view controller (as if that controller's view wasn't rotated to landscape).
The funny thing is this only goes wrong in iOS 9, in iOS 8 everything seems to work just fine. Did anything change with custom transition API I don't know of? Or is this simply a really nasty iOS 9 bug? If anyone can tell me what I did wrong or if anyone can provide me with a workaround I would really appreciate that!
These classes reproduce the problem:
import UIKit
class ViewController: UIViewController, UIViewControllerTransitioningDelegate {
override func viewDidLoad() {
super.viewDidLoad()
let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: "tap")
view.addGestureRecognizer(tapGestureRecognizer)
}
func tap() {
let controller = ModalViewController()
controller.transitioningDelegate = self
presentViewController(controller, animated: true, completion: nil)
}
func animationControllerForPresentedController(presented: UIViewController,
presentingController presenting: UIViewController,
sourceController source: UIViewController) -> UIViewControllerAnimatedTransitioning? {
return Transitioning()
}
func animationControllerForDismissedController(dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {
return Transitioning()
}
}
The presented view controller:
import UIKit
class ModalViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = UIColor.redColor()
let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: "tap")
view.addGestureRecognizer(tapGestureRecognizer)
}
func tap() {
dismissViewControllerAnimated(true, completion: nil)
}
}
And finally the UIViewControllerAnimatedTransitioning implementation:
import UIKit
class Transitioning: NSObject, UIViewControllerAnimatedTransitioning {
func transitionDuration(transitionContext: UIViewControllerContextTransitioning?) -> NSTimeInterval {
return 0.5
}
func animateTransition(transitionContext: UIViewControllerContextTransitioning) {
let fromView = transitionContext.viewForKey(UITransitionContextFromViewKey)
let toView = transitionContext.viewForKey(UITransitionContextToViewKey)
let containerView = transitionContext.containerView()
if let fromView = fromView, toView = toView {
containerView?.addSubview(fromView)
containerView?.addSubview(toView)
toView.alpha = 0
UIView.animateWithDuration(0.5, animations: {
toView.alpha = 1
}, completion: {
finished in
transitionContext.completeTransition(true)
})
}
}
}
I generally use the following in animateTransition:
toView.frame = fromView.frame
FYI, you don't have to add fromView to the hierarchy, as it's already there.