UIScrollView Tutorial: Getting Started

Owen Brown
uiscrollview tutorial

Learn how to use UIScrollViews for paging, zooming, scrolling, and more!

Update Note: This tutorial has been updated to Xcode 9.0 and Swift 4 by Owen Brown. The original tutorial was written by Ray Wenderlich.

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, you’ll create an app that’s very similar to the Photos app and learn all about UIScrollView. 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 to allow scrolling through multiple pages of content.

This tutorial assumes you understand how to use Interface Builder to add objects and connect outlets between your code and storyboard scenes. If you’re not familiar with Interface Builder or Storyboards, work through our Storyboards tutorial before this one.

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:

uiscrollview tutorial

You can select a photo to see it full sized, but sadly, you can’t see the whole image due to the limited size of the device. What you really want is to fit the image to the device’s screen by default, and zoom to see details just like the Photos app.

Can you fix it? Yes you can!

Scrolling and Zooming a Large Image

To kick off this UIScrollView tutorial, you’ll set up a scroll view that lets the user pan and zoom an image.

Open Main.storyboard, and drag a Scroll View from the Object Library onto the Document Outline right below View on the Zoomed Photo View Controller scene. Then, move Image View inside your newly-added Scroll View. Your Document Outline should now look like this:

uiscrollview tutorial

See that red dot? Xcode is complaining that your Auto Layout rules are wrong.

To fix them, select Scroll View and tap the pin button at the bottom of the storyboard window. Add four new constraints: top, bottom, leading and trailing. Set each constraint’s constant to 0, and uncheck Constrain to Margins. This should look like this:

uiscrollview tutorial

Now select Image View and add the same four constraints on it too.

If you get an Auto Layout warning afterwards, select Zoomed Photo View Controller in the Document Outline and then select Editor\Resolve Auto Layout Issues\Update Frames. If you don’t get a warning, Xcode likely updated the frames automatically for you, so you don’t need to do anything.

Build and run.

uiscrollview tutorial

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 and out?

You’ll need to write code for these!

Open ZoomedPhotoViewController.swift, and add the following outlets inside the class declaration:

@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, set the scrollView outlet to the Scroll View, and set the Scroll View's delegate to Zoomed View Controller. Also, connect the new constraint outlets the appropriate constraints in the Document Outline like this:

uiscrollview tutorial

Back in ZoomedPhotoViewController.swift, add the following to the end of the file:

extension ZoomedPhotoViewController: UIScrollViewDelegate {
  func viewForZooming(in scrollView: UIScrollView) -> UIView? {
    return imageView
  }
}

This makes ZoomedPhotoViewController conform to UIScrollViewDelegate and implement viewForZooming(in:). The scroll view calls this method to get which of its subviews to scale whenever its pinched, and here you tell it to scale imageView.

Next, add the following inside the class right after viewDidLoad():

fileprivate 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
}

This method calculates the zoom scale for the scroll view. A zoom scale of one indicates that the content is displayed at normal size. A zoom scale less than one shows the content zoomed out, and a zoom scale greater than one shows the content zoomed in.

To get the minimum zoom scale, you first calculate the required zoom to fit the image view snugly within the scroll view based on its width. You then calculate the same for the height. You take the minimum of the width and height zoom scales, and set this for both minimumZoomScale and zoomScale on the scroll view. Thereby, you’ll initially see the entire image fully zoomed out, and you’ll be able to zoom out to this level too.

Since the maximumZoomScale defaults to 1, you don’t need to set it. If you set it to greater than 1, the image may appear blurry when fully zoomed in. If you set it to less than 1, you wouldn’t be able to zoom in to the full image’s resolution.

Finally, you also need to update the minimum zoom scale each time the controller updates its subviews. Add the following right before the previous method to do this:

override func viewWillLayoutSubviews() {
  super.viewWillLayoutSubviews()
  updateMinZoomScaleForSize(view.bounds.size)
}

Build and run, and you should get the following result:

uiscrollview tutorial

You can now pan and zoom, and the image initially fits on the screen. Awesome!

However, there’s still one problem: the image is pinned to the top of the scroll view. It’d sure be nice to have it centered instead, right?

Still in ZoomedPhotoViewController.swift, add the following inside the class extension right after viewForZooming(in:)

func scrollViewDidZoom(_ scrollView: UIScrollView) {
  updateConstraintsForSize(view.bounds.size)
}

fileprivate 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 scroll view calls scrollViewDidZoom each time the user scrolls. In response, you simply call updateConstraintsForSize(_:) and pass in the view’s bounds size.

updateConstraintsForSize(_:) gets around an annoyance with UIScrollView: if the scroll view’s content size is smaller than its bounds, the contents are placed at the top-left rather than the center.

You get around this by adjusting the layout constraints for the image view. You first 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. Similarly, you calculate an offset for the leading and trailing constraints of imageView based on the width.

Give yourself a pat on the back, and build and run your project! Select an image, and if everything went smoothly, you’ll end up with a lovely image that you can zoom and pan. :]

uiscrollview tutorial

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 its 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 next learn how to fix the width of a scroll view, which is really its content size width, using Auto Layout.

Scroll View and Auto Layout

Open Main.storyboard and lay out a new scene:

First, add a new View Controller. In the Size Inspector replace Fixed with Freeform for the Simulated Size, and enter a width of 340 and a height of 800.

uiscrollview tutorial

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.

uiscrollview tutorial

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, and 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, and resize it to fit the entire space of the Scroll View.

Rename its storyboard Label to Container View. Like before, add top, bottom, leading and trailing constraints, with constants of 0 and unchecked Constrain to Margins.

To fix the Auto Layout errors, you need to specify the scroll view’s size. Set 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 the Attributes Inspector, specify photo1 for the image; choose Aspect Fit for the mode; and check clips to bounds.

uiscrollview tutorial

Add top, leading, and trailing constraints to Container View like before, and add a height constraint of 300.

Add a Label inside Container View below the image view. Specify the label’s text as What name fits me best?, and add a centered horizontal constraint relative to Container View. Add a vertical spacing constraint of 0 with Photo View.

Add a Text Field inside of Container View below the new label. Add leading and trailing constraints to Container View with constant values of 8, and no margin. Add a vertical-space constraint of 30 relative to the label.

You next need to connect a segue to your new View Controller.

To do so, first delete 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 the new View Controller, add a show segue. Make the identifier showPhotoPage.

Build and Run.

uiscrollview tutorial

You can see that the 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 need to pass the image name to the view controller when the segue is executed.

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.

Replace the contents of PhotoCommentViewController.swift with this code:

import UIKit

class PhotoCommentViewController: UIViewController {
  @IBOutlet weak var imageView: UIImageView!
  @IBOutlet weak var scrollView: UIScrollView!
  @IBOutlet weak var nameTextField: UITextField!
  
  var photoName: String?

  override func viewDidLoad() {
    super.viewDidLoad()
    if let photoName = photoName {
      self.imageView.image = UIImage(named: photoName)
    }
  }
}

This adds IBOutlets and sets the image of imageView based on a passed-in photoName.

Back in the storyboard, open the Identity Inspector for View Controller, and select PhotoCommentViewController for the Class. Then wire the IBOutlets for the Scroll View, Image View and Text Field.

Open CollectionViewController.swift, and replace prepare(segue:sender:) with this:

override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
  if let cell = sender as? UICollectionViewCell,
      let indexPath = collectionView?.indexPath(for: cell),
      let photoCommentViewController = segue.destination as? PhotoCommentViewController {
    photoCommentViewController.photoName = "photo\(indexPath.row + 1)"
  }
}

This sets the name of the photo to be shown on PhotoCommentViewController whenever one of the photos is tapped.

Build and run.

uiscrollview tutorial

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?

Managing the Keyboard

Unlike UITableViewController, which automatically handles moving content out of the way of the keyboard, you manually have to manage the keyboard when you use a UIScrollView directly.

You can do this by making PhotoCommentViewController observe keyboard Notification objects sent by iOS whenever the keyboard will show and hide.

Open PhotoCommentViewController.swift, and add the following code at the bottom of viewDidLoad() (ignore the compiler errors for now):

NotificationCenter.default.addObserver(
  self,
  selector: #selector(PhotoCommentViewController.keyboardWillShow(_:)),
  name: Notification.Name.UIKeyboardWillShow,
  object: nil
)
NotificationCenter.default.addObserver(
  self,
  selector: #selector(PhotoCommentViewController.keyboardWillHide(_:)),
  name: Notification.Name.UIKeyboardWillHide,
  object: nil
)

Next, add the following method to stop listening for notifications when the object’s life ends:

deinit {
  NotificationCenter.default.removeObserver(self)
}

Then add the promised methods from above to the view controller:

func adjustInsetForKeyboardShow(_ show: Bool, notification: Notification) {
  let userInfo = notification.userInfo ?? [:]
  let keyboardFrame = (userInfo[UIKeyboardFrameEndUserInfoKey] as! NSValue).cgRectValue
  let adjustmentHeight = (keyboardFrame.height + 20) * (show ? 1 : -1)
  scrollView.contentInset.bottom += adjustmentHeight
  scrollView.scrollIndicatorInsets.bottom += adjustmentHeight
}
  
@objc func keyboardWillShow(_ notification: Notification) {
  adjustInsetForKeyboardShow(true, notification: notification)
}
  
@objc func keyboardWillHide(_ notification: Notification) {
  adjustInsetForKeyboardShow(false, notification: notification)
}

adjustInsetForKeyboardShow(_:,notification:) takes the keyboard’s height as delivered in the notification and 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 dismiss the keyboard.

Finally, open Main.storyboard, and from Object Library drag a Tap Gesture Recognizer onto the View on the Photo Comment View Controller scene. Then, wire it to the hideKeyboard(_:) IBAction in Photo Comment View Controller.

To make it more user friendly, the keyboard should also dismiss when the return key is pressed. Right click on nameTextField and wire Primary Action Triggered to hideKeyboard(_:) also.

Build and run.

uiscrollview tutorial

Navigate to the Photo Comment View Controller scene. Tap the text field and then tap somewhere else on the view. The keyboard should properly show and hide itself relative to the other content on the screen. Likewise, tapping the return key does the same.

Paging with UIPageViewController

In the third section of this UIScrollView tutorial, you’ll create a scroll view that allows paging. This means that the scroll view locks onto a page when you stop dragging. You can see this in action in the App Store app when you view screenshots of an app.

Go to Main.storyboard and drag a Page View Controller from the Object Library. Open the Identity Inspector and enter PageViewController for the Storyboard ID.

In the Attributes Inspector, the Transition Style is set to Page Curl by default; change it to Scroll and set the Page Spacing to 8.

uiscrollview tutorial

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 this property after the others:

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.

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()
    
    // 1
    if let viewController = viewPhotoCommentController(currentIndex ?? 0) {
      let viewControllers = [viewController]
      
      // 2
      setViewControllers(
        viewControllers,
        direction: .forward,
        animated: false,
        completion: nil
      )
    }    
  }
  
  func viewPhotoCommentController(_ index: Int) -> PhotoCommentViewController? {
    guard let storyboard = storyboard,
      let page = storyboard.instantiateViewController(withIdentifier: "PhotoCommentViewController")
        as? PhotoCommentViewController else {
          return nil
    }
    page.photoName = photos[index]
    page.photoIndex = index
    return page
  }
}

Here’s what this code does:

  1. viewPhotoCommentController(_:_) creates an instance of PhotoCommentViewController 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.
  2. You setup the UIPageViewController by passing it an array that contains the single view controller you just created.

You next need to implement UIPageViewControllerDataSource. Add the following class extension to the end of this file:

extension ManagePageViewController: UIPageViewControllerDataSource {

  func pageViewController(_ pageViewController: UIPageViewController,
                          viewControllerBefore viewController: UIViewController) -> UIViewController? {
    
    if let viewController = viewController as? PhotoCommentViewController,
      let index = viewController.photoIndex,
      index > 0 {
      return viewPhotoCommentController(index - 1)
    }
    
    return nil
  }
  
  func pageViewController(_ pageViewController: UIPageViewController,
                          viewControllerAfter viewController: UIViewController) -> UIViewController? {
    
    if let viewController = viewController as? PhotoCommentViewController,
      let index = viewController.photoIndex,
      (index + 1) < photos.count {
      return viewPhotoCommentController(index + 1)
    }
    
    return nil
  }
}

The UIPageViewControllerDataSource allows 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, and based on the photoIndex, a new controller is created and returned.

You also need to actually set the dataSource. Add the following to the end of viewDidLoad():

dataSource = self

There are only a couple things left to do to get your page view running. First, you will fix the flow of the application.

Switch back to Main.storyboard and select your newly created
Page View Controller scene. In the Identity Inspector, specify ManagePageViewController for its class.

Delete the push segue showPhotoPage you created earlier. Then 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 as before.

Open CollectionViewController.swift and change the implementation of prepare(segue:sender:) to the following:

override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
  if let cell = sender as? UICollectionViewCell,
    let indexPath = collectionView?.indexPath(for: cell),
    let managePageViewController = segue.destination as? ManagePageViewController {
    managePageViewController.photos = photos
    managePageViewController.currentIndex = indexPath.row
  }
}

Build and run.

uiscrollview tutorial

You can now scroll side to side to page between different detail views. :]

Displaying a Page Control Indicator

For the final part of this UIScrollView tutorial, you will add a UIPageControl to your application.

Fortunately, 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 on UIPageViewControllerDataSource. You previously set the Transition Style- great job!- so all you need to do is add these two methods inside the UIPageViewControllerDataSource extension on ManagePageViewController:

func presentationCount(for pageViewController: UIPageViewController) -> Int {
  return photos.count
}
  
func presentationIndex(for pageViewController: UIPageViewController) -> Int {
  return currentIndex ?? 0
}

In presentationCount(for:), you specify the number of pages to display in the page view controller.

In presentationIndex(for:), you tell the 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 this:

func application(_ application: UIApplication,
                   didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
    
  let pageControl = UIPageControl.appearance()
  pageControl.pageIndicatorTintColor = UIColor.lightGray
  pageControl.currentPageIndicatorTintColor = UIColor.red
  return true
}

This will customize the colors of the UIPageControl.

Build and run.

uiscrollview tutorial

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 to the end of the class:

@IBAction func openZoomingController(_ sender: AnyObject) {
  self.performSegue(withIdentifier: "zooming", sender: nil)
}
  
override func prepare(for segue: UIStoryboardSegue,
                           sender: Any?) {  
  if let id = segue.identifier,
    let zoomedPhotoViewController = segue.destination as? ZoomedPhotoViewController,
    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 and set the Identifier to zooming.

Select the Image View in Photo Comment View Controller, open the Attributes Inspector and check User Interaction Enabled. Drag a Tap Gesture Recognizer onto the Image View, 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.

uiscrollview tutorial

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 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 skillz!

If you run into any problems along the way or want to leave feedback about what you've read here, join the discussion in the comments below.

Team

Each tutorial at www.raywenderlich.com is created by a team of dedicated developers so that it meets our high quality standards. The team members who worked on this tutorial are:

Owen L Brown

Owen is a software engineer with a strong background in mechanical and electrical design. He has 17+ years of experience in writing embedded microcontroller firmware for machine automation. For the last several years, he has focused on integrating machine control with iOS using Bluetooth. He has several cool machine-control Bluetooth LE applications on the app store. Owen is also the author of a book called 'Integrating iOS Bluetooth LE with PIC18 Microcontrollers'. He can be reach via email. Feel free to checkout his website at Back-40.com.

Other Items of Interest

Save time.
Learn more with our video courses.

raywenderlich.com Weekly

Sign up to receive the latest tutorials from raywenderlich.com each week, and receive a free epic-length tutorial as a bonus!

Advertise with Us!

PragmaConf 2016 Come check out Alt U

Our Books

Our Team

Video Team

... 20 total!

iOS Team

... 78 total!

Android Team

... 26 total!

Unity Team

... 11 total!

Articles Team

... 15 total!

Resident Authors Team

... 18 total!

Podcast Team

... 7 total!

Recruitment Team

... 9 total!