跳转至

5 Scroll View

By now, you understand the power of stack views. But what options do you have when you need to create user interfaces that go beyond the screen size? Scroll views.

Scroll views allow you to expand your interfaces beyond the limits of the screen. As written in the official Apple documentation at https://apple.co/2WoN2Be:

UIScrollView: A view that allows the scrolling and zooming of its contained views.

A scroll view acts as a parent view and can contain as many views as your interface needs; they are a key component for any app that requires more space. For example, you might need more space to display parts of a form or when dealing with a significant amount of text. Or maybe your app includes a large image that users need to zoom into and some way to navigate the UI that mimics a carousel. These are all great reasons to use a scroll view, and in this chapter, you’ll learn how to use them in your app.

Working with scroll view and Auto Layout

Scroll views are different than other views when it comes to Auto Layout because you need to make two types of constraints: One that sets the x, y, width and height of the scroll view, and one that sets the x, y, width and height of the subviews in the content area. Generally, when you make constraints between the scroll view and views outside of its view hierarchy, you set the scroll view’s frame. But, when you make constraints between the scroll view and views inside of its view hierarchy, you set the frame of the subviews in the scrollable content area.

img

When working with scroll views, keep the following in mind:

  • You need to define both the size and position of the scroll view’s frame within its superview and the size of the scroll view’s content area.
  • To define the content area, it’s best to add a content view that’s anchored to the edges of the scroll view.
  • If you don’t want to have horizontal scrolling, make the content view’s width equal to the scroll view’s width. The same is true for vertical scrolling but using the height instead.
  • When adding content, add constraints related to the content view. If you want to create a floating effect for a view inside of the scroll view, add constraints between the target view and objects outside of the scroll view.

Adding the Options Menu to the Profile Screen

Go to the starter project, open the MessagingApp project, and build and run.

img

The app already contains part of the UI you need; however, the bottom part is missing. You’ll need to add some buttons so that the user can see all of the options.

Go to ProfileViewController.swift and add the following code below viewDidAppear(_:):

private func setupMainStackView() {
  mainStackView.axis = .vertical
  mainStackView.distribution = .equalSpacing
  mainStackView.translatesAutoresizingMaskIntoConstraints =
    false

  view.addSubview(mainStackView)

  let contentLayoutGuide = view.safeAreaLayoutGuide

  NSLayoutConstraint.activate([
    mainStackView.leadingAnchor.constraint(equalTo:
      contentLayoutGuide.leadingAnchor),
    mainStackView.trailingAnchor.constraint(equalTo:
      contentLayoutGuide.trailingAnchor),
    mainStackView.topAnchor.constraint(equalTo:
      contentLayoutGuide.topAnchor),
  ])

  setupProfileHeaderView()
  setupButtons()
}

With this new method, you:

  1. Set up mainStackView, indicating its alignment, axis and distribution. You also set its translatesAutoresizingMaskIntoConstraints to false. The mainStackView is already declared at the top of the class.
  2. Add mainStackView to view so that you can add it to the view hierarchy.
  3. Create the constant contentLayoutGuide to get a reference to the view.safeAreaLayoutGuide. You’ll create constraints related to this layout guide instead of the view itself. In this case, you’ll use the safeAreaLayoutGuide, which allows creating UIs respecting the margins on the devices, so things like the iPhone X notch do not interfere with the views.
  4. Set the leading, trailing and top constraints for mainStackView so that you can properly position it on the screen. Since the Stack View will grow vertically, it’s not necessary to indicate the bottom constraint.

Go to setupProfileHeaderView() and replace its code with the following:

profileHeaderView.translatesAutoresizingMaskIntoConstraints =
  false
profileHeaderView.heightAnchor.constraint(
  equalToConstant: 360).isActive = true
mainStackView.addArrangedSubview(profileHeaderView)

This code:

  1. Removes all of the previous constraints and adds one constraint for the height.
  2. Adds profileHeaderView to mainStackView.

Because the view is now inside of a stack view, you don’t have to add the same constraints you added before; the alignment and distribution properties will cause the contained views to resize accordingly. That’s part of the magic behind working with stack views.

Go to viewDidLoad() and replace the call to setupProfileHeaderView() with the following:

 setupMainStackView()

Build and run.

img

The app now displays some options in the bottom section; however, there are additional options you can’t see. More importantly, if you rotate the simulator using Command-Right-Arrow, you’ll see even fewer options or not at all depending on the device:

img

Stop the project, and in ProfileViewController.swift, go to setupButtons(). Look for the following block of code:

func setupButtons() {
  let buttonTitles = [
    "Share Profile", "Favorites Messages", "Saved Messages",
    "Bookmarks", "History", "Notifications", "Find Friends",
    "Security", "Help", "Logout"]

  let buttonStack = UIStackView()
  buttonStack.translatesAutoresizingMaskIntoConstraints = false
  buttonStack.alignment = .fill
  buttonStack.axis = .vertical
  buttonStack.distribution = .equalSpacing

  buttonTitles.forEach { (buttonTitle) in
    buttonStack.addArrangedSubview(
      createButton(text: buttonTitle))
  }

  mainStackView.addArrangedSubview(buttonStack)
  NSLayoutConstraint.activate([
    buttonStack.widthAnchor.constraint(equalTo:
      mainStackView.widthAnchor),
    buttonStack.centerXAnchor.constraint(equalTo:
      mainStackView.centerXAnchor)
  ])
}

This code takes an array named buttonTitles and creates a button for each title. In this case, there are ten titles, but the app doesn’t show all of them because the size of the stack view after — adding all the buttons — is greater than the space available on the screen. To fix this problem, you’ll use a scroll view.

Setting up the scroll view

Go to the top of ProfileViewController and add a new property after the mainStackView declaration. This new property will contain the Scroll View:

private let scrollView = UIScrollView()

Next, add this method below setupMainStackView():

private func setupScrollView() {
  //1
  scrollView.translatesAutoresizingMaskIntoConstraints = false
  view.addSubview(scrollView)

  //2
  NSLayoutConstraint.activate([
    scrollView.leadingAnchor.constraint(equalTo: 
      view.leadingAnchor),
    scrollView.trailingAnchor.constraint(equalTo: 
      view.trailingAnchor),
    scrollView.topAnchor.constraint(equalTo:
      view.safeAreaLayoutGuide.topAnchor),
    scrollView.bottomAnchor.constraint(equalTo:
      view.safeAreaLayoutGuide.bottomAnchor)
  ])
}

With this new method, you:

  1. Set translatesAutoresizingMaskIntoConstraints to false so that scrollView won’t translate the autoresizing mask into constraints. Because you’re creating all of the constraints using Auto Layout, this translation is not necessary.
  2. Create leading, trailing, top and bottom constraints for scrollView anchored to its superview. Since these are constraints to a view outside of the scroll view’s hierarchy, they’ll set the scroll view’s frame.

You now need to put mainStackView inside of the newly created scroll view.

Replace setupMainStackView() with the following:

private func setupMainStackView() {
  mainStackView.axis = .vertical
  mainStackView.distribution = .equalSpacing
  mainStackView.translatesAutoresizingMaskIntoConstraints
    = false

  //1
  scrollView.addSubview(mainStackView)

  //2
  let contentLayoutGuide = scrollView.contentLayoutGuide

  NSLayoutConstraint.activate([
    //3
    mainStackView.widthAnchor.constraint(equalTo:
      view.widthAnchor),
    mainStackView.leadingAnchor.constraint(equalTo:
      contentLayoutGuide.leadingAnchor),
    mainStackView.trailingAnchor.constraint(equalTo:
      contentLayoutGuide.trailingAnchor),
    mainStackView.topAnchor.constraint(equalTo:
      contentLayoutGuide.topAnchor),
    //4
    mainStackView.bottomAnchor.constraint(equalTo:
      contentLayoutGuide.bottomAnchor)
  ])

  //5
  setupProfileHeaderView()
  setupButtons()
}

This code looks similar to the previous implementation; however, they are some important modifications:

  1. You made mainStackView a subview of scrollView.
  2. The constantcontentLayoutGuide now contains a reference to the scroll view’s Content Layout Guide. This layout guide represents the content area of the scrollView. You could have used the scrollView itself to create the constraint, and it would look the same, but doing it as you did here brings more clarity to your code.
  3. There’s a new constraint for the width of the mainStackView. Notice that widthAnchor is created in relation to the view. That’s because a scroll view takes on the size of its content, and not doing it this way will cause the scroll view to shrink. Also, by doing this, you’re indicating that horizontal scrolling is disabled. All the other constraints are between mainStackView and the contentLayoutGuide reference created above.
  4. There’s another new constraint between the bottom of the mainStackView and the contentLayoutGuide. This is what makes the scrollView grow so that it can fit all of the views.

The only thing left to do is to call your set up methods. In viewDidLoad(), add the following immediately before the call to setupMainStackView():

setupScrollView()

Build and run.

img

Using the Frame Layout Guide

Before you move on, there’s one more refactor you can do. Go to setupScrollView()and replace the implementation with this one:

private func setupScrollView() {
  scrollView.translatesAutoresizingMaskIntoConstraints = false
  view.addSubview(scrollView)

  //1
  let frameLayoutGuide = scrollView.frameLayoutGuide

  //2
  NSLayoutConstraint.activate([
    frameLayoutGuide.leadingAnchor.constraint(equalTo:
      view.leadingAnchor),
    frameLayoutGuide.trailingAnchor.constraint(equalTo:
      view.trailingAnchor),
   frameLayoutGuide.topAnchor.constraint(equalTo:
      view.safeAreaLayoutGuide.topAnchor),
    frameLayoutGuide.bottomAnchor.constraint(equalTo:
      view.safeAreaLayoutGuide.bottomAnchor)
  ])
}

Here’s what you did:

  1. Create the constant frameLayoutGuide to get a reference to the scrollView.frameLayoutGuide. You’ll create constraints related to this layout guide instead of the scroll view itself. This layout guide refers to the frame of the scroll view, not the content area.
  2. Create the leading, trailing, top and bottom constraints between the Frame Layout Guide of scrollView and the view. Notice how the leading and trailing constraints are created using the view, not the Safe Area Layout Guide; in this case, scrollView will occupy the entire width of the screen.

Build and run.

img

It looks like nothing changed, but with the use of the Frame Layout Guide, your code is now more precise and easier to understand. If you want to learn more about Layout Guide, read Chapter 7, “Layout Guide.”

That wasn’t too bad, was it? With the new options menu you added, users will have a better experience with your app. Before wrapping things up, try running the app on different simulators so you can see how it works on any screen size.

Challenge

For this challenge, you’ll need to create the Settings button and add it to the scroll view. This button should appear near the bottom of the scroll view and close to the right side, but it should not be part of the stack view. For reference, here’s how it should look:

img

Key points

  • Constraints between a scroll view and the views outside of its view hierarchy act on the scroll view’s frame.
  • Constraints between a scroll view and views inside of its view hierarchy act on the scroll view’s content area.
  • Be extra careful when setting the size and position of your scroll view. Remember that apart from setting its size and position, you’ll have to specify a content area that will affect the way the scroll view behaves.
  • It’s strongly recommended to add a content view that acts as a container for all of the views inside of the scroll view. This makes it easier to work with a scroll view that can grow in size.
  • While working with stack views (or any views) inside of a scroll view that acts as the content area, the width and height determine if the scroll view will have vertical and horizontal scrolling.
  • Remember to use frameLayoutGuide and contentLayoutGuide when creating the constraints for scroll views.