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

16. Layout Prototyping with Playgrounds
Written by Jayven Nhan

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

With a Swift playground, developers can quickly prototype their ideas and layouts using a minimal amount of code, allowing for instant feedback. In this chapter, you’ll look at some of the benefits of using playgrounds over a full Xcode project.

There are three main issues when using full Xcode projects for layout prototyping:

  • Boilerplate code and extra files: When you create a new single view project, Xcode adds boilerplate code along with additional files that you often don’t need.
  • Lack of Speed: Iteration is a vital component while building new apps, especially for layout prototyping. With full Xcode projects, you need to repeatedly build and run your project to see how everything works. With playgrounds, you can immediately see the results.
  • Dependency: When you build UIs on top of existing projects, you’re dealing with existing code, which means there’s a good chance your new code will influence the existing code and vice versa.

When you prototype your layout, you iterate. Because your time is valuable, playgrounds are useful for prototyping; they also give you some additional benefits, such as:

  • Documentation: Playground pages provide clear, convenient step-by-step documentation.

  • Xcode integration: You can integrate a playground with an Xcode project. This makes the project’s documentation convenient and accessible. This is something you can find in many of Apple’s latest sample projects.

  • Mobility: With the newer iPads, developers can create and run code right on the iPads using Swift Playgrounds. In doing so, they’ll have direct access to features not available on a simulator such as a camera.

These are just some of the benefits of using playgrounds. By the end of this chapter, you’ll have learned how to prototype layouts and provide markup documentation using playgrounds.

Getting started with playgrounds

Open the starter project and select the Working with Live View page.

At the top of the page, add the following code:

import PlaygroundSupport

This code imports the PlaygroundSupport framework, giving you access to the live view.

A live view allows Xcode to interact with a playground to display the executed code. A live view is the view you’ll be able to see and interact with. You can put almost any UI component into a live view.

Setting up the live view

Working with a live view requires minimal setup — all you need to do is give the live view a view to display. You can have the live view show UIView, UIButton, UIViewController and more.

// 1
let size = CGSize(width: 400, height: 400)
let frame = CGRect(origin: .zero, size: size)
let view = UIView(frame: frame)

// 2
PlaygroundPage.current.liveView = view

Executing playground code

To execute your code, click Execute Playground, which you’ll find below the standard editor.

Displaying the live view

When you execute your playground code, Xcode shows the live view on the right by default. If Xcode disabled the live view, click the Adjust Editor Options menu and select Live View:

Markup formatting

Markup is a computer language made for document annotations. Swift playgrounds support markup, so you can use it to document your playground, which helps organize and clarify your code.

//: # Sampling Pads
//: ## Featuring rock, jazz and pop samples.
//: ### By: Your Name

//: # Sampling Pads
//: ## Featuring rock, jazz and pop samples.
//: ### By: Your Name
/*:
 # Working with [live view](https://developer.apple.com/documentation/playgroundsupport/playgroundpage/1964506-liveview)
 ## Featuring rock, jazz and pop samples.
 ### By: Your Name
 */
//: `view` is used for experimental purposes on this page.

//: [Previous Page](@previous)
//: [Next Page](@next)
/*:
 # Table of Contents
 
 1. [Working with Live View](Working%20with%20Live%20View)
 2. [Music Button](Music%20Button)
 3. [Sampling Pad](Sampling%20Pad)

*/

Experimenting with layout

In this section, you’ll go through a layout experimentation workflow in a playground.

view.backgroundColor = .lightGray
view.backgroundColor = .blue
view.backgroundColor = .red
view.backgroundColor = .magenta

view.layer.cornerRadius = 50
view.layer.masksToBounds = true
view.layoutIfNeeded()

// 1
let label = UILabel()
label.backgroundColor = .white
view.addSubview(label)
// 2
label.translatesAutoresizingMaskIntoConstraints = false
let labelLeadingAnchorConstraint = 
  label.leadingAnchor.constraint(
    equalTo: view.leadingAnchor, 
    constant: 8)
let labelTrailingAnchorConstraint = 
  label.trailingAnchor.constraint(
    equalTo: view.trailingAnchor, 
    constant: -8)
let labelTopAnchorConstraint = 
  label.topAnchor.constraint(
    equalTo: view.topAnchor, 
    constant: 8)
labelLeadingAnchorConstraint.isActive = true
labelTrailingAnchorConstraint.isActive = true
labelTopAnchorConstraint.isActive = true
// 3
label.text = "Hello, wonderful people!"
view.layoutIfNeeded()

// 1
label.font = UIFont.systemFont(ofSize: 64, weight: .bold)
label.adjustsFontSizeToFitWidth = true
// 2
labelLeadingAnchorConstraint.constant = 24
labelTrailingAnchorConstraint.constant = -24
labelTopAnchorConstraint.constant = 24
// 3
view.layoutIfNeeded()
// 4
label.textAlignment = .center
label.backgroundColor = .clear
label.textColor = .white
label.text = "WONDERFUL PEOPLE!"

// 1
label.removeConstraint(labelTopAnchorConstraint)
// 2
let labelCenterYAnchorConstraint = 
  label.centerYAnchor.constraint(
    equalTo: view.centerYAnchor, 
    constant: -32)
labelCenterYAnchorConstraint.isActive = true
// 3
view.layoutIfNeeded()
// 4
UIView.animate(
  withDuration: 3, 
  delay: 1, 
  usingSpringWithDamping: 0.1, 
  initialSpringVelocity: 0.1, 
  options: [.curveEaseInOut, .autoreverse, .repeat], 
  animations: {
    labelCenterYAnchorConstraint.constant -= 32
    view.layoutIfNeeded()
  }, 
  completion: nil)

Creating a custom button

With structured and focused playground pages, developers visiting your playground can understand your code with less cognitive load. In this section, you’ll up your game and create a custom button: MusicButton.

let size = CGSize(width: 200, height: 300)
let frame = CGRect(origin: .zero, size: size)
let musicButton = MusicButton(frame: frame)
PlaygroundPage.current.liveView = musicButton
//: Different music genre gives `MusicButton` a different look.
musicButton.musicGenre = .rock

musicButton.musicGenre = .jazz

musicButton.musicGenre = .pop

/*:
 Each music genre is associated with an audio track from the **Resources** folder.

 You can prepare the audio player by calling `makeAudioPlayer()`.

 Afterward, you can call `play()` on the audio player to play the associated audio track`.

 */

let audioPlayer = musicButton.makeAudioPlayer()
audioPlayer?.play()

audioPlayer?.volume -= 0.5
audioPlayer?.volume += 0.5

Moving code to the Sources folder

Playgrounds allow you to share code between playground pages. In this section, you’re going to make MusicButton accessible to all playground pages.

Working with more complex custom views

When views get complex, it’s a good idea to include documentation on how a complex view works. With clear documentation, other developers can understand your views. In addition to documentation, the live view can make any custom view interactive for developers. They can test, experiment and observe empirical evidence.

final class SamplingPad: UIView {
  private var audioPlayer: AVAudioPlayer?
  
  init() {
    let size = CGSize(width: 600, height: 400)
    let frame = CGRect(origin: .zero, size: size)
    super.init(frame: frame)
  }
  
  required init?(coder aDecoder: NSCoder) {
    super.init(coder: aDecoder)
  }
  
  func set(_ audioPlayer: AVAudioPlayer) {
    audioPlayer.enableRate = true
    self.audioPlayer = audioPlayer
  }
  
  func playAudioPlayer() {
    audioPlayer?.play()
  }
  
  func update(_ volume: Float) {
    audioPlayer?.volume = volume
  }
}
//: ### Embed sampling pad and music buttons in a horizontal stack view.
let samplingPad = SamplingPad()
PlaygroundPage.current.liveView = samplingPad

let rockMusicButton = MusicButton(type: .system)
rockMusicButton.musicGenre = .rock

let jazzMusicButton = MusicButton(type: .system)
jazzMusicButton.musicGenre = .jazz

let popMusicButton = MusicButton(type: .system)
popMusicButton.musicGenre = .pop

let horizontalStackView = HorizontalStackView(arrangedSubviews:
  [rockMusicButton,
   jazzMusicButton,
   popMusicButton])
horizontalStackView.distribution = .fillEqually
//: ### Embed the horizontal stack view into a vertical stack view.
let verticalStackView = VerticalStackView(
  arrangedSubviews: [horizontalStackView])
verticalStackView.translatesAutoresizingMaskIntoConstraints =
  false
verticalStackView.axis = .vertical
samplingPad.addSubview(verticalStackView)
//: ### Set up vertical stack view layout.
let verticalSpacing: CGFloat = 16
let horizontalSpacing: CGFloat = 16
NSLayoutConstraint.activate(
  [verticalStackView.leadingAnchor.constraint(
    equalTo: samplingPad.leadingAnchor, 
    constant: horizontalSpacing),
   verticalStackView.topAnchor.constraint(
    equalTo: samplingPad.topAnchor, 
    constant: verticalSpacing),
   verticalStackView.trailingAnchor.constraint(
    equalTo: samplingPad.trailingAnchor, 
    constant: -horizontalSpacing),
   verticalStackView.bottomAnchor.constraint(
    equalTo: samplingPad.bottomAnchor, 
    constant: -verticalSpacing)])
samplingPad.layoutIfNeeded()

//: ### Create and set up layouts for volume controls.
// 1
let decreaseVolumeButton = VolumeButton(type: .system)
decreaseVolumeButton.volumeButtonType = .decrease
let increaseVolumeButton = VolumeButton(type: .system)
increaseVolumeButton.volumeButtonType = .increase
let volumeSlider = VolumeSlider()
// 2
let volumeButtonsStackView =
  HorizontalStackView(
    arrangedSubviews: [
      decreaseVolumeButton,
      increaseVolumeButton])
volumeButtonsStackView.distribution = .fillEqually
let volumeControlsStackView =
  HorizontalStackView(
    arrangedSubviews: [
      volumeSlider,
      volumeButtonsStackView])
verticalStackView.insertArrangedSubview(
  volumeControlsStackView, 
  at: 0)
// 3
volumeButtonsStackView.widthAnchor.constraint(
  equalToConstant: 120).isActive = true
samplingPad.layoutIfNeeded()

//: ### Add spacer view to `volumeControlsStackView`.
let leftSpacerView = UIView()
let rightSpacerView = UIView()
volumeControlsStackView.insertArrangedSubview(
  leftSpacerView, 
  at: 0)
volumeControlsStackView.addArrangedSubview(rightSpacerView)

//: ### Add width constraints to spacer views.
NSLayoutConstraint.activate(
  [leftSpacerView.widthAnchor.constraint(equalToConstant: 8),
   rightSpacerView.widthAnchor.constraint(
     equalTo: leftSpacerView.widthAnchor)])
samplingPad.layoutIfNeeded()

//: ### Set up `samplingPad` with  `MusicButtonDelegate`.
extension SamplingPad: MusicButtonDelegate {
  func touchesEnded(_ sender: MusicButton) {
    guard let audioPlayer = sender.makeAudioPlayer()
      else { return }
    set(audioPlayer)
    playAudioPlayer()
    volumeSlider.setValue(1, animated: true)
  }
}
//: ### Set up `SamplingPad` with  `MusicButtonDelegate` adoption.
[rockMusicButton, jazzMusicButton, popMusicButton]
  .forEach { $0.delegate = samplingPad }
// ### Update volume slider to associate with volume button action.
extension SamplingPad {
  @objc func volumeSliderValueDidChange(
    _ sender: VolumeSlider) {
    audioPlayer?.volume = sender.value
  }
}
volumeSlider.addTarget(
  samplingPad,
  action: #selector(
    SamplingPad.volumeSliderValueDidChange),
  for: .valueChanged)
// ### Set up `SamplingPad` with volume buttons.
extension SamplingPad {
  @objc func volumeButtonDidTouchUpInside(
    _ sender: VolumeButton) {
    let change: Float =
      sender.volumeButtonType == .increase ? 0.2 : -0.2
    volumeSlider.value += change
    audioPlayer?.volume = volumeSlider.value
  }
}
[increaseVolumeButton, decreaseVolumeButton].forEach {
  $0.addTarget(
    samplingPad,
    action: #selector(
      SamplingPad.volumeButtonDidTouchUpInside(_:)),
    for: .touchUpInside) }

Key points

  • Playgrounds are arguably the quickest way to go from idea to prototype.
  • In comparison to an Xcode project, playgrounds reduce the amount of boilerplate code developers must see. Developers also won’t need to build and run in the prototype phase continuously and will no longer need to risk having existing code unintentionally influencing new code and vice versa.
  • Use markup formatting to structure and format your playground content. Aim for clarity, focus, and ease of experimentation.
  • Playgrounds have a live view to display interactive content.
  • Playgrounds allow developers to run code up to a particular line; then, write and execute new code without having to stop and execute the playground entirely again.
  • Place playground assets in the Resources folder.
  • Place code you’d like to make accessible to all playground pages in the Sources folder.
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