Modal views are one of the fundamental ways to present 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. They need the user’s attention and suspend everything else in your UI, making them 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 requires 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 view on top of the main window.
Apple in iOS8 merged popovers and alerts into modals to create the current adaptive layout system. Instead of the developer having to write different code for different devices, adaptive layout does the heavy lifting and the developer writes code once. In this lesson we’ll show several ways to add popovers and modals to different devices.
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 Swift3PizzaPopover, 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 Assets.xcassets, and from the folder you saved the images drag them into the assets work area.
Go into the storyboard. In the lower left hand side of the storyboard, you will find the Class Size Preview Button with the words View as: iPhone6s(wC hR)
Click this button to show the preview bar.
We’ll skip auto layout in this lesson and use some drag and drop layout. To make sure our controls show up on all devices we’ll lay out for the smallest device. Select the iPhone 4s device, leaving the orientation in portrait.
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, and in the drop down change to RGB Color.
Change the view’s background color to Hex Color #FCE8E5. 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 26 points. Change the title of the button to Popover and set the font size to 26 points. Arrange the buttons to something like this:
Now drag two more view controllers out. Change each to have different background colors. I used a background color of #D337C8 for one and the other #FDDB28
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.
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.
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 26 points font size. Make the background Red(#FF0000) with White(#FFFFFF) text. Popovers dismiss when anywhere outside the popover gets a touch event. However we’ll find that popovers aren’t always popovers, so cut and paste this button to the popover view.
When done, your two controllers should look like this:
Hit Command-N to get a New File. Under iOS click Source> Cocoa Touch Class. Create a new view controller subclassing UIViewController
called PizzaModalViewController. Repeat and make a UIViewController
subclass called PopoverViewController Assign the subclasses to their respective view controllers using the identity inspector. Your finished 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. In the storyboard, control-drag from the pizza picture button to the PizzaModalVC
view. In the menu that appears, 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 Present as Popover.
Select the segue, then set the identifier to Popover in the attributes inspector.
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. 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. 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
:
@IBAction func pizzaModalDone(_ sender: UIButton) { dismiss(animated: true, completion: nil) }
In the PopoverVC
class, add the following to popoverDone
:
@IBAction func popoverDone(_ sender: UIButton) { dismiss(animated: true, completion: nil) }
We dismiss any modal view controller with dismiss
. It is a method of the modal view controller, hence we are calling it from within the class. It has two arguments. The first indicates if an animation should accompany dismissal. This is the usual way to dismiss, However for speed when dismissing a modal view behind another view, you can set this to false
. The second argument is a closure if you need to do anything after successfully dismissing the view controller. Generally I leave it nil.
Build and run. Now both Done buttons work, and well as clicking in the shaded area outside the popover.
Presenting a Segue Programmatically
Go back to the storyboard. In our main view controller add this above viewDidLoad()
:
@IBAction func openPizzaModal(_ sender: UIButton) { performSegue(withIdentifier:"Pizza", sender: self) } @IBAction func openPopover(_ sender:UIButton){ performSegue(withIdentifier:"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 performSegue(withIdentifier:)
method. When we set up our segues we gave them identifiers. We’ve used that identifer to call the segue. 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 Presenting a Modal View Controller from the Storyboard
When using a storyboard, we can use the storyboard identifier to do that. Go to the story board and select the pizza view controller. In the Identity inspector change the Storyboard ID to Pizza
We’ll use this identifier to call our view controller from the storyboard. Add another button Pizza Prog with 26 point type to the root controller next to the Pizza Modal Button
Open the assistant editor and control drag the new button tot he code. Make another action openPizzaProgModal in the ViewController
class.
In ViewController.swift
and change openPizzaProgModal
to this:
@IBAction func openPizzaProgModal(_ sender: UIButton) { let vc = ( storyboard?.instantiateViewController( withIdentifier: "Pizza") )! vc.view.backgroundColor = UIColor.orange() vc.modalTransitionStyle = .crossDissolve present(vc, animated: true, completion: nil) }
Line two gets the view controller from the storyboard using the Storyboard Identifier we set up. If these two do not not match exactly you will get a fatal error.
Line 8 is the core of this. The present
method presents the modal view, with animation and no completion handler. It’s sort of the bracket to the dismiss
method, with almost the same arguments. There is one difference. In dismiss
, the object dismissed calls dismiss
. In present
, the current view controller calls present
, and the first argument is the view controller to be presented. Pretty much all presentation of view controllers look exactly like this.
When using present
, there is no prepare(for segue:)
equivalent, which you would have with performSegue
or running from a storyboard segue. Once we have the view controller, we change its properties directly, such as changing the background
to orange in line 6. We can also change the transition to a cross dissolve, using the property modaltransitionStyle
.
Build and run. Select the Pizza Modal button and you will see our flip transition and a different layout than the segue version.
Programmatically Presenting a Modal View
The present
method can be used with any view controller. If you made your own programmatic one, you need only an instance of the view controller to present it.
Press Command-N and add a new Cocoa Touch Class named PizzaModalProgViewController subclassing UIViewController
.
I won’t go into details about this code, but suffice it to say it creates a view controller with a blue Done button, a label, and an image.
class PizzaModalProgViewController: UIViewController { let dismissButton:UIButton! = UIButton(type:.custom) var imageName = "pizza_sm" var text = "BBQ Chicken Pizza!!!!" let myLabel = UILabel() func pizzaDidFinish(){ dismiss(animated: true, completion: nil) } override func viewDidLoad() { super.viewDidLoad() // Build a programmatic view //Set the Background color of the view. view.backgroundColor = UIColor( red: 0.8, green: 0.6, blue: 0.3, alpha: 1.0) // Add the image to the view let myImage:UIImage! = UIImage(named: imageName) let myImageView = UIImageView(image: myImage) myImageView.frame = view.frame myImageView.frame = CGRect( x: 10, y: 10, width: 200, height: 200) view.addSubview(myImageView) // Add the label to the view myLabel.text = text myLabel.frame = CGRect( x: 220, y: 10, width: 300, height: 150) myLabel.font = UIFont( name: "Helvetica", size: 24) myLabel.textAlignment = .left view.addSubview(myLabel) //Add the Done button to the view. let normal = UIControlState(rawValue: 0) //.normal doesn't exist in beta. workaround dismissButton.setTitle("Done", for: normal) dismissButton.setTitleColor(UIColor.white, for: normal) dismissButton.backgroundColor = UIColor.blue dismissButton.titleLabel!.font = UIFont( name: "Helvetica", size: 24) dismissButton.titleLabel?.textAlignment = .left dismissButton.frame = CGRect( x:10, y:275, width:400, height:50) dismissButton.addTarget(self,action: #selector(self.pizzaDidFinish),for: .touchUpInside) view.addSubview(dismissButton) } }
Go back to ViewController.swift
and the openPizzaModal
action. Comment out the code for the storyboard and add a few new lines of code above it.
@IBAction func openPizzaProgModal(_ sender: UIButton) { //using a view controller let vc = PizzaModalProgViewController() vc.modalTransitionStyle = .partialCurl present(vc, animated: true, completion: nil) /* //using a story board let vc = (storyboard?.instantiateViewController( withIdentifier: "Pizza"))! vc.view.backgroundColor = UIColor.orange vc.modalTransitionStyle = .crossDissolve present(vc, animated: true, completion: nil) */ }
Since the present
method uses a view controller, line 3 gets an instance of one. Line 4 changes the transition style again, this time to a page curl transition. Line 5 presents the controller, looking identical to line 12 when we presented a view controller from the storyboard..
Build and run. Click the Pizza Prog button to get a different modal than before.
Programmatically Presenting a Popover
How to present a popover is similar to calling a modal programmatically. There are a few differences however. Change the view controller method openPopover
as follows, commenting out the previous code:
@IBAction func openPopover(_ sender:UIButton){ //performSegue(withIdentifier:"Popover", sender: self) let vc = PizzaModalProgViewController() //change properties of the view controller vc.text = "Cheesy!!" vc.imageName = "popover_sm" vc.view.backgroundColor = UIColor.yellow //present from a view and rect vc.modalPresentationStyle = .popover //presentation style present(vc, animated: true, completion: nil) vc.popoverPresentationController?.sourceView = view vc.popoverPresentationController?.sourceRect = sender.frame }
Line three will give us the same view controller as the last example. As discussed earlier, lines 5 through 7 set properties of the view controller, this time changing properties to use a different image and text than the default values of the class. The code sets a yellow background as well.
Line 9 tells the system to present this modal as a popover. There are two ways 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 between popovers on a CGRect
and on a bar button is in the next two lines, which must come immediately after the presentation. These two lines of code set up the popover 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 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.
Popovers often appear from bar button items in a toolbar or navigation bar. While we need two properties for a popover off of a CGRect
, 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 }
You can see an example of bar button items in the lesson about UIImagePickerController
. They look like this:
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 6s in the simulator, and run the app again. Select a popover and you get a modal in portrait and landscape:
The iPhone 6 plus and 6s plus in landscape will give you a .formsheet
presentation style for a popover, but .fullscreen
in Portrait. Try iPhone 6s plus in the simulator.
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 Swift3PizzaModal app, 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 .fullscreen
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 .fullScreen.
Instead of having to figure out any of these cases, the adaptive layout features of iOS does it for you. For popovers, views with a compact width get a .fullsheet
modal view and ones with a regular width get a popover or .formsheet
.
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
// // ViewController.swift // Swift3PizzaPopover // // Created by Steven Lipton on 6/29/16. // Copyright © 2016 Steven Lipton. All rights reserved. // // Demo of various ways or presenting modal view controllers import UIKit class ViewController: UIViewController { // //Programmatic ways of presenting modals //the goal is to get a view controller and // present it with present(vc, animated: true, completion: nil) // // for a storyboard identifier, we use // let vc = (storyboard?.instantiateViewController( withIdentifier: "Pizza"))! //for a view controller with programmatic layout we use // let vc = init() // @IBAction func openPizzaProgModal(_ sender: UIButton) { /* //using a story board let vc = (storyboard?.instantiateViewController( withIdentifier: "Pizza"))! vc.view.backgroundColor = UIColor.orange vc.modalTransitionStyle = .crossDissolve present(vc, animated: true, completion: nil) */ //using a view controller let vc = PizzaModalProgViewController() vc.modalTransitionStyle = .partialCurl present(vc, animated: true, completion: nil) } // // To present a modal from its segue identifier use this // performSegue(withIdentifier:String, sender: self) @IBAction func openPizzaModal(_ sender: UIButton) { performSegue(withIdentifier: "Pizza", sender: self) } // // Presenting popovers // Popovers are a special presentation style of a modal view controller // which present a small window on iPads, and a full screen modal on other devices // From a segue, you present it the same way as any other modal // From a view controller, you need two more components // // 1) set the modalPresentationStyle property to .popover // vc.modalPresentationStyle = .popover // 2) Tell the popover where to present. This code goes immediately after the // present() call. If you are anchoring to a view, you need two lines of code // vc.popoverPresentationController?.sourceView = viewToPresentIn // vc.popoverPresentationController?.sourceRect = aCGRectToAnchorTo // //If anchoring to a UIBarButtonItem, you need only one line of code // vc.popoverPresentationController?.barButtonItem = aBarButtonItem // @IBAction func openPopover(_ sender:UIButton){ /* //Present a popover from a segue performSegue(withIdentifier:"Popover", sender: self) */ //Present a popover programmatically let vc = PizzaModalProgViewController() vc.modalPresentationStyle = .popover //change properties of the view controller vc.text = "Cheesy!!" vc.imageName = "popover_sm" vc.view.backgroundColor = UIColor.yellow //present from a view and rect present(vc, animated: true, completion: nil) vc.popoverPresentationController?.sourceView = view vc.popoverPresentationController?.sourceRect = sender.frame /* //Code to present popover from a bar button Item, //though we don't have one in this example. present(vc, animated: true, completion: nil) vc.popoverPresentationController?.barButtonItem = sender */ } override func viewDidLoad() { super.viewDidLoad() // Do any additional setup after loading the view, typically from a nib. } }
PizzaModalViewController.swift
// // PizzaModalViewController.swift // Swift3PizzaPopover // // Created by Steven Lipton on 6/29/16. // Copyright © 2016 Steven Lipton. All rights reserved. // import UIKit class PizzaModalViewController: UIViewController { // // Dismissal code for any modal // @IBAction func pizzaModalDone(_ sender: UIButton) { dismiss(animated: true, completion: nil) } }
PopoverViewController.swift
// // PopoverViewController.swift // Swift3PizzaPopover // // Created by Steven Lipton on 6/29/16. // Copyright © 2016 Steven Lipton. All rights reserved. // import UIKit class PopoverViewController: UIViewController { // // Dismissal code for any modal // // Note on popovers this is not necessary, since any touch outside the popover // dismisses the popover. However, if presented as a full sheet on a compact width device, // this is necessary. For code to hide the done button based on traits, // see my book Swift Swift View Controllers @IBAction func popoverDone(_ sender: UIButton) { dismiss(animated: true, completion: nil) } }
PizzaModalProgViewController.swift
// // PizzaModalProgViewController.swift // Swift3PizzaPopover // // Created by Steven Lipton on 6/29/16. // Copyright © 2016 Steven Lipton. All rights reserved. // // Code to programmatically create a View Controller. // Explanation of this code is beyond the scope of this lesson // // import UIKit class PizzaModalProgViewController: UIViewController { let dismissButton:UIButton! = UIButton(type:.custom) var imageName = "pizza_sm" var text = "BBQ Chicken Pizza!!!!" let myLabel = UILabel() func pizzaDidFinish(){ dismiss(animated: true, completion: nil) } override func viewDidLoad() { super.viewDidLoad() // Build a programmatic view //Set the Background color of the view. view.backgroundColor = UIColor( red: 0.8, green: 0.6, blue: 0.3, alpha: 1.0) // Add the image to the view let myImage:UIImage! = UIImage(named: imageName) let myImageView = UIImageView(image: myImage) myImageView.frame = view.frame myImageView.frame = CGRect( x: 10, y: 10, width: 200, height: 200) view.addSubview(myImageView) // Add the label to the view myLabel.text = text myLabel.frame = CGRect( x: 220, y: 10, width: 300, height: 150) myLabel.font = UIFont( name: "Helvetica", size: 24) myLabel.textAlignment = .left view.addSubview(myLabel) //Add the Done button to the view. let normal = UIControlState(rawValue: 0) //UIControlState.normal doesn't exist in beta(#27105189). This is a workaround. dismissButton.setTitle("Done", for: normal) dismissButton.setTitleColor(UIColor.white, for: normal) dismissButton.backgroundColor = UIColor.blue dismissButton.titleLabel!.font = UIFont( name: "Helvetica", size: 24) dismissButton.titleLabel?.textAlignment = .left dismissButton.frame = CGRect( x:10, y:275, width:400, height:50) dismissButton.addTarget(self,action: #selector(self.pizzaDidFinish),for: .touchUpInside) view.addSubview(dismissButton) } }
Leave a Reply