[Updated to Swift 2.0/iOS9 9/29/15]
There are times your user interface needs to grab attention for a control. This is what modal views and popovers are. Modal views are one of the oldest of the view controllers. Some early applications on the iPhone were completely modal views — some still do. Modals do not do their own housekeeping like navigation controllers or tab controllers, so they are not as efficient for the backbone of your application. Since they need the user attention and suspend everything else in your UI, they are good for settings pages and the like.
Developed originally for the iPad, Popovers are similar to modals. They are a type of view controller that require user attention. Modals take over the window, which on the bigger real estate of the iPad, is often a waste of space. Popovers use limited space and place a smaller “window” on top of the main window.
In this lesson we’ll show several ways to implement popovers and modals.
Set Up the Project
First we’ll need a new project. In Xcode 6, go to File>New>Project… or hit Command-Shift-N. Create a Single-View project called SwiftPizzaPopover, set the language to Swift. Set the device to Universal, then save the project to a place you’d like.
We will be using some photos. You can use your own, here are the two I used:
These are the full-size images. Right-Click and save the images from your browser to some folder or your desktop on your Mac. In Xcode, click on the images.assets, and from the folder you saved the images drag them into the assets work area.
Go into the storyboard, and drag four buttons on the scene. On the two top buttons, remove the title and add the pizza and popover images we just loaded as the button image. On the button below the pizza image button, change the title to Pizza Modal and set the font size to 20. Change the title of the button to Popover but and set the font size to 20. Using whatever method you’d like ( I used some Auto Layout), arrange the buttons to something like this:
Now drag two more view controllers out. Change to the media library by selecting the clip of film in the lower right. In the first view controller, add an UImageView
of the pizza by dragging the pizza image to one of the new controllers. Do the same with the popover image in the other controller. If you use auto layout,pin them to the upper left corner
Click the square-in-circle button to go back to the object library, and drag a label to each of these view controllers. Change the text to read BBQ Chicken Pizza and Cheddar Popover respectively. Make the font for each 20 points. If you use Autolayout, I center aligned them. For the pizza view controller, click the back ground of the controller. Click the color strip in the background color property for the controller, In the color picker that appears, click the sliders button if not already pressed. Change the color to red 255, green 0, and blue 222. Do the same for the popover, except change the color to red 222, green 222, and blue 0.
Unlike Navigation controllers, you need to explicitly dismiss a modal controller. We’ll need a done button. Add a button to the modal view, labeled Done at 22 points font size.Make the background red with a whtie text. Popovers dismiss when anywhere outside the popover gets a touch event. However it might be handy to know how to dismiss a popover, so cut and paste this button to the popover view. I pinned these with auto layout to the bottom of the scene.
Hit Command-n to get the New File dialog. Under iOS click Source> Cocoa Touch Class. Create a new view controller subclassing UIViewController
called PizzaModalVC. Repeat and make a UIViewController
subclass called PopoverVC Assign the subclasses to their respective view controllers using the identity inspector.
When done your two controllers should look like this:
Your story board should look like this:
Using Segues Directly
The easiest way of using segues and modals is to directly connect a segue from the button to the view controller. Get into Story board. From the pizza picture button, control-drag to the PizzaModalVC
view. In the pop up menu, select Present Modally. Click on the circle for the segue and set the identifier to Pizza. Now for the popover, control drag from the popover photo to the PopoverVC
view. Select popover presentation. Select the segue, then set the identifier to Popover.
Select an iPad Air 2 in the simulator. Build and run.
Click on the Popover picture. You will get this:
Tap anywhere but the popover and the popover disappears.
Now tap the pizza picture, and the pizza appears from the bottom. However, we can’t get rid of it.
We did not yet make the dismissal code. Stop the simulator, and go back to the storyboard. Bring up the assistant editor, and select the PizzaModalVC
view. Connect the done button to the view controller code by selecting the done button and then control dragging the button to the view controller. I usually stick my target actions at the top of the class, above viewDidLoad
. Select an action for the connection as a UIButton
. Call this pizzaModalDone Do the same for the popover, calling the method popoverDone.
In the PizzaModalVC
class add the following to pizzaModalDone
:
dismissViewControllerAnimated(true, completion: nil)
In the PopoverVC
class, add the following to popoverDone
:
dismissViewControllerAnimated(true, completion: nil)
Build and run. Now both Done buttons work, and well as clicking in the shaded area outside the popover.
Calling a Segue Programmatically
Go back to the storyboard. In our main view controller add this above viewDidLoad()
:
@IBAction func openPizzaModal(sender: UIButton) { performSegueWithIdentifier("Pizza", sender: self) } @IBAction func openPopover(sender:UIButton){ performSegueWithIdentifier("Popover", sender: self) }
Connect the openPizzaModal(sender:)
to the Pizza Modal button and the openPoppover(sender:)
to the Popover button by dragging from the circle to the correct button. Build and run. Now you can use the bottom text buttons as well as the photos. Line 2 and 5 use the performSegueWithIdentifier()
method. When we set up our segues we gave them identifiers. Once set up, more than the buttons we connected can use the segue, as in this case. When you have two buttons, this is a good way of getting the same behavior out of them. If you want all your buttons to use segues programmatically, control drag from the source view controller icon in the storyboard to the destination view controller.
Programmatically Calling a Modal View Controller
Some times we need to code modal view controllers. Press Command-n to make a new view controller called PizzaModalProgVC subclassing UIViewController
. Add the following properties just after the class declaration.
let dismissButton:UIButton! = UIButton(type:.Custom) let myImage:UIImage! = UIImage(named:"pizza_sm") let myLabel = UILabel()
It is a good practice to keep UI as properties the class can use, unless you are sure you will do nothing with it. Think of it as a strong version of an outlet or action. Anything you will need an outlet or action for should be a property when writing programmatically. Make a view by changing viewDidLoad
to this:
override func viewDidLoad() { super.viewDidLoad() //set our transition style modalTransitionStyle = UIModalTransitionStyle.CrossDissolve // Build a programmatic view view.backgroundColor = UIColor( red: 0.8, green: 0.5, blue: 0.2, alpha: 1.0) if (myImage != nil){ let myImageView = UIImageView(image: myImage) myImageView.frame = view.frame myImageView.frame = CGRectMake(10, 10, 200, 200) view.addSubview(myImageView) }else{ println("image not found") } myLabel.text = "BBQ Chicken Pizza!" myLabel.frame = CGRectMake(220, 10, 300, 50) myLabel.font = UIFont( name: "Helvetica", size: 24) myLabel.textAlignment = .Left view.addSubview(myLabel) dismissButton.setTitle("Done", forState: .Normal) dismissButton.titleLabel!.font = UIFont( name: "Helvetica", size: 24) dismissButton.titleLabel!.textAlignment = .Left dismissButton.frame = CGRectMake(150,175,200,50) dismissButton.addTarget(self, action: "pizzaDidFinish", forControlEvents: .TouchUpInside) view.addSubview(dismissButton) }
I covered most of the view programming in another tutorial. For compactness, I’m leaving out auto layout and setting the position by setting the frame. This is different enough so we can tell the difference between our two Pizza controllers.
All view controllers have a property modalTransitionStyle
which sets the style of the modal transition. To show we are doing a different modal view, I set the transition to FlipHorizontal
instead of its default CoverVertical
.
We still need a target for the done button, so add:
func pizzaDidFinish(){ dismissViewControllerAnimated(true, completion: nil) }
As we did in the segue case, we added a dismissal. View controllers dismiss themselves. Now we have a class to call. Go back to ViewController.swift
and change openPizzaModal
to this:
@IBAction func openPizzaModal(sender: UIButton) { //performSegueWithIdentifier("Pizza", sender: self) presentViewController(PizzaModalProgVC(), animated: true, completion: nil) }
This time we will call presentViewController
and add the view controller we just made PizzaModalProgVC
. We initialize it right here in the method, saving us extra lines of declaration code.
Build and run. Select the Pizza Modal button and you will see our flip transition and a different layout than the segue version.
Programmatically Calling a Popover
The subclass that calls a popover looks pretty much the same as a modal. Select Command-n again. Make the PopoverProgVC
subclass of UIViewController
look like this:
class PopoverProgVC: UIViewController { let dismissButton:UIButton! = UIButton.(type:UIButtonType.Custom) let myImage:UIImage! = UIImage(named:"popover_sm") func pizzaDidFinish(){ dismissViewControllerAnimated(true, completion: nil) } override func viewDidLoad() { super.viewDidLoad() // Build a programmatic view view.backgroundColor = UIColor( red: 0.8, green: 0.5, blue: 0.2, alpha: 1.0) if (myImage != nil){ let myImageView = UIImageView(image: myImage) myImageView.frame = view.frame myImageView.frame = CGRectMake(10, 10, 200, 200) view.addSubview(myImageView) }else{ println("image not found") } let myLabel = UILabel() myLabel.text = "Cheddar Popover" myLabel.frame = CGRectMake(10, 250, 300, 50) myLabel.font = UIFont( name: "Helvetica", size: 24) myLabel.textAlignment = .Left view.addSubview(myLabel) dismissButton.setTitle("Done", forState: .Normal) dismissButton.titleLabel!.font = UIFont(name: "Helvetica", size: 24) dismissButton.titleLabel!.textAlignment = .Left dismissButton.frame = CGRectMake(150,175,200,50) dismissButton.addTarget(self, action: "pizzaDidFinish", forControlEvents: .TouchUpInside) view.addSubview(dismissButton) } }
We call a popover differently though. Change the view controller method as follows:
@IBAction func openPopover(sender:UIButton){ //performSegueWithIdentifier("Popover", sender: self) let vc = PopoverProgVC() vc.modalPresentationStyle = .Popover presentViewController(vc, animated: true, completion: nil) vc.popoverPresentationController?.sourceView = view vc.popoverPresentationController?.sourceRect = sender.frame }
There are two methods to present a popover, one for popovers originating in toolbar buttons and one which presents a popover attached to a CGRect
and displayed in some view. Both start the same. We set a modal presentation style of .Popover
, then present the popover like the modal view we did for the pizza.
The difference is in the next two lines, which must come immediately after the presentation. These two lines of code set up the second kind of popover, one that anchors to a CGRect
and presents itself in the space of a UIView
. When we use a popover to present, we get a popoverPresentationController
on the modal view. This controls the appearance and presentation of the popover. The property sourceView
of code>popoverPresentationController indicates the view that the popover will appear, which often is view
. The property sourceRect
indicates the CGRect
that the popover will be anchored to. In this example I attached the popover to the button that opens it, which is a common occurrence.
Now build and run. Select the popover button, and it works.
Often Popovers appear from bar button items in a toolbar or navigation bar. While we need two properties in other cases, for a bar button item you need only one, appropriately named barButtonItem
. Though we do not use it on our project, an equivalent code for using a bar button item might be this:
@IBAction func openPopover(sender:UIBarButtonItem){ let vc = PopoverProgVC() vc.modalPresentationStyle = .Popover presentViewController(vc, animated: true, completion: nil) vc.popoverPresentationController?.barButtonItem = sender }
Size Classes, Modals, and Popovers
Prior to iOS8, popovers and modals were different. Starting with iOS8, popovers became a type of modal. This was due to size classes. While I go into the details about size classes in my books Swift Swift View Controllers and Practical Autolayout, the basics of size classes is that your layout changes and adapts to different size screens and orientations automatically.
Before iOS8, if you ran a popover on any iPhone, you would get an error. Now iOS just adapts. For most phones in most orientations you get a .Fullscreen
modal presentation. Try it. Select an iPhone 5s in the simulator, and run the app again. Select a popover and you get a modal:
The iPhone 6 plus and 6s plus in landscape will give you a .Formsheet
presentation style. Try it there as well.
Starting with iOS9 on iPads, we also have mutitasking, which will act like an iPhone for popovers. Run the simulator With an iPad air 2. I rotated my screen for better effect by pressing Command-right arrow:
With the simulator running, hit command-Shift-H. to get back to the home screen. Select safari as an app. Drag from the right edge of the screen towards the center. In the icons that appear, pick the Modal demo, then once loaded, click the modal once again.
On the left you will have Safari, on the right our App, though compressed. Try the popover again and you get a .Fullsheet popover.
Drag the white bar to the left of our app, and the bar turns black. Move it to the center. We still get a full Sheet.
Instead of having to figure out any of these cases, iOS does it for you.
We’ve covered the basics of popovers and modals. Modals work anywhere, on iPhone or iPad. Popovers and some special case modals work or appear differently on iPad only. In this lesson we saw how similar popovers are to modals. There is a few more things to do with modals which we will still need to get to. In future posts, we’ll discuss the one popover bug that will definitely get you kicked out of the app store, and how to avoid it.
The Whole Code
// // ViewController.swift // SwiftPizzaPopover // // Created by Steven Lipton on 8/27/14. // Copyright (c) 2014 MakeAppPie.Com. All rights reserved. // import UIKit class ViewController: UIViewController { @IBOutlet weak var openPopover: UIButton! /* if you are paying attention, here is the code for a popover bar button // and how to comply with the single popover rule. var currentPopover:UIPopoverController! = nil @IBAction func showBarPopoverButton(sender: UIBarButtonItem) { if presentedViewController != nil { //how to avoid that // error I mentioned in the conclusion presentedViewController?.dismissViewControllerAnimated(true, completion: nil) } let vc = PopoverProgVC() vc.modalPresentationStyle = .Popover presentViewController(vc, animated: true, completion: nil) vc.popoverPresentationController?.barButtonItem = sender */ @IBAction func openPopover(sender:UIButton){ //performSegueWithIdentifier("Popover", sender: self) let vc = PopoverProgVC() vc.modalPresentationStyle = .Popover presentViewController(vc, animated: true, completion: nil) vc.popoverPresentationController?.sourceView = view vc.popoverPresentationController?.sourceRect = sender.frame } @IBAction func openPizzaModal(sender: UIButton) { //performSegueWithIdentifier("Pizza", sender: self) presentViewController(PizzaModalProgVC(), animated: true, completion: nil) } } // // PizzaModalVC.swift // SwiftPizzaPopover // // Created by Steven Lipton on 8/27/14. // Copyright (c) 2014 MakeAppPie.Com. All rights reserved. // import UIKit class PizzaModalVC: UIViewController { @IBAction func pizzaModalDone(sender: UIButton) { dismissViewControllerAnimated(true, completion: nil) } } // // PopoverVC.swift // SwiftPizzaPopover // // Created by Steven Lipton on 8/27/14. // Copyright (c) 2014 MakeAppPie.Com. All rights reserved. // import UIKit class PopoverVC: UIViewController { @IBAction func popoverDone(sender: UIButton) { dismissViewControllerAnimated(true, completion: nil) } } // // PizzaModalProgVC.swift // SwiftPizzaPopover // // Created by Steven Lipton on 8/28/14. // Copyright (c) 2014 MakeAppPie.Com. All rights reserved. // import UIKit class PizzaModalProgVC: UIViewController { let dismissButton:UIButton! = UIButton(type:.Custom) let myImage:UIImage! = UIImage(named:"pizza_sm") func pizzaDidFinish(){ dismissViewControllerAnimated(true, completion: nil) } override func viewDidLoad() { super.viewDidLoad() //set our transition style modalTransitionStyle = .FlipHorizontal // Build a programmatic view view.backgroundColor = UIColor( red: 0.8, green: 0.5, blue: 0.2, alpha: 1.0) //add the image if myImage != nil{ let myImageView = UIImageView(image: myImage) myImageView.frame = view.frame myImageView.frame = CGRectMake(10, 10, 200, 200) view.addSubview(myImageView) }else{ print("image not found") } //add the label let myLabel = UILabel() myLabel.text = "BBQ Chicken Pizza!" myLabel.frame = CGRectMake(220, 10, 300, 50) myLabel.font = UIFont( name: "Helvetica", size: 24) myLabel.textAlignment = .Left view.addSubview(myLabel) //add the done button dismissButton.setTitle("Done", forState: .Normal) dismissButton.titleLabel.font = UIFont( name: "Helvetica", size: 24) dismissButton.titleLabel.textAlignment = .Left dismissButton.frame = CGRectMake(150,175,200,50) dismissButton.addTarget(self, action: "pizzaDidFinish", forControlEvents: .TouchUpInside) view.addSubview(dismissButton) } } // // PopoverProgVC.swift // SwiftPizzaPopover // // Created by Steven Lipton on 8/28/14. // Copyright (c) 2014 MakeAppPie.Com. All rights reserved. // import UIKit class PopoverProgVC: UIViewController { let dismissButton:UIButton! = UIButton(type:.Custom) let myImage:UIImage! = UIImage(named:"popover_sm") func pizzaDidFinish(){ dismissViewControllerAnimated(true, completion: nil) } override func viewDidLoad() { super.viewDidLoad() // Build a programmatic view //background view.backgroundColor = UIColor( red: 0.8, green: 0.5, blue: 0.2, alpha: 1.0) //image if (myImage != nil){ let myImageView = UIImageView(image: myImage) myImageView.frame = view.frame myImageView.frame = CGRectMake(10, 10, 200, 200) view.addSubview(myImageView) }else{ print("image not found") } // add the label let myLabel = UILabel() myLabel.text = "Cheddar Popover" myLabel.frame = CGRectMake(10, 250, 300, 50) myLabel.font = UIFont(name: "Helvetica", size: 24) myLabel.textAlignment = .Left view.addSubview(myLabel) // add the done button dismissButton.setTitle("Done", forState: .Normal) dismissButton.titleLabel.font = UIFont( name: "Helvetica", size: 24) dismissButton.titleLabel.textAlignment = .Left dismissButton.frame = CGRectMake(150,175,200,50) dismissButton.addTarget(self, action: "pizzaDidFinish", forControlEvents: .TouchUpInside) view.addSubview(dismissButton) } }
Leave a Reply