Chapters

Hide chapters

Auto Layout by Tutorials

First Edition · iOS 13 · Swift 5.1 · Xcode 11

Section II: Intermediate Auto Layout

Section 2: 10 chapters
Show chapters Hide chapters

Section III: Advanced Auto Layout

Section 3: 6 chapters
Show chapters Hide chapters

6. Self-Sizing Views
Written by Jayven Nhan & Libranner Santos

Heads up... You're reading this book for free, with parts of this chapter shown beyond this point as scrambled text.

Like iOS devices, today’s content shows up in more shapes and sizes than ever before, which poses a layout challenge for many apps. To account for dynamic content, developers need views with self-sizing capabilities. Whether you’re reading your messages, browsing through your photo albums, or choosing your preferred font size, the content you receive can vary significantly in size.

An app that uses static-sized views for dynamic content can face many drawbacks, for example:

  • Content truncation.
  • Inefficiency in screen space utilization.
  • Inability to support user interface preferences.

Drawbacks like the ones above can drive countless users away from your app due to a poor user experience. With self-sizing views, you can address these problems, but first, you need to understand how they work. Often we think of Auto Layout from the top down. A full-screen view controller dictates the size of its view. Then, if you start creating constraints to it, you build the Auto Layout system from the top down. But there’s another option: You can create constraints on the child views and then from those views to their superviews. There’s no real difference in the type of constraints, it’s just a different way of thinking about them.

In this chapter, you’ll learn about the following topics:

  • Strategies to accomplish self-sizing views.
  • Sizing views with the bottom-to-top in a view hierarchy approach.
  • Dynamic sizing of table view cells.
  • Sizing views with the top-to-bottom in a view hierarchy approach.
  • Manually sizing collection view cells.

By the end of this chapter, you’ll know how to prepare your app’s user interface to consume and display virtually any content.

Accomplishing self-sizing views

Usually, a self-sizing view has a position determined by outside constraints or its parent view. This type of setup leaves two metrics for the view to determine: width and height. In some cases, like with a table view cell, the width is also determined by the parent, leaving only the height. Essentially, a self-sizing view acts as a container view for the views within itself. As long as the container view can figure out its size definitively, it can self-size.

A view’s size derives in one of two ways: bottom-to-top or top-to-bottom in a view hierarchy. A view either gets its size from the container view, or the view is the container view and gets its size from its child views. Look at the following diagram:

Both container views (black background) encapsulate an image view. Both image views contain the same image and have identical standard spacing Auto Layout constraints (indicated in red lines) around the edges.

However, the left container view is bigger than the right container view.

Look at the container view’s constraints:

Both container views have leading and top edge constraints. However, only the right container view has a set of width and height constraints.

If a container view contains child views with intrinsic size, it’ll grow and shrink to accommodate those views while taking Auto Layout constraints, such as the padding around the child views, into account. In this case, an inside-out or bottom-to-top approach to the view hierarchy gives the container view its size.

On the other hand, if a container view has a fixed width and/or height, then the child views will grow and shrink to accommodate the container view’s size constraints. In this case, an outside-in or top-to-bottom approach to the view hierarchy gives the child views their size.

For example, think of a table view. If the table view sets a fixed row height, like 50 points, then the row size is going to be 50 points high, no matter what the size of its children. If there are less than 50 points of content, there will be extra whitespace in that row. And if another row has more than 50 points of content, it will be clipped or shrunk to fit within 50 points. But, if you don’t set an explicit row height and let the cells self-size, the rows that have less content will be smaller, and the rows with more content will grow to show all the content.

You’ll want to use the inside-out or bottom-to-top in the view hierarchy approach to give the container view its size. Let the container view derive its width and height from its children and Auto Layout. The child views must interconnect in a way with Auto Layout that pushes and pulls the container view outward or inward and grows or shrinks the container view. Consequently, the views within give shape to the container view.

What you’ve seen here with the child views giving shape to the container view is analogous to self-sizing UITableViewCell and UICollectionViewCell. In this chapter, you’ll learn about the bottom-to-top approach to size a UITableViewCell. In contrast, you’ll also learn about the top-to-bottom approach to size a UICollectionViewCell.

If self-sizing UICollectionViewCell using the bottom-to-top approach interests you, read Chapter 11, “Dynamic Type.”

Table views

In the previous chapter, you saw how you could use a scroll view to create an interface that goes beyond the physical size of the screen. But scroll views aren’t the only views at your disposal in iOS. There’s another convenient tool available for when you need to display lists of elements in an organized and user-friendly way. These views are known as table views.

Self-sizing table view cells

Open the table view’s starter project. Build and run.

tableView.rowHeight = UITableView.automaticDimension
override func tableView(
  _ tableView: UITableView, 
  cellForRowAt indexPath: IndexPath
) -> UITableViewCell {
  let message = messages[indexPath.row]
  
  //1
  var cell: MessageBubbleTableViewCell
  if message.sentByMe {
    cell = tableView.dequeueReusableCell(
      withIdentifier: MessageBubbleCellType.rightText.rawValue,
      for: indexPath) as! RightMessageBubbleTableViewCell
  } else {
    cell = tableView.dequeueReusableCell(
      withIdentifier: MessageBubbleCellType.leftText.rawValue,
      for: indexPath) as! LeftMessageBubbleTableViewCell
  }
  
  //2
  cell.textLabel?.text = message.text

  return cell
}
func configureLayout() {
  contentView.addSubview(messageLabel)
  
  NSLayoutConstraint.activate([
    messageLabel.topAnchor.constraint(
      equalTo: contentView.topAnchor, 
      constant: 10),
   
    messageLabel.rightAnchor.constraint(
      equalTo: contentView.rightAnchor, 
      constant: -10),
   
    messageLabel.bottomAnchor.constraint(
      equalTo: contentView.bottomAnchor, 
      constant: -10),
   
    messageLabel.leftAnchor.constraint(
      equalTo: contentView.leftAnchor, 
      constant: 10)
  ])
}

Implementing self-sizing cells

To give this app a standard-looking chat app appearance, you’ll place the messages inside chat bubbles and make them look different for each user.

contentView.addSubview(bubbleImageView)
func configureLayout() {
  contentView.addSubview(bubbleImageView)
  contentView.addSubview(messageLabel)
}
lazy var messageLabel: UILabel = {
  let messageLabel = UILabel(frame: .zero)
  messageLabel.textColor = .white
  ...
}()
NSLayoutConstraint.activate([
  //1
  contentView.topAnchor.constraint(
    equalTo: bubbleImageView.topAnchor, 
    constant: -10),
  contentView.trailingAnchor.constraint(
    greaterThanOrEqualTo: bubbleImageView.trailingAnchor, 
    constant: 20),
  contentView.bottomAnchor.constraint(
    equalTo: bubbleImageView.bottomAnchor, 
    constant: 10),
  contentView.leadingAnchor.constraint(
    equalTo: bubbleImageView.leadingAnchor, 
    constant: -20),
  //2
  bubbleImageView.topAnchor.constraint(
    equalTo: messageLabel.topAnchor, 
    constant: -5),
  bubbleImageView.trailingAnchor.constraint(
    equalTo: messageLabel.trailingAnchor, 
    constant: 10),
  bubbleImageView.bottomAnchor.constraint(
    equalTo: messageLabel.bottomAnchor, 
    constant: 5),
  bubbleImageView.leadingAnchor.constraint(
    equalTo: messageLabel.leadingAnchor, 
    constant: -20)
])

//3
let insets = UIEdgeInsets(
  top: 0, 
  left: 20,
  bottom: 0, 
  right: 10)
//4
let image = UIImage(named: blueBubbleImageName)!
  .imageFlippedForRightToLeftLayoutDirection()
//5    
bubbleImageView.image = image.resizableImage(
  withCapInsets: insets, 
  resizingMode: .stretch)

NSLayoutConstraint.activate([
  contentView.topAnchor.constraint(
    equalTo: bubbleImageView.topAnchor, 
    constant: -10),
  contentView.trailingAnchor.constraint(
    equalTo: bubbleImageView.trailingAnchor, 
    constant: 20),
  contentView.bottomAnchor.constraint(
    equalTo: bubbleImageView.bottomAnchor, 
    constant: 10),
  //1
  contentView.leadingAnchor.constraint(
    lessThanOrEqualTo: bubbleImageView.leadingAnchor, 
    constant: -20),
  //2
  bubbleImageView.topAnchor.constraint(
    equalTo: messageLabel.topAnchor, 
    constant: -5),
  bubbleImageView.trailingAnchor.constraint(
    equalTo: messageLabel.trailingAnchor, 
    constant: 20),
  bubbleImageView.bottomAnchor.constraint(
    equalTo: messageLabel.bottomAnchor, constant: 5),
  bubbleImageView.leadingAnchor.constraint(
    equalTo: messageLabel.leadingAnchor, 
    constant: -10)
])

//3
let insets = UIEdgeInsets(
  top: 0, 
  left: 10,
  bottom: 0, 
  right: 20)
//4
let image = UIImage(named: greenBubbleImageName)!
  .imageFlippedForRightToLeftLayoutDirection()
//5
bubbleImageView.image = image.resizableImage(
  withCapInsets: insets, 
  resizingMode: .stretch)

Collection View

UITableView presents a list of rows in a single column. You’d typically want to choose table views for row layouts. Table views can benefit you with an easy to setup layout process. You can get them up and running in almost no time. As amazing as table views are, they do fall short when you want to support layouts beyond a single-column layout.

Why collection view?

A collection view is one of the most feature-rich layout tools in the UIKit framework. A collection view can display a list of items in almost any layout imaginable. Collection views come to mind usually when a table view doesn’t offer the more complex layout features. In other words, whenever you intend your list to present layout in ways different than rows, a collection view is often the hero to save the day.

Collection view anatomy

To build out a collection view, there are four basic components you need to know about to get started:

Building mini-story view

In many social media apps, you’ll find story sharing features. The stories contain events shared within the last 24 hours. You can find the story sharing features in apps such as Snapchat, Messenger and Instagram.

Setting up collection view properties

Open the collection view’s starter project. Build and run.

// 1
private let verticalInset: CGFloat = 8
private let horizontalInset: CGFloat = 16
// 2
private lazy var flowLayout: UICollectionViewFlowLayout = {
  let flowLayout = UICollectionViewFlowLayout()
  flowLayout.minimumLineSpacing = 16
  flowLayout.scrollDirection = .horizontal
  flowLayout.sectionInset = UIEdgeInsets(
    top: verticalInset,
    left: horizontalInset,
    bottom: verticalInset,
    right: horizontalInset)
  return flowLayout
}()
// 3
private lazy var collectionView: UICollectionView = {
  let collectionView = UICollectionView(
    frame: .zero, 
    collectionViewLayout: flowLayout)
  collectionView.register(
    MiniStoryCollectionViewCell.self,
    forCellWithReuseIdentifier: cellIdentifier)
  collectionView.showsHorizontalScrollIndicator = false
  collectionView.alwaysBounceHorizontal = true
  collectionView.backgroundColor = .systemGroupedBackground
  collectionView.dataSource = self
  collectionView.delegate = self
  return collectionView
}()

Creating Auto Layout constraint extensions

In your project, you can abstract commonly used code into a helper method. Also, extensions allow you to add additional functionalities to existing code.

import UIKit

extension UIView {
  func fillSuperview(withConstant constant: CGFloat = 0) {
    guard let superview = superview else { return }
    translatesAutoresizingMaskIntoConstraints = false
    NSLayoutConstraint.activate(
      [leadingAnchor.constraint(
        equalTo: superview.leadingAnchor, 
        constant: constant),
       topAnchor.constraint(
        equalTo: superview.topAnchor, 
        constant: constant),
       trailingAnchor.constraint(
        equalTo: superview.trailingAnchor, 
        constant: -constant),
       bottomAnchor.constraint(
        equalTo: superview.bottomAnchor, 
        constant: -constant)]
    )
  }
}

Adding collection view constraints

Now, add the following code to setupCollectionView() in MiniStoryView:

addSubview(collectionView)
collectionView.fillSuperview()

Manually sizing collection view cells

There are two ways to determine a collection view cell size. Although you can size collection view cells manually or automatically, this section focuses on ways to manually size collection view cells.

override func layoutSubviews() {
  super.layoutSubviews()
  let height = collectionView.frame.height - verticalInset * 2
  let width = height
  let itemSize = CGSize(width: width, height: height)
  flowLayout.itemSize = itemSize
}

Building user story view controller’s collection view

Now, you’ll build out the collection view to display the user stories. You can see a user’s stories by tapping on a MiniStoryCollectionViewCell in the MessagesViewController. You’ll work with the collection view’s UICollectionViewDelegateFlowLayout, reusable supplementary views and handle app orientation transitions.

Working with UICollectionViewDelegateFlowLayout

Earlier, you’ve learned to set the collection view item size, content insets, and line spacings with the collection view flow layout properties. Now, you’ll learn to implement the collection view item size and content insets by implementing UICollectionViewDelegateFlowLayout’s methods.

// MARK: - UICollectionViewDelegateFlowLayout
extension UserStoryViewController:
  UICollectionViewDelegateFlowLayout {
  // 1
  func collectionView(
    _ collectionView: UICollectionView, 
    layout collectionViewLayout: UICollectionViewLayout, 
    sizeForItemAt indexPath: IndexPath
  ) -> CGSize {
    return collectionView.frame.size
  }
  // 2
  func collectionView(
    _ collectionView: UICollectionView, 
    layout collectionViewLayout: UICollectionViewLayout, 
    insetForSectionAt section: Int
  ) -> UIEdgeInsets {
    return .zero
  }
  // 3
  func collectionView(
    _ collectionView: UICollectionView, 
    layout collectionViewLayout: UICollectionViewLayout, 
    minimumLineSpacingForSectionAt section: Int
  ) -> CGFloat {
    return 0
  }
}
collectionView.delegate = self

Working with reusable supplementary views

Now, your goal is to implement a header view in the collection view. Open HeaderCollectionReusableView.swift inside of User Story/Views. Add the following code to the setupStackView():

addSubview(stackView)
stackView.fillSuperview()
NSLayoutConstraint.activate(
  [topSpacerView.heightAnchor.constraint(
    equalTo: bottomSpacerView.heightAnchor)]
)
collectionView.register(
  HeaderCollectionReusableView.self,
  forSupplementaryViewOfKind:
    UICollectionView.elementKindSectionHeader,
  withReuseIdentifier: headerViewIdentifier)
// 1
func collectionView(
  _ collectionView: UICollectionView,
  viewForSupplementaryElementOfKind kind: String,
  at indexPath: IndexPath
) -> UICollectionReusableView {
  // 2
  guard let headerView = collectionView
    .dequeueReusableSupplementaryView(
      ofKind: UICollectionView.elementKindSectionHeader,
      withReuseIdentifier: headerViewIdentifier,
      for: indexPath) as? HeaderCollectionReusableView
      else { fatalError("Dequeued unregistered reusable view") }
  // 3
  headerView.configureCell(username: userStory.username)
  return headerView
}
func collectionView(
  _ collectionView: UICollectionView,
  layout collectionViewLayout: UICollectionViewLayout,
  referenceSizeForHeaderInSection section: Int
) -> CGSize {
  return collectionView.frame.size
}

Handling app orientation transitions

Currently, there’s a visual conflict when UserStoryViewController is present and the device rotates. To see the visual conflict, build and run. Tap a user story. With a UserStoryViewController in view, scroll to the second collection view cell. Rotate the device. You’ll see something like this:

// 1
func scrollViewWillEndDragging(
  _ scrollView: UIScrollView,
  withVelocity velocity: CGPoint,
  targetContentOffset: UnsafeMutablePointer<CGPoint>
) {
  let contentOffsetX = targetContentOffset.pointee.x
  let scrollViewWidth = scrollView.frame.width
  currentItemIndex = Int(contentOffsetX / scrollViewWidth)
}
private func centerCollectionViewContent() {
  DispatchQueue.main.async { [weak self] in
    guard let self = self else { return }
    // 1
    let x = self.collectionView.frame.width 
      * CGFloat(self.currentItemIndex)
    let y: CGFloat = 0
    let contentOffset = CGPoint(x: x, y: y)
    
    // 2
    self.collectionView.setContentOffset(
      contentOffset,animated: false)
  }
}
override func viewWillTransition(
  to size: CGSize,
  with coordinator: UIViewControllerTransitionCoordinator
) {
  super.viewWillTransition(to: size, with: coordinator)
  centerCollectionViewContent()
}

Challenges

Using what you’ve learned so far, implement a collection view will the following instructions:

Key points

  • Use self-sizing views for dynamic content.
  • When working with self-sizing table view cells, make sure to set rowHeight of the table view to automaticDimension.
  • Be careful while setting the constraints inside of the table view cell. You should create these constraints with the content view, not the cells.
  • UICollectionView offers more layout flexibilities than UITableView.
  • A collection view requires a UICollectionViewLayout and UICollectionViewCell to populate itself. Supplementary and decoration views are optional items.
  • Typically, use UICollectionViewFlowLayout in the collection view for grid layout.
  • Subclass UICollectionViewLayout for layout that couldn’t be easily achieved with UICollectionViewFlowLayout.
  • You can manually or dynamically size a collection view cell or reusable view.
  • Set the collection view data source with UICollectionViewDataSource.
  • You can size a collection view cell or reusable view by adopting and implementing methods from UICollectionViewDelegateFlowLayout.
  • Get notified of collection view events with UICollectionViewDelegate.
  • To make your layout look great, you may need to adjust it for the event of device orientation.
Have a technical question? Want to report a bug? You can ask questions and report bugs to the book authors in our official book forum here.
© 2024 Kodeco Inc.

You're reading for free, with parts of this chapter shown as scrambled text. Unlock this book, and our entire catalogue of books and videos, with a Kodeco Personal Plan.

Unlock now