Shared posts

07 Sep 13:52

Easier Scrolling With Layout Guides

by Keith Harrison

Did you spot that iOS 11 added a couple of new layout guides to UIScrollView? If you find using Auto Layout with a scroll view confusing they might help you remember how to add your constraints.

I read a great post recently by Agnes Vasarhelyi on Scrollable UIStackView which describes setting up constraints for a scroll view. It reminded me that I meant to take a look at the new layout guides Apple added to UIScrollView in iOS 11 to see if they make the setup any easier to understand.

Let's first recap the classic way to setup your constraints with a scroll view.

Scroll Views - The Classic Setup

Here is my layout for a weather forecast view. The view has a title label, an image view and a description label. Both of the labels are using dynamic type which means the height of my view can change dramatically so it needs to scroll vertically:

Forecast view

It is often easier to use a containing content view when working with scroll views. This could be a stack view or in my case a custom UIView subclass to contain my two labels and the image view:

class ForecastView : UIView {
  let titleLabel: UILabel
  let imageView: UIImageView
  let summaryLabel: UILabel
}

I will skip the details of the internal layout of the content view and focus on the setup of the scroll view. In my view controller I create an instance of my content view and add it as a subview of the scroll view:

class ForecastViewController: UIViewController {
  private let forecastView: ForecastView = {
    let view = ForecastView()
    view.translatesAutoresizingMaskIntoConstraints = false
    view.backgroundColor = UIColor(named: "SkyBlue")
    return view
  }()

  private lazy var scrollView: UIScrollView = {
    let scrollView = UIScrollView()
    scrollView.translatesAutoresizingMaskIntoConstraints = false
    scrollView.addSubview(forecastView)
    return scrollView
  }()

Then from viewDidLoad I add the scroll view to the root view:

view.addSubview(scrollView)

We can now start building our constraints. We need two groups of constraints. The first group fixes the scroll view frame to its superview. The second group constrains our content subview to the scroll view setting the content area.

The first group of four constraints pin the frame of my scroll view to its superview:

NSLayoutConstraint.activate([
  scrollView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
  scrollView.topAnchor.constraint(equalTo: view.topAnchor),
  scrollView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
  scrollView.bottomAnchor.constraint(equalTo: view.bottomAnchor),

This next step is where I find the scroll view API confusing. We pin the content subview to the scroll view but in this case the scroll view anchors are acting to fix the content area of the scroll view:

  scrollView.leadingAnchor.constraint(equalTo: forecastView.leadingAnchor),
  scrollView.topAnchor.constraint(equalTo: forecastView.topAnchor),
  scrollView.trailingAnchor.constraint(equalTo: forecastView.trailingAnchor),
  scrollView.bottomAnchor.constraint(equalTo: forecastView.bottomAnchor),

Finally we constrain the width of our content subview. This fixes the width of the content area to the scroll view frame preventing horizontal scrolling:

  forecastView.widthAnchor.constraint(equalTo: scrollView.widthAnchor)
])

The confusion comes from the dual nature of the scroll view layout anchors. When constrained to the super view they are acting as the frame of the scroll view. When constrained to our content subview they act to set the content area of the scroll view.

Scroll View Layout Guides (iOS 11)

The UIScrollView class has two new layout guide properties in iOS 11:

var frameLayoutGuide: UILayoutGuide { get }

As the names suggest the frame layout guide matches the scroll view frame. We can use it when creating constraints from the scroll view to its superview or when we want to fix (float) a subview of the scroll view over the content.

var contentLayoutGuide: UILayoutGuide { get }

The content layout guide matches the content area of the scroll view so we can use it when constraining our content to the scroll view.

The guides don't add any new functionality but they do (I think) make it easier to follow what is going on.

Scroll View Setup With Layout Guides

So let's see how our scroll view setup changes if we use the new layout guides:

let frameGuide = scrollView.frameLayoutGuide
let contentGuide = scrollView.contentLayoutGuide

The first group of constraints fix the frame of the scroll view to the superview using the frame layout guide:

NSLayoutConstraint.activate([
  frameGuide.leadingAnchor.constraint(equalTo: view.leadingAnchor),
  frameGuide.topAnchor.constraint(equalTo: view.topAnchor),
  frameGuide.trailingAnchor.constraint(equalTo: view.trailingAnchor),
  frameGuide.bottomAnchor.constraint(equalTo: view.bottomAnchor),

The next four constraints fix the content area. This time we can use the content guide of the scroll view which makes it clearer (to me at least) what is happening.

  contentGuide.leadingAnchor.constraint(equalTo: forecastView.leadingAnchor),
  contentGuide.topAnchor.constraint(equalTo: forecastView.topAnchor),
  contentGuide.trailingAnchor.constraint(equalTo: forecastView.trailingAnchor),
  contentGuide.bottomAnchor.constraint(equalTo: forecastView.bottomAnchor),

Finally we fix the width of the content area by creating a constraint between the width of the content guide and the frame guide:

  contentGuide.widthAnchor.constraint(equalTo: frameGuide.widthAnchor)
])

Using the new guides takes the same number of constraints so it is not any less work. But I think it is easier to understand. In other good news, Xcode 11 also finally added support for the frame and content layout guides to Interface Builder. See Scroll View Layouts with Interface Builder for details.

Floating Content

You sometimes want a subview of the scroll view to have a fixed position in the scroll view frame so it appears to float over the scrollable content. You might do this by constraining the subview to some superview of the scroll view but that can be tricky. The frame layout guide makes this easy.

For example, to fix a button in the center of the scroll view frame:

infoButton.centerXAnchor.constraint(equalTo: frameGuide.centerXAnchor),
infoButton.centerYAnchor.constraint(equalTo: frameGuide.centerYAnchor)

We need to be careful though as my scroll view frame extends above and below the safe area. I want to fix my button up in the top left corner so in this case the layout margin guide of the scroll view works better (margins are relative to the safe area by default in iOS 11):

infoButton.leadingAnchor.constraint(equalTo: scrollView.layoutMarginsGuide.leadingAnchor),
infoButton.topAnchor.constraint(equalTo: scrollView.layoutMarginsGuide.topAnchor)
Scroll view with floating button

In summary, the two new layout guides are nothing magical but they do make using a scroll view with Auto Layout easier to understand once you can drop support for iOS 10.

Want To Learn More?

If you are struggling to build layouts that work across the growing range of iOS devices, you should get my book - Modern Auto Layout.

Get The Code

You can find the example Xcode project for this post in my GitHub repository.

Further Reading

The session from WWDC 2017 where they (briefly) mention the new layout guides:

I have not covered it here but you should be aware of the keyboard when using this type of layout:


Easier Scrolling With Layout Guides was originally posted 20 Aug 2018 on useyourloaf.com.

Want this direct to your inbox? Sign up and get my free WWDC viewing guide.


19 Jul 14:28

NSHipster Quiz #8

by nate@nshipster.com (Nate Cook)

This year’s WWDC edition of the NSHipster Pub Quiz was held on June 14th, once again testing the assembled developers with questions both random and obscure. We’re enormously grateful to Realm, who hosted the quiz for the second year in a row, with delicious food and drink and enough tables to seat nearly two hundred contestants.

Competition was as fierce as always. Laughs and groans were heard. And after the points were tallied, team “Hey Siri” won the evening and the mustache medallions with a score of 31 out of a possible 43 points. A hearty congratulations to Alek Åström, Cezary Wojcik, Kyle Sherman, Marcus Brissman, Marius Rackwitz, Melissa Huang, Nevyn Bengtsson, and Rob Stevenson!

Now it’s time for you to play along with the home edition—sharpen your pencil and give it your best!

  • Four rounds of ten questions
  • Record your answers on a separate sheet of paper
  • Each correct answer earns 1 point (unless otherwise specified)
  • Play with friends for maximum enjoyment
  • Don’t be lame and look things up on the internet or in Xcode

Round 1: General Knowledge

  1. In the WWDC keynote, Apple introduced the new OS X, er… macOS Sierra. The actual Sierra mountain range is home to the highest peak in the contiguous US. What is the name of that mountain?
  2. The Sierra were one focal point of a mass migration to California. What San Francisco sports team has ties to the Sierra during that period in history?
  3. Another highlight of the keynote was when Bozoma Saint John introduced the new Apple Music and got the crowd singing along to “Rapper’s Delight”—who recorded the song, and in what year? (2 points)
  4. Which version of iPhoto first introduced “Faces and Places?”
  5. As part of Foundation’s Swiftification, many classes have lost their NS prefixes. Which of these classes remains unchanged so far: NSBundle, NSCalendar, NSExpression, or NSOperation?
  6. More than just class names have changed—write the new Swift signature for this NSString method:

    func stringByReplacingCharactersInRange(
        _ range: NSRange,
                withString replacement: String) -> String
        
  7. Write the Swift 3 code to execute an asynchronous “Hello, world!” using GCD.
  8. Swift went open source in November and the pace of community contributions has been amazing to see. Within 100, how many pull requests (open, closed, or merged) has the Swift project received on GitHub?
  9. Swift was released to the public just over two years ago, but was clearly under development long before that at Apple. What were the month and year of the first commit to the Swift repository?
  10. After Chris Lattner, who was the second contributor to Swift? When was their first commit?

Round 2: Name That Framework

Foundation classes are losing their NS prefixes left and right. What would it look like if we got rid of prefixes in every framework? For each question in this round, you’ll be given three classes with their identifying prefix removed. Name the framework that contains all three.

  1. Statistic, Sample, Correlation
  2. CallObserver, Transaction, Provider
  3. Visit, Heading, Region
  4. Conversation, Session, Sticker
  5. IndexSet, ValueTransformer, Scanner
  6. Participant, Reminder, StructuredLocation
  7. Circle, LocalSearch, GeodesicPolyline
  8. LabeledValue, PhoneNumber, SocialProfile
  9. Quadtree, NoiseSource, MonteCarloStrategist
  10. RideStatus, PaymentMethod, CarAirCirculationModeResolutionResult

Round 3: Who Is That?

Many Apple advertisements over the years have featured celebrity voiceovers intoning words of wisdom, inspiration, or at times something else entirely. So pop in your earbuds and for each of the ads below, name the person(s) providing their voice talents.

Round 4: Easy as 1, 2, 3

Swift is an easy language to learn and use, but its breakneck speed of development has meant breaking changes with each release. For the following snippets of code, answer with the version of Swift that will compile and give the desired result. Because some snippets can run in more than one version, some questions may be worth up to 2 points. Only the major versions are required—for example, if a snippet will run in Swift 2.2, “Swift 2” is a scoring answer.

1

let a = ["1", "2", "3", "four", "5"]
    let numbers = map(a) { $0.toInt() }
    let onlyNumbers = filter(numbers) { $0 != nil }
    let sum = reduce(onlyNumbers, 0) { $0 + $1! }
    // sum == 11
    

2

let a = ["1", "2", "3", "four", "5"]
    let sum = a.flatMap { Int($0) }
    .reduce(0, combine: +)
    // sum == 11
    

3

var a = [8, 6, 7, 5, 3, 0, 9]
    a.sort()
    print(a)
    // [0, 3, 5, 6, 7, 8, 9]
    

4

var a = [8, 6, 7, 5, 3, 0, 9]
    sort(a)
    print(a)
    // [0, 3, 5, 6, 7, 8, 9]
    

5

var a = [8, 6, 7, 5, 3, 0, 9]
    a.sort()
    print(a)
    // [8, 6, 7, 5, 3, 0, 9]
    

6

for i in stride(from: 3, to: 10, by: 3) {
    print(i)
    }
    // 3
    // 6
    // 9
    

7

for i in 3.stride(to: 10, by: 3) {
    print(i)
    }
    // 3
    // 6
    // 9
    

8

enum MyError: ErrorProtocol {
    case Overflow
    case NegativeInput
    }
    func square(_ value: inout Int) throws {
    guard value >= 0 else { throw MyError.NegativeInput }
    let (result, overflow) = Int.multiplyWithOverflow(value, value)
    guard !overflow else { throw MyError.Overflow }
    value = result
    }
    var number = 11
    try! square(&number)
    // number == 121
    

9

enum MyError: ErrorType {
    case Overflow
    case NegativeInput
    }
    func squareInPlace(inout value: Int) throws {
    guard value >= 0 else { throw MyError.NegativeInput }
    let (result, overflow) = Int.multiplyWithOverflow(value, value)
    guard !overflow else { throw MyError.Overflow }
    value = result
    }
    var number = 11
    try! squareInPlace(&number)
    // number == 121
    

10

var a: Int[] = [1, 2, 3, 4, 5]
    let b = a
    a[0] = 100
    // b == [100, 2, 3, 4, 5]
    

That’s all! When you’re finished, scroll down a bit for the answers.



.

.

.


Answers

Round 1: General Knowledge

  1. Mount Whitney
  2. San Francisco 49ers
  3. The Sugarhill Gang, 1979 (2 points for both)
  4. iPhoto ’09
  5. NSExpression
  6. One of:

    // 1
        replacingCharacters(in: NSRange, with: String)
        // 2
        func replacingCharacters(
        in range: NSRange,
                with replacement: String) -> String
        
  7. One of:

    // 1
        let queue = DispatchQueue(label: "quiz")
        queue.async {
        print("Hello, world!")
        }
        // 2
        DispatchQueue.main.async {
        print("Hello, world!")
        }
        
  8. 3,012 as of June 14th, 2016 (correct if between 2,912 and 3,112)—check here for the current stats
  9. July 2010 (1 point if correct year, 2 if both)
  10. Doug Gregor, July 2011 (2 points)

Round 2: Name That Framework

  1. HealthKit
  2. CallKit
  3. Core Location
  4. Messages
  5. Foundation
  6. EventKit
  7. MapKit
  8. Contacts
  9. GamePlayKit
  10. Intents

Round 3: Who Is That?

  1. Jimmy Fallon & Justin Timberlake (2 points for both)
  2. Martin Scorsese
  3. Jeff Goldblum
  4. Lake Bell
  5. Kiefer Sutherland
  6. Robin Williams
  7. Jony Ive
  8. Jeff Daniels
  9. Richard Dreyfuss
  10. Drunk Jeff Goldblum

Round 4: Easy as 1, 2, 3

If you listed multiple versions, all must be correct for the answer to score.

  1. Swift 1
  2. Swift 2 or 3 (2 points for both)
  3. Swift 3
  4. Swift 1
  5. Swift 2
  6. Swift 1 or 3 (2 points for both)
  7. Swift 2
  8. Swift 3
  9. Swift 2
  10. Initial beta release of Swift

How’d you do? Tweet out your score to see how you stack up to your peers!