Note from Ray: This is a Swift update to a popular Objective-C tutorial on our site. Update to Swift, iOS 9 and Xcode 7.1.1 by Corinne Krych; Original post by Tutorial Team member Matt Galloway. Enjoy!
UIScrollView
is one of the most versatile and useful controls in iOS. It is the basis for the very popular UITableView
and is a great way to present content larger than a single screen. In this UIScrollView
tutorial, by building an app very similar to the Photos app, you’ll learn all about using this control. You’ll learn:
- How to use a
UIScrollView
to zoom and view a very large image. - How to keep the
UIScrollView
‘s content centered while zooming. - How to use
UIScrollView
for vertical scrolling with Auto Layout. - How to keep text input components visible when the keyboard is displayed.
- How to use
UIPageViewController
, in conjunction with UIPageControl, to allow scrolling through multiple pages of content.
This tutorial assumes that you know how to use Interface Builder to add new objects to a view and connect outlets between your code and the Storyboard. You’ll want to be familiar with Storyboards before proceeding, so definitely take a look at our Storyboards tutorial if you’re new to them.
Getting Started
Click here to download the starter project for this UIScrollView
tutorial, and then open it in Xcode.
Build and run to see what you’re starting with:
When selecting a photo, you can see it full sized. But, sadly, the photo is cropped. You can not see the whole image with the limited size of your device. What you really want is for the image to fit the device’s screen by default, and then be able to zoom to see details as you would do in the Photos app.
Can you fix it? Yes you can!
Scrolling and Zooming a Large Image
The first thing you’re going to learn in this UIScrollView
tutorial is how to set up a scroll view that allows the user to zoom into an image and pan around.
First, you need to add a Scroll View. Open Main.storyboard, drag a Scroll View from the Object Library and drop it in Document Outline just under View of Zoomed Photo View Controller Scene. Move Image View inside your newly added Scroll View. Your document outline should now look like this:
See that red dot? Xcode is now complaining that your Auto Layout rules are not properly defined. To fix them first select your Scroll View. Tap the Pin button at the bottom of the storyboard window. Add four new constraints: top, bottom, leading and trailing spaces. Uncheck Constrain to margins and set all the constraint constants to 0.
Now select Image View and add the same four constraints to that view.
Resolve the Auto Layout warning by selecting Zoomed Photo View Controller in Document Outline, and then selecting Editor\Resolve Auto Layout Issues\Update Frames.
Finally, uncheck Adjust Scroll View Insets in Attributes Inspector for Zoomed Photo View Controller.
Build and run.
Thanks to the Scroll View you can now see the full size image by swiping. But what if you want to see the picture scaled to fit the device screen? Or what if you want to zoom in or out of the photo?
Ready to start with some coding?
Open ZoomedPhotoViewController.swift, inside the class declaration, add the following outlet properties:
@IBOutlet weak var scrollView: UIScrollView! @IBOutlet weak var imageViewBottomConstraint: NSLayoutConstraint! @IBOutlet weak var imageViewLeadingConstraint: NSLayoutConstraint! @IBOutlet weak var imageViewTopConstraint: NSLayoutConstraint! @IBOutlet weak var imageViewTrailingConstraint: NSLayoutConstraint! |
Back in Main.storyboard, wire up the Scroll View to the Zoomed View Controller by attaching it to the scrollView
outlet and setting Zoomed View Controller as the Scroll View’s delegate. Also, connect the new constraint outlets from Zoomed View Controller to the appropriate constraints in the Document Outline like this:
Now you’re going to get down and dirty with the code. In ZoomedPhotoViewController.swift, add the implementation of the UIScrollViewDelegate
‘s methods as an extension:
extension ZoomedPhotoViewController: UIScrollViewDelegate { func viewForZoomingInScrollView(scrollView: UIScrollView) -> UIView? { return imageView } } |
This is the heart and soul of the scroll view’s zooming mechanism. You’re telling it which view should be made bigger and smaller when the scroll view is pinched. So, you tell it that it’s your imageView
.
Now, add the implementation of updateMinZoomScaleForSize(_:)
to the ZoomedPhotoViewController
class:
private func updateMinZoomScaleForSize(size: CGSize) { let widthScale = size.width / imageView.bounds.width let heightScale = size.height / imageView.bounds.height let minScale = min(widthScale, heightScale) scrollView.minimumZoomScale = minScale scrollView.zoomScale = minScale } |
You need to work out the minimum zoom scale for the scroll view. A zoom scale of one means that the content is displayed at normal size. A zoom scale below one shows the content zoomed out, while a zoom scale of greater than one shows the content zoomed in. To get the minimum zoom scale, you calculate how far you’d need to zoom out so that the image fits snugly in your scroll view’s bounds based on its width. Then you do the same based upon the image’s height. The minimum of those two resulting zoom scales will be the scroll view’s minimum zoom scale. That gives you a zoom scale where you can see the entire image when fully zoomed out. Note that maximum zoom scale defaults to 1. You leave it as the default because zooming in more than what the image’s resolution can support will cause it to look blurry.
You set the initial zoom scale to be the minimum, so that the image starts fully zoomed out.
Finally, update the minimum zoom scale each time the controller updates it’s subviews:
override func viewDidLayoutSubviews() { super.viewDidLayoutSubviews() updateMinZoomScaleForSize(view.bounds.size) } |
Build and run. You should get the following result:
You can zoom and the image is displayed in portrait mode to fill the whole screen. But there are several glitches:
- The image is pinned at the top of view. It would be nice to have it centered.
- If you turn you phone to landscape orientation, your view doesn’t get resized.
Still in ZoomedPhotoViewController.swift implement the updateConstraintsForSize(size:)
function to fix these issues:
private func updateConstraintsForSize(size: CGSize) { let yOffset = max(0, (size.height - imageView.frame.height) / 2) imageViewTopConstraint.constant = yOffset imageViewBottomConstraint.constant = yOffset let xOffset = max(0, (size.width - imageView.frame.width) / 2) imageViewLeadingConstraint.constant = xOffset imageViewTrailingConstraint.constant = xOffset view.layoutIfNeeded() } |
The method helps to get around a slight annoyance with UIScrollView
: if the scroll view content size is smaller than its bounds, then it sits at the top-left rather than in the center. Since you’ll be allowing the user to zoom all the way out, it would be nice if the image sat in the center of the view. This function accomplishes that by adjusting the layout constraints.
You center the image vertically by subtracting the height of imageView
from the view
‘s height and dividing it in half. This value is used as padding for the top and bottom imageView
constraints.
Likewise, you calculate an offset for the leading and trailing constraints of imageView
.
In the UIScrollViewDelegate
extension, add scrollViewDidZoom(_:)
implementation:
func scrollViewDidZoom(scrollView: UIScrollView) { updateConstraintsForSize(view.bounds.size) } |
Here, the scroll view re-centers the view each time the user scrolls – if you don’t, the scroll view won’t appear to zoom naturally; instead, it will sort of stick to the top-left.
Now take a deep breath, give yourself a pat on the back and build and run your project! Tap on an image and if everything went smoothly, you’ll end up with a lovely image that you can zoom, pan and tap. :]
Scrolling Vertically
Now suppose you want to change PhotoScroll to display the image at the top and add comments below it. Depending on how long the comment is, you may end up with more text than your device can display: Scroll View to the rescue!
Note: In general, Auto Layout considers the top, left, bottom, and right edges of a view to be the visible edges. However, UIScrollView
scrolls its content by changing the origin of it’s bounds. To make this work with Auto Layout, the edges within a scroll view actually refer to the edges of its content view.
To size the scroll view’s frame with Auto Layout, constraints must either be explicit regarding the width and height of the scroll view, or the edges of the scroll view must be tied to views outside of its own subtree.
You can read more in this technical note from Apple.
You’ll see, in practice, how to fix the width of a scroll view, or really its content size width, using auto layout in storyboards.
Scroll view and Auto Layout
Open Main.storyboard and lay out a new scene:
First, add a new View Controller. In Size Inspector, for the Simulated Size, replace Fixed with Freeform and enter a width of 340 and a height of 800. You’ll notice the layout of the controller gets narrower and longer, simulating the behavior of a long vertical content. The simulated size helps you visualize the display in Interface Builder. It has no runtime effect.
Uncheck Adjust Scroll View Insets in the Attributes Inspector for your newly created view controller.
Add a Scroll View that fills the entire space of the view controller. Add leading and trailing constraints with constant values of 0 to the view controller. (Make sure to uncheck Constrain to margin). Add top and bottom constraints from Scroll View to the Top and Bottom Layout guides, respectively. They should also have constants of 0.
Add a View as a child of the Scroll View that takes the entire space of the Scroll View. Rename its storyboard Label to Container View. Like before, add top, bottom, leading and trailing constraints.
To define the size of the scroll view and fix the Auto Layout errors, you need to define its content size. Define the width of Container View to match the view controller’s width. Attach an equal width constraint from the Container View to the View Controller’s main view. For the height of Container View define a height constraint of 500.
Note: Auto Layout rules must comprehensively define a Scroll View’s contentSize
. This is the key step in getting a Scroll View to be correctly sized when using Auto Layout.
Add an Image View inside Container View. In Attributes Inspector: specify photo1 as the image, choose Aspect Fit mode and check Clip Subviews. Add top, leading, and trailing constraints to Container View like before. Add n width constraint of 300 to the image view.
Add a Label inside of Container View below the image view. Specify the label’s text as: What name fits me best? Add a centered horizontal constraint to Container View. Add a vertical spacing constraint of 0 with Photo View.
Add a Text Field inside of Container View, right below the new label. Add leading and trailing constraints to Container View with constant values of 8, and no margin. Add a vertical spacing constraint of 30 with the label.
Finally, connect your newly created View Controller to another screen via a segue. Remove the existing push segue between the Photo Scroll scene and the Zoomed Photo View Controller scene. Don’t worry, all the work you’ve done on Zoomed Photo View Controller will be added back to your app later.
In the Photo Scroll scene, from PhotoCell, control-drag to View Controller, add a show segue. Make the identifier showPhotoPage.
Build and Run.
You can see that layout is correct in vertical orientation. Try rotating to landscape orientation. In landscape, there is not enough vertical room to show all the content, yet the scroll view allows you to properly scroll to see the label and the text field. Unfortunately, since the image in the new view controller is hard-coded, the image you selected in the collection view is not shown.
To fix this, you’ll need to pass it along to the view controller when the segue is executed. So, create a new file with the iOS\Source\Cocoa Touch Class template. Name the class PhotoCommentViewController and set the subclass to UIViewController. Make sure that the language is set to Swift. Click Next and save it with the rest of the project.
Update PhotoCommentViewController.swift with this code:
import UIKit public class PhotoCommentViewController: UIViewController { @IBOutlet weak var imageView: UIImageView! @IBOutlet weak var scrollView: UIScrollView! @IBOutlet weak var nameTextField: UITextField! public var photoName: String! override public func viewDidLoad() { super.viewDidLoad() if let photoName = photoName { self.imageView.image = UIImage(named: photoName) } } } |
This updated implementation of PhotoCommentViewController
adds IBOutlet
s, and sets the image of imageView
based on a photoName
.
Back in the storyboard, open the Identity Inspector for View Controller, select PhotoCommentViewController for the Class. Open the Connections Inspector and wire up the IBOutlets for the Scroll View, Image View and Text Field of PhotoCommentViewController
.
Open CollectionViewController.swift, replace prepareForSegue(_:sender:)
with:
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) { if let cell = sender as? UICollectionViewCell, indexPath = collectionView?.indexPathForCell(cell), photoCommentViewController = segue.destinationViewController as? PhotoCommentViewController { photoCommentViewController.photoName = "photo\(indexPath.row + 1)" } } |
This sets the name of the photo to be shown on PhotoCommentViewController
when one of the photos is tapped.
Build and run.
Your view nicely displays the content and when needed allows you to scroll down to see more. You’ll notice two issues with the keyboard: first, when entering text, the keyboard hides the Text Field. Second, there is no way to dismiss the keyboard. Ready to fix the glitches?
Keyboard
Keyboard offset
Unlike when using UITableViewController
where it manages moving content out of the way of the on-screen keyboard, when working with UIScrollView
you have to deal with handling the keyboard appearance by yourself.
View controllers can make adjustments to their contents when the keyboard appears by listening for NSNotifications
issued by iOS. The notifications contain a dictionary of geometry and animation parameters that can be used to smoothly animate the contents out of the way of the keyboard. You’ll first update your code to listen for those notifications. Open PhotoCommmentViewController.swift, and add the following code at the bottom of viewDidLoad()
:
NSNotificationCenter.defaultCenter().addObserver( self, selector: "keyboardWillShow:", name: UIKeyboardWillShowNotification, object: nil ) NSNotificationCenter.defaultCenter().addObserver( self, selector: "keyboardWillHide:", name: UIKeyboardWillHideNotification, object: nil ) |
When the view loads, you will begin listening for notifications that the keyboard is appearing and disappearing.
Next, add the following code, to stop listening for notifications when the object’s life ends:
deinit { NSNotificationCenter.defaultCenter().removeObserver(self) } |
Next add the following methods to the view controller:
func adjustInsetForKeyboardShow(show: Bool, notification: NSNotification) { let userInfo = notification.userInfo ?? [:] let keyboardFrame = (userInfo[UIKeyboardFrameBeginUserInfoKey] as! NSValue).CGRectValue() let adjustmentHeight = (CGRectGetHeight(keyboardFrame) + 20) * (show ? 1 : -1) scrollView.contentInset.bottom += adjustmentHeight scrollView.scrollIndicatorInsets.bottom += adjustmentHeight } func keyboardWillShow(notification: NSNotification) { adjustInsetForKeyboardShow(true, notification: notification) } func keyboardWillHide(notification: NSNotification) { adjustInsetForKeyboardShow(false, notification: notification) } |
adjustInsetForKeyboardShow(_:,notification:)
takes the keyboard’s height as delivered in the notification, adds a padding value of 20 to either be subtracted from or added to the scroll views’s contentInset
. This way, the UIScrollView
will scroll up or down to let the UITextField
always be visible on the screen.
When the notification is fired, either keyboardWillShow(_:)
or keyboardWillHide(_:)
will be called. These methods will then call adjustInsetForKeyboardShow(_:,notification:)
, indicating which direction to move the scroll view.
Dismissing the Keyboard
To dismiss the keyboard, add this method to PhotoCommentViewController.swift
:
@IBAction func hideKeyboard(sender: AnyObject) { nameTextField.endEditing(true) } |
This method will resign the first responder status of the text field which will, in turn, close the keyboard.
Finally, open Main.storyboard. From Object Library drag a Tap Gesture Recognizer into the root view. Then, wire it to the hideKeyboard(_:) IBAction
in Photo Comment View Controller.
Build and run.
Tap the text field, and then tap somewhere else on the screen. The keyboard should properly show and hide itself relative to the other content on the screen.
Paging with UIPageViewController
In the third section of this UIScrollView
tutorial, you’ll be creating a scroll view that allows paging. This means that the scroll view locks onto a page when you stop dragging. You’ll see this in action in the App Store app when you view screenshots of an app.
Add UIPageViewController
Go to Main.storyboard, drag a Page View Controller from the Object Library. Open Identity Inspector, enter PageViewController for the Storyboard ID. In Attributes Inspector, the Transition Style is set to Page Curl by default; change it to Scroll and set the Page Spacing to 8.
In the Photo Comment View Controller scene’s Identity Inspector, specify a Storyboard ID of PhotoCommentViewController, so that you can refer to it from code.
Open PhotoCommentViewController.swift and add:
public var photoIndex: Int! |
This will reference the index of the photo to show and will be used by the page view controller.
Create a new file with the iOS\Source\Cocoa Touch Class template. Name the class ManagePageViewController and set the subclass to UIPageViewController . Make sure that the language is set to Swift. Click Next and save it with the rest of the project.
Open ManagePageViewController.swift and replace the contents of the file with the following:
import UIKit class ManagePageViewController: UIPageViewController { var photos = ["photo1", "photo2", "photo3", "photo4", "photo5"] var currentIndex: Int! override func viewDidLoad() { super.viewDidLoad() dataSource = self // 1 if let viewController = viewPhotoCommentController(currentIndex ?? 0) { let viewControllers = [viewController] // 2 setViewControllers( viewControllers, direction: .Forward, animated: false, completion: nil ) } } func viewPhotoCommentController(index: Int) -> PhotoCommentViewController? { if let storyboard = storyboard, page = storyboard.instantiateViewControllerWithIdentifier("PhotoCommentViewController") as? PhotoCommentViewController { page.photoName = photos[index] page.photoIndex = index return page } return nil } } |
Here’s what this code does:
viewPhotoCommentController(_:_)
creates an instance ofPhotoCommentViewController
though the Storyboard. You pass the name of the image as a parameter so that the view displayed matches the image you selected in previous screen.- You setup the
UIPageViewController
by passing it an array that contains the single view controller you just created.
You’ll notice that you have an Xcode error indicating that delegate
cannot be assigned a value of self
. This is because ManagePageViewController
does not yet conform to UIPageViewControllerDataSource
. Add the following in ManagePageViewController.swift but outside of the ManagePageViewController
definition:
//MARK: implementation of UIPageViewControllerDataSource extension ManagePageViewController: UIPageViewControllerDataSource { // 1 func pageViewController(pageViewController: UIPageViewController, viewControllerBeforeViewController viewController: UIViewController) -> UIViewController? { if let viewController = viewController as? PhotoCommentViewController { var index = viewController.photoIndex guard index != NSNotFound && index != 0 else { return nil } index = index - 1 return viewPhotoCommentController(index) } return nil } // 2 func pageViewController(pageViewController: UIPageViewController, viewControllerAfterViewController viewController: UIViewController) -> UIViewController? { if let viewController = viewController as? PhotoCommentViewController { var index = viewController.photoIndex guard index != NSNotFound else { return nil } index = index + 1 guard index != photos.count else {return nil} return viewPhotoCommentController(index) } return nil } } |
The UIPageViewControllerDataSource
allows for you to provide content when the page changes. You provide view controller instances for paging in both the forward and backward directions. In both cases, photoIndex
is used to determine which image is currently being displayed. (The viewController
parameter to both methods indicates the currently displayed view controller.) Based on the photoIndex
a new controller is created and returned for either method.
There are only a couple things left to do to get your page view running. First, you will fix the application flow of the app. Switch back to Main.storyboard and select your newly created
Page View Controller scene. Then, in the Identity Inspector, specify ManagePageViewController for its class. Delete the push segue showPhotoPage you created earlier. Control drag from Photo Cell in Scroll View Controller to Manage Page View Controller Scene and select a Show segue. In the Attributes Inspector for the segue, specify its name as showPhotoPage.
Open CollectionViewController.swift and change the implementation of prepareForSegue(_:sender:)
to the following:
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) { if let cell = sender as? UICollectionViewCell, indexPath = collectionView?.indexPathForCell(cell), managePageViewController = segue.destinationViewController as? ManagePageViewController { managePageViewController.photos = photos managePageViewController.currentIndex = indexPath.row } } |
Build and run.
You can now scroll side to side to page between different detail views. :]
Display Page Control indicator
For the final part of this UIScrollView
tutorial, you will add a UIPageControl
to your application.
UIPageViewController
has the ability to automatically provide a UIPageControl
. To do so, your UIPageViewController
must have a transition style of UIPageViewControllerTransitionStyleScroll
, and you must provide implementations of two special methods of UIPageViewControllerDataSource
. (If you remember, you already set the Transition Style to Scroll in the Storyboard). Add these methods to the UIPageViewControllerDataSource
extension in ManagePageViewController.swift:
// MARK: UIPageControl func presentationCountForPageViewController(pageViewController: UIPageViewController) -> Int { return photos.count } func presentationIndexForPageViewController(pageViewController: UIPageViewController) -> Int { return currentIndex ?? 0 } |
In the first method, you specify the number of pages to display in the page view controller. And, in the second method, you tell page view controller which page should initially be selected.
After you’ve implemented the required delegate methods, you can add further customization with the UIAppearance
API. In AppDelegate.swift, replace application(application: didFinishLaunchingWithOptions:)
with:
func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool { let pageControl = UIPageControl.appearance() pageControl.pageIndicatorTintColor = UIColor.lightGrayColor() pageControl.currentPageIndicatorTintColor = UIColor.redColor() return true } |
This will customize the colors of the UIPageControl
.
Build and run.
Putting it all together
Almost there! The very last step is to add back the zooming view when tapping an image. Open PhotoCommentViewController.swift and add the following:
@IBAction func openZoomingController(sender: AnyObject) { self.performSegueWithIdentifier("zooming", sender: nil) } override public func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) { if let id = segue.identifier, zoomedPhotoViewController = segue.destinationViewController as? ZoomedPhotoViewController { if id == "zooming" { zoomedPhotoViewController.photoName = photoName } } } |
In Main.storyboard, add a Show Detail segue from Photo Comment View Controller to Zoomed Photo View Controller. With the new segue selected, open the Identity Inspector, set the Identifier to zooming.
Select the Image View in Photo Comment View Controller, open the Attributes Inspector and check User Interaction Enabled. Add a Tap Gesture Recognizer, and connect it to openZoomingController(_:)
.
Now, when you tap an image in Photo Comment View Controller Scene, you’ll be taken to the Zoomed Photo View Controller Scene where you can zoom the photo.
Build and run one more time.
Yes you did it! You’ve created a Photos app clone: a collection view of images you can select and navigate through by swiping, as well as the ability to zoom the photo content.
Where to Go From Here?
Here is the final PhotoScroll project with all of the code from this UIScrollView
tutorial.
You’ve delved into many of the interesting things that a scroll view is capable of. If you want to go further, there is an entire 21-part video series dedicated to scroll views. Take a look.
Now go make some awesome apps, safe in the knowledge that you’ve got mad scroll view skills!
If you run into any problems along the way or want to leave feedback about what you’ve read here, join in the discussion in the comments below.
The post UIScrollView Tutorial: Getting Started appeared first on Ray Wenderlich.