Creating a popover from a UIButton in Swift - ios

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

Related

How to reload the collectionview after dismiss a present modally view?

I have a tab bar with two tab bar item and I add a central addButton on it which can lead me to the adding items viewcontroller. Here is the setup of the middle button.
func setupMiddleButton() {
let tabBarHeight = tabBar.frame.size.height
let menuButton = UIButton(frame: CGRect(x: 0, y: 0, width: tabBarHeight*1.5, height: tabBarHeight*1.5))
var menuButtonFrame = menuButton.frame
menuButtonFrame.origin.y = view.bounds.height - menuButtonFrame.height/2 - tabBarHeight - 8
menuButtonFrame.origin.x = view.bounds.width/2 - menuButtonFrame.size.width/2
menuButton.frame = menuButtonFrame
menuButton.backgroundColor = UIColor.red
menuButton.layer.cornerRadius = menuButtonFrame.height/2
view.addSubview(menuButton)
let largeConfiguration = UIImage.SymbolConfiguration(scale: .large)
let addIcon = UIImage(systemName: "plus", withConfiguration: largeConfiguration)
menuButton.setImage((addIcon), for: .normal)
menuButton.addTarget(self, action: #selector(menuButtonAction(sender:)), for: .touchUpInside)
view.layoutIfNeeded()
}
#objc private func menuButtonAction(sender: UIButton) {
let addVC = AddViewController()
let navigationController = UINavigationController(rootViewController: addVC)
performSegue(withIdentifier: "addEventSegue", sender: sender)
}
and I did this in one of my tab bar view to reloadData, but I found the collectionView unable to reload after I end editing and dismiss the present modally
override func viewWillAppear(_ animated: Bool) {
lifeCollectionView.delegate = self
lifeCollectionView.dataSource = self
self.fetchData()
DispatchQueue.main.async {
self.lifeCollectionView.reloadData()
}
}
What should I do to reload the collection view after I dismiss that present modally? Thank you so much!
There are multiple ways to achieve that delegate , closure and modalPresentation Style
Way 1:
if you add navigationController.modalPresentationStyle = .fullScreen this will call viewWillAppear of the controller who presents controller and your reload method will get called
Way 2: Delegate & Protocol
Declare a protocol like the one below in Second Controller
protocol CallParent{
func reloadCollection()
}
Declare a property to hold the reference of view controller confirming to this protocol in Second Controller
weak var myParent : CallParent?
Now call reloadCollection before dismiss
if let parent = myParent {
parent.reloadCollection()
}
Confirm first controller with CallParent protocol
class FirstVC: UIViewController,CallParent{
Then while calling segue
#objc private func menuButtonAction(sender: UIButton) {
let addVC = AddViewController()
addVC.myParent = self
let navigationController = UINavigationController(rootViewController: addVC)
performSegue(withIdentifier: "addEventSegue", sender: sender)
}
Way 3 : In your presented controller call dismiss function like this
if let fvc = self.presentingViewController as? FirstController {
self.dismiss(animated: true) {
fvc.callReloadFunctionHere()
}
}
Way 4: Closure
class SecondViewController: UIViewController {
var onViewWillDisappear: (()->())?
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
onViewWillDisappear?()
}
...
}
In FirstController
#objc private func menuButtonAction(sender: UIButton) {
let addVC = AddViewController()
addVC.onViewWillDisappear = {
// reload collection view here
}
let navigationController = UINavigationController(rootViewController: addVC)
performSegue(withIdentifier: "addEventSegue", sender: sender)
}
Set your collection view datasource, delegate and reload only in viewDidLoad. You don't need to use viewWillAppear to reload your collectionView every time!
Use delegates to pass messages.
protocol ContentChangedDelegate: class {
func itemAdded()
}
Within AddViewController add the below variable
weak var delegate: ContentChangedDelegate? = nil
Set your the UIViewController subclass instance where the lifeCollectionView exists as delegate for AddViewController.
let addVC = AddViewController()
addVC.delegate = self
//push or present addVC
Conform your view controller to the ContentChangedDelegate protocol and within the itemAdded method you can reload your collection view.
Call delegate?.itemAdded() after adding your item in AddViewController. This would call the method in your listVC and it should update immediately.

How do I create a ViewController that is only half the height of the screen?

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

how to use popover controller in iPhone

I couldn't show popover controller as popover in iPhone whereas it works very well with iPad.
Any ideas on how to do that in iPhone.
As far as I have searched I couldn't find any.
anyways to make the popover appear in iphone as it is in iPad is appreciated !
Set yourself as the popover view controller's delegate before presenting it, and implement the delegate method adaptivePresentationStyle(for:traitCollection:) to return .none. This will cause the popover to stop adapting on iPhone as a fullscreen presented view controller and turn into an actual popover just like on the iPad.
This is a complete working example that presents the popover in response to a button tap:
class ViewController: UIViewController {
#IBAction func doButton(_ sender: Any) {
let vc = MyPopoverViewController()
vc.preferredContentSize = CGSize(400,500)
vc.modalPresentationStyle = .popover
if let pres = vc.presentationController {
pres.delegate = self
}
self.present(vc, animated: true)
if let pop = vc.popoverPresentationController {
pop.sourceView = (sender as! UIView)
pop.sourceRect = (sender as! UIView).bounds
}
}
}
extension ViewController : UIPopoverPresentationControllerDelegate {
func adaptivePresentationStyle(for controller: UIPresentationController, traitCollection: UITraitCollection) -> UIModalPresentationStyle {
return .none
}
}

Popover on iPhone (Swift 3) from Programmatically Created Button

I am trying to have a popover appear on an iPhone when I click on a programmatically created button. However, I am instead finding that the popover is taking over the entire screen. I've coded the following thus far in the popover view controller's class:
import UIKit
class GroupSetsPopoverController: UIViewController, UIPopoverPresentationControllerDelegate {
override func viewDidLoad() {
super.viewDidLoad()
self.preferredContentSize = CGSize(width: 375, height: 162)
}
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if segue.identifier == "infoButtonSegue" {
let popoverViewController = segue.destination as! GroupSetsPopoverController
let pvc = popoverViewController.popoverPresentationController
pvc?.delegate = self
//popoverViewController.modalPresentationStyle = UIModalPresentationStyle.popover
//popoverViewController.popoverPresentationController!.delegate = self
}
}
func adaptivePresentationStyle(for controller: UIPresentationController, traitCollection: UITraitCollection) -> UIModalPresentationStyle {
return .none
}
}
Any guidance would be appreciated. I've tried to follow various tutorials and posts here on Stack Overflow, to no avail.
You can check out the answer and library for your references. If the GroupSetsPopoverController is your popover viewcontroller, you can do something like this:
init(for sender: UIView)) {
super.init(nibName: nil, bundle: nil)
modalPresentationStyle = .popover
guard let pop = popoverPresentationController else { return }
pop.sourceView = sender
pop.sourceRect = sender.bounds
pop.delegate = self
}

How to change the size of a popover

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.

Resources