When I'm showing a popover, I expect all views outside the popover to be dimmed. When I create a popover via IB, this works fine. When I create a popover programmatically and call it via an UIBarButtonItem, this doesn't quite work: the back chevron in the navigationbar is not dimmed. Instead, it remains blue:
Code:
class GreenViewController: UIViewController {
private var barButtonItem: UIBarButtonItem!
func barButtonItemAction() {
let blueViewController = BlueViewController()
let navigationController = UINavigationController(rootViewController: blueViewController)
navigationController.modalPresentationStyle = .popover
navigationController.popoverPresentationController?.barButtonItem = self.barButtonItem
self.present(navigationController, animated: true, completion: nil)
}
override func viewDidLoad() {
super.viewDidLoad()
self.barButtonItem = UIBarButtonItem(title: "Show blue popover", style: .plain, target: self, action: #selector(barButtonItemAction))
self.navigationItem.rightBarButtonItem = barButtonItem
}
}
Why does this happen?
Test project on Github:
https://github.com/bvankuik/TestNavigationBarChevronTint/
I think something may be off in the view hierarchy when the popovercontroller uses the UIBarButtonItem as it's anchor. In InterfaceBuilder, the UIButton is the anchor for the presented popover, and since the UIButton is in the view hierarchy of the presenting view controller, seems to just work.
So I attempted to reproduce some similar conditions by setting the sourceRect and sourceView properties on the popoverPresentationController as follows and it did the trick.
class GreenViewController: UIViewController, UIPopoverPresentationControllerDelegate {
private var barButtonItem: UIBarButtonItem!
override func viewDidLoad() {
super.viewDidLoad()
barButtonItem = UIBarButtonItem(title: "Show blue popover", style: .plain,
target: self, action: #selector(barButtonItemAction))
navigationItem.rightBarButtonItem = barButtonItem
}
// Defined constants for solution readability
private let sourceRectHeight : CGFloat = 44.0 // NavigationBar Height?
private let sourceRectWidth : CGFloat = 160.0 // UIBarButtonItem Width?
private let sourceRectRightMargin : CGFloat = 20.0 // Right Margin
// This returns the source rect to align our popoverPresentationController
// against, this is pretty much my imaginary frame of the UIBarButtonItem
private var sourceRect : CGRect
{
var rect = navigationController!.navigationBar.frame
rect.origin.x = view.bounds.width - sourceRectWidth - sourceRectRightMargin
rect.origin.y = sourceRectHeight / 2.0
rect.size.width = sourceRectWidth
return rect
}
func barButtonItemAction() {
let blueViewController = BlueViewController()
let navigationController = UINavigationController(rootViewController: blueViewController)
navigationController.modalPresentationStyle = .popover
// Instead of setting the barButtonItem on the popoverPresentationController
// set the srouce view as the root view of the presenting controller
navigationController.popoverPresentationController?.sourceView = view
// Set the source rec to present from, which is calclated relative to the width
// of the current device orientation
navigationController.popoverPresentationController?.sourceRect = sourceRect
// Set self as the delegate for the popoverPresentationController because
// we need to provide a relaculated rect when the device changes orientation
navigationController.popoverPresentationController?.delegate = self
// Present the view controller, and voila :)
self.present(navigationController, animated: true, completion: nil)
}
// UIPopoverPresentationControllerDelegate method that allows us to update
// the source rect of the popover after an orientation change has occurred,
// which calculated relative to with in the sourceRect property above
public func popoverPresentationController(_ popoverPresentationController: UIPopoverPresentationController,
willRepositionPopoverTo rect: UnsafeMutablePointer<CGRect>,
in view: AutoreleasingUnsafeMutablePointer<UIView>)
{
rect.initialize(to: sourceRect)
}
}
Hope this helps :)
Related
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 have a UIViewController that only shows a UIView at the bottom with a fixed height:
fileprivate lazy var box: UIView = {
let box = UIView()
box.backgroundColor = UIColor.blue
return box
}()
And I'd like to set a clear color to the rest of the view (self.view). I'm presenting the view controller from a navigation controller. I'm trying this:
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = UIColor.clear
view.isOpaque = false
configView()
configConstraints()
}
The subview is displayed in blue but the rest of the view is opaque white, I need it to be transparent to see the previous viewController's view.
What can I be missing?
To have transparent Model view controller background you have to set UIViewController providesPresentationContextTransitionStyle and definesPresentationContext properties while presenting the view controller.
Example
if let vc = self.storyboard?.instantiateViewController(withIdentifier: "YOUR_VIEW_CONTROLLER_ID") {
vc.providesPresentationContextTransitionStyle = true
vc.definesPresentationContext = true
vc.modalPresentationStyle=UIModalPresentationStyle.overCurrentContext
self.present(vc, animated: true, completion: nil)
}
I'm having trouble changing the size of my popover presentation. Here is what I have so far
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) // func for popover
{
if segue.identifier == "popoverView"
{
let vc = segue.destinationViewController
let controller = vc.popoverPresentationController
if controller != nil
{
controller?.delegate = self
controller?.sourceView = self.view
controller?.sourceRect = CGRect(x:CGRectGetMidX(self.view.bounds), y: CGRectGetMidY(self.view.bounds),width: 315,height: 230)
controller?.permittedArrowDirections = UIPopoverArrowDirection(rawValue: 0)
}
}
}
So far all this does is center the popover and remove the arrow, which is good. but it doesn't resize the container. any help would be greatly appreciated. thank you.
when I use preferredContentSize I get the error "Cannot assign to property: 'preferredContentSize' is immutable"
Set the preferred content size on the view controller being presented not the popoverPresentationController
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) // func for popover
{
if segue.identifier == "popoverView"
{
let vc = segue.destinationViewController
vc.preferredContentSize = CGSize(width: 200, height: 300)
let controller = vc.popoverPresentationController
controller?.delegate = self
//you could set the following in your storyboard
controller?.sourceView = self.view
controller?.sourceRect = CGRect(x:CGRectGetMidX(self.view.bounds), y: CGRectGetMidY(self.view.bounds),width: 315,height: 230)
controller?.permittedArrowDirections = UIPopoverArrowDirection(rawValue: 0)
}
}
I fixed it via storyboard :
Click on your controller
Click on Attribute inspector
ViewController>
Check Use Preferred Explicit size and input values.
Using Auto Layout
It may be worth mentioning that you can use layout constraints instead of setting preferredContentSize to specific values. To do so,
Add this to your view controller:
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
self.preferredContentSize = self.view.systemLayoutSizeFitting(
UIView.layoutFittingCompressedSize
)
}
Ensure that you have constraints from your popover view to the controller's root view. These can be low priority, space >= 0 constraints.
Above answers are correct which states about using the preferredContentSize, but the most important thing is to implement the protocol UIPopoverPresentationControllerDelegate and implement the below method otherwise it will not change the content size as expected.
func adaptivePresentationStyle(for controller: UIPresentationController) -> UIModalPresentationStyle {
return UIModalPresentationStyle.none
}
Similar to Xeieshan's answer above, I set this via the storyboard.
Except that "Presentation" also needed to be to "Form Sheet".
I'm not using storyboards. I just present a UINavigationController in the popover:
self.present(popoverNavigationController!, animated: true) {}
The way to resize the popover size when a new view controller is pushed, it is just change the preferredContentSize before pushing it. For example:
let newViewController = NewViewController()
popoverNavigationController!.preferredContentSize = CGSize(width: 348, height: 400)
popoverNavigationController!.pushViewController(newViewController, animated: true)
The problem is when we try to resize the popover when we pop a view controller.
If you use viewWillDisappear of the current view controller to change the preferredContentSize of the popover, the popover will resize but after the view controller is popped. That means that the animation has a delay.
You have to change the preferredContentSize before executing popViewController. That's mean you have to create a custom back button in the navigation bar like it is explained here. This is the implementation updated for Swift 4:
self.navigationItem.hidesBackButton = true
let newBackButton = UIBarButtonItem(title: "Back", style: .plain, target: self, action: #selector(CurrentViewController.backButtonTapped(sender:)))
self.navigationItem.leftBarButtonItem = newBackButton
And run the next code when the new Back button is pressed:
#objc func backButtonTapped(sender: UIBarButtonItem) {
self.navigationController?.preferredContentSize = CGSize(width: 348, height: 200)
self.navigationController?.popViewController(animated: true)
}
Basically, the preferredContentSize has to be changed before pushing and popping the view controller.
I am trying modify the width of my popover, which is a UITableViewController, so that it only takes up half of the width of the parent view. The popover is called programmatically when a button in another UITableView (the parent view) is tapped. I tried setting the preferredContentSize of the popover and setting the sourceRect but the popover still takes over the entire screen.
class MyTableViewController: UITableViewController, UICollectionViewDataSource, UICollectionViewDelegate, UIDynamicAnimatorDelegate, UIGestureRecognizerDelegate, CLLocationManagerDelegate, UIPopoverPresentationControllerDelegate, UIAdaptivePresentationControllerDelegate {
...
func goToPlaces(button: UIButton) {
let fromRect = CGRectMake(50.0, 50.0, self.view.bounds.width / 2.0, self.view.bounds.height)
let popoverVC = storyboard?.instantiateViewControllerWithIdentifier("otherPlaces")
popoverVC?.modalPresentationStyle = .OverFullScreen
presentViewController(popoverVC!, animated: true, completion: nil)
popoverVC?.view.backgroundColor = UIColor.blackColor().colorWithAlphaComponent(0.6)
popoverVC?.preferredContentSize = CGSizeMake(self.view.bounds.width / 2.0, self.view.bounds.height)
let popoverController = popoverVC?.popoverPresentationController
popoverPresentationController?.sourceView = self.view
popoverPresentationController?.sourceRect = fromRect
popoverController?.permittedArrowDirections = .Any
popoverController?.delegate = self
}
func adaptivePresentationStyleForPresentationController(controller: UIPresentationController) -> UIModalPresentationStyle {
return .None
}
EDIT:
When I do a print of
popoverPresentationController?.sourceView and
popoverPresentationController?.sourceRect
they both return nil for some reason
You are asking for
popoverVC?.modalPresentationStyle = .OverFullScreen
so you get it covering the whole screen. Try using:
popoverVC?.modalPresentationStyle = .Popover
The
presentViewController(popoverVC!, animated: true, completion: nil)
should also be last so that the delegate can get the calls for which it wants to respond. (I think -- it might not matter if UIKit is actually delaying the presentation.)
Try using popoverVC.modalPresentationStyle = UIModalPresentationStyle.PageSheet,
I wish to create a small popover about 50x50px from a UIButton. I have seen methods using adaptive segue's but I have my size classes turn of thus meaning I can not use this features!
How else can I create this popover? Can I create it with code inside my button IBACtion? Or is there still a way I can do this with storyboards?
You can do one of the following two options :
Create an action for the UIButton in your UIViewController and inside present the ViewController you want like a Popover and your UIViewController has to implement the protocol UIPopoverPresentationControllerDelegate, take a look in the following code :
#IBAction func showPopover(sender: AnyObject) {
var popoverContent = self.storyboard?.instantiateViewControllerWithIdentifier("StoryboardIdentifier") as! UIViewController
popoverContent.modalPresentationStyle = .Popover
var popover = popoverContent.popoverPresentationController
if let popover = popoverContent.popoverPresentationController {
let viewForSource = sender as! UIView
popover.sourceView = viewForSource
// the position of the popover where it's showed
popover.sourceRect = viewForSource.bounds
// the size you want to display
popoverContent.preferredContentSize = CGSizeMake(200,500)
popover.delegate = self
}
self.presentViewController(popoverContent, animated: true, completion: nil)
}
func adaptivePresentationStyleForPresentationController(controller: UIPresentationController) -> UIModalPresentationStyle {
return .None
}
According to the book of #matt Programming iOS 8:
A popover presentation controller, in iOS 8, is a presentation controller (UIPresentationController), and presentation controllers are adaptive. This means that, by default, in a horizontally compact environment (i.e. on an iPhone), the .Popover modal presentation style will be treated as .FullScreen. What appears as a popover on the iPad will appear as a fullscreen presented view on the iPhone, completely replacing the interface.
To avoid this behavior in the iPhone you need to implement the delegate method adaptivePresentationStyleForPresentationController inside your UIViewController to display the Popover correctly.
The other way in my opinion is more easy to do, and is using Interface Builder, just arrange from the UIButton to create a segue to the ViewController you want and in the segue select the Popover segue.
I hope this help you.
Swift 4 Here is fully working code. So here you will see popup window with size of 250x250:
import UIKit
class ViewController: UIViewController {
#IBOutlet weak var button: UIButton!
override func viewDidLoad() {
super.viewDidLoad()
// in case if you don't want to make it via IBAction
button.addTarget(self, action: #selector(tapped), for: .touchUpInside)
}
#objc
private func tapped() {
guard let popVC = storyboard?.instantiateViewController(withIdentifier: "popVC") else { return }
popVC.modalPresentationStyle = .popover
let popOverVC = popVC.popoverPresentationController
popOverVC?.delegate = self
popOverVC?.sourceView = self.button
popOverVC?.sourceRect = CGRect(x: self.button.bounds.midX, y: self.button.bounds.minY, width: 0, height: 0)
popVC.preferredContentSize = CGSize(width: 250, height: 250)
self.present(popVC, animated: true)
}
}
// This is we need to make it looks as a popup window on iPhone
extension ViewController: UIPopoverPresentationControllerDelegate {
func adaptivePresentationStyle(for controller: UIPresentationController) -> UIModalPresentationStyle {
return .none
}
}
Take into attention that you have to provide popVC identifier to one viewController you want to present as a popup.
Hope that helps!
Here you can present a popover on button click.
func addCategory( _ sender : UIButton) {
var popoverContent = self.storyboard?.instantiateViewControllerWithIdentifier("NewCategory") as UIViewController
var nav = UINavigationController(rootViewController: popoverContent)
nav.modalPresentationStyle = UIModalPresentationStyle.Popover
var popover = nav.popoverPresentationController
popoverContent.preferredContentSize = CGSizeMake(50,50)
popover.delegate = self
popover.sourceView = sender
popover.sourceRect = sender.bounds
self.presentViewController(nav, animated: true, completion: nil)
}
Swift 4 Version
Doing most work from the storyboard
I added a ViewController, went to it's attribute inspector and ticked the "Use Preferred Explicit size". After that I changed the Width and Height values to 50 each.
Once this was done I ctrl clicked and dragged from the Button to the ViewController I added choosing "Present as Popover" and naming the segue Identifier as "pop"
Went to the ViewController where I had my Button and added the following code:
class FirstViewController: UIViewController, UIPopoverPresentationControllerDelegate {
#IBOutlet weak var popoverButton: UIButton! // the button
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if segue.identifier == "pop" {
let popoverViewController = segue.destination
popoverViewController.modalPresentationStyle = .popover
popoverViewController.presentationController?.delegate = self
popoverViewController.popoverPresentationController?.sourceView = popoverButton
popoverViewController.popoverPresentationController?.sourceRect = CGRect(x: 0, y: 0, width: popoverButton.frame.size.width, height: popoverButton.frame.size.height)
}
}
func adaptivePresentationStyle(for controller: UIPresentationController) -> UIModalPresentationStyle {
return UIModalPresentationStyle.none
}
override func viewDidLoad() {
super.viewDidLoad()
}
}