跳转至

16 Layout Prototyping with Playgrounds

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.

At the bottom of the playground page, add the following code:

// 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

With this code, you:

  1. Initialize a 400×400 UIView.
  2. Set the playground page’s live view to the initialized UIView.

Now that you’ve attached a view to the live view, you’ll learn how to execute code within a playground.

Executing playground code

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

img

To execute your code up to a certain line, hover over the line number and click or press Shift-Return.

img

With the line execution feature, you can run the code line-by-line and gain the following benefits:

  • Understanding: You can break down mysterious presentation or business logic behind lengthy code.
  • Save time: You can quickly execute either the next line, the next few lines or the remaining lines of code; you don’t have to run everything.

Now that you know how to execute code within a playground, you’ll learn how to display the live view.

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:

img

With the live view in the Assistant editor, execute the playground. You’ll see a view with a black background.

At this point, you’re ready to use markup formatting to document your playground.

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.

To tell Xcode you’re writing a line of markup, start your code comment followed by a colon, //:.

Make sure Xcode is set to show you the raw markup code. Choose Editor ▸ Show Raw Markup from the menu. Then, add the following markup text to the top of the page:

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

Here, you declare three single-line markup comments. The first has the largest heading font size. Each line that follows has a smaller font size due to the number of consecutive #.

To see the markup annotation, you need the playground to render the markup text. If you open the Editor menu, you’ll see the Show Raw Markup option has changed to Show Rendered Markup.

Afterward, your page will render something like this:

img

That’s a great start to your playground documentation, but you’ve got more work to do.

Replace the following markup text:

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

With this:

/*:
 # Working with [live view](https://developer.apple.com/documentation/playgroundsupport/playgroundpage/1964506-liveview)
 ## Featuring rock, jazz and pop samples.
 ### By: Your Name
 */

By starting with /*: and ending with */, you’ve declared multiline markup. You also added a link reference to the live view documentation.

After import PlaygroundSupport, add the following markup text:

//: `view` is used for experimental purposes on this page.

This text describes the role of view. When rendered, you see this:

img

To declare inline code, you can put the code between two backticks.

Next, add the following to the top of your playground page:

//: [Previous Page](@previous)

When you’re done with that, at the bottom of the playground page, add this:

//: [Next Page](@next)

These two markup formatting syntaxes are rather straightforward. The first link navigates to the previous playground page, and the second link navigates to the next playground page.

Open the Table of Contents page and add the following markup text:

/*:
 # Table of Contents

 1. [Working with Live View](Working%20with%20Live%20View)
 2. [Music Button](Music%20Button)
 3. [Sampling Pad](Sampling%20Pad)

*/

This block of code creates links to the specific playground pages. When adding links, always encode the playground page name according to the URI rules in RFC 3986. Essentially, replace any whitespace with %20.

Documentation is an essential part of code maintenance and sharing. Now that you know how to document your playground, it’s time to fire up your creativity and start prototyping.

Experimenting with layout

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

Open the Working with Live View page, and below PlaygroundPage.current.liveView = view, add the following code:

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

With the code above in the playground, you can see incremental changes. In this experimentation stage, suppose you want to see how view looks with different background colors. To do this, you can run the code above line-by-line, which helps you iterate through the different background colors and immediately see the results.

Execute the code you added, and your live view will look like this:

img

Below the code you already added, add this:

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

Now, let’s say you want to alter the view shape. As you execute your code line-by-line, you need to call layoutIfNeeded(). In doing so, the view knows to take the latest layout changes into effect.

Execute the code you added, and your live view now looks like this:

img

Now, you’re going to add a label to customize your view further. Add the following code:

// 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()

With this code, you:

  1. Add a label with a white background to view.
  2. Create and activate constraints programmatically, keeping references to the constraints.
  3. Set the label’s text and update any view’s pending layout changes.

Execute the code, and your live view now looks like this:

img

Notice the label is clipped. You can update the label’s position by updating the constraints. In addition to that change, you can also make a few more UI adjustments to give the label a facelift.

Next, you’re going to set up the layout and user interface for the label. Below the code you just added, add the following:

// 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!"

With this code, you:

  1. Set the label’s font and tell the label to adjust its font to fit the available width.
  2. Update the leading, trailing and top anchor constraint constants.
  3. Update any view’s pending layout changes.
  4. Update the label’s text alignment, background color, text and text color.

Execute the code, and your live view now looks like this:

img

Hmm, not bad. But suppose you want to center the label’s y position in the container and animate the vertical axis constraint. To do that, add the following code below what you’ve just added:

// 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)

With this code, you:

  1. Remove labelTopAnchorConstraint from label.
  2. Add and activate labelCenterYAnchorConstraint, which centers label in its container’s on the vertical axis.
  3. Update any view’s pending layout changes.
  4. Animate label’s vertical center constraint.

Execute the code you added, and your live view now looks like this:

img

Great! It looks like you’ve got the hang of experimentation using a playground. Next, you’ll create a custom button.

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.

Open the Music Button page. Inside this page, you’ll see MusicButton, a subclass of UIButton. MusicButton contains MusicGenre, which dictates MusicButton’s appearance and its associated audio track.

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

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

First, you initialize musicButton with a frame. You then set musicButton to the live view. After that, you create some documentation explaining the code. Finally, you set the music genre of the music button to rock.

Execute the playground, and you’ll see this:

img

When developers visit your playground, they may want to understand the effect a music genre has on MusicButton. At the same time, you may want to let developers know that the button’s music genre is what makes it special.

Below the code you added, add the following to see the jazz effect:

musicButton.musicGenre = .jazz

Execute the next line of code, and you’ll see this:

img

Now, add the following code to see the pop effect:

musicButton.musicGenre = .pop

Execute the next line of code, and you’ll see this:

img

Next, add the following code to document the audio playing feature of musicButton:

/*:
 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()

Here, you add some documentation informing developers about audio tracks, creating an audio player, and getting the audio player to play.

Execute the playground to the final line of code.

You’ll hear a song, specifically an audio track that’s associated with the button’s music genre, so make sure you turn up your sound.

To integrate assets like image, video and audio, you can drag them into the playground’s Resources folder; you’ll find that folder in the Project navigator.

img

To decrease and increase the audio player’s volume in real-time, add the following code:

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

Execute the code above line-by-line. Notice how the audio player first decreases and then increases the volume.

Now that you have a grasp on how to prototype in a playground, you’ll put what you’ve learned into creating a more fun and complex view. How about a sampling pad?

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.

Open the Music Button page. Comment out MusicButton’s class declaration.

In the playground’s main Sources folder, open MusicButton.swift, and then uncomment all of the code inside that file.

The MusicButton code you’ve uncommented is a refactored version of the previous MusicButton with two main differences:

  1. It uses public access modifiers to make the code accessible to all playground pages.
  2. There’s a new delegate property, and when touchesEnded(_:with:) triggers, it notifies that delegate.

Now that you’ve made your code reusable across all playground pages, you’ll create a sampling pad.

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.

Open the Sampling Pad page. Below the frameworks import, add the following code to begin creating the sampling pad:

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

The code declares SamplingPad, a custom UIView. SamplingPad has an audio player and methods that play sound and adjust the audio player’s volume.

Immediately below the code you just added, add the following to customize the sampling pad with music buttons:

//: ### 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

With this code, you set the live view to SamplingPad. SamplingPad initializes with the default 600×400 frame size. You then create the rock, jazz and pop music buttons. Afterward, you embed the buttons into a horizontal stack view with its distribution set to fill equally.

Next, add the following code:

//: ### Embed the horizontal stack view into a vertical stack view.
let verticalStackView = VerticalStackView(
  arrangedSubviews: [horizontalStackView])
verticalStackView.translatesAutoresizingMaskIntoConstraints =
  false
verticalStackView.axis = .vertical
samplingPad.addSubview(verticalStackView)

Here, you embed the horizontal stack view into a vertical stack view. You also prepare verticalStackView for Auto Layout, set its axis to vertical, and then add it to samplingPad.

Below that code, add the following:

//: ### 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()

This code adds and activates constraints for verticalStackView. It also gets samplingPad to update any pending layout changes.

Execute the playground, and you’ll see this:

img

Immediately below the code you already added, add the following to begin integrating VolumeButtons into the sampling pad:

//: ### 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()

With this code, you:

  1. Create an increase VolumeButton, decrease VolumeButton and VolumeSlider.
  2. Arrange the controls inside of the stack views, and insert the final stack view into verticalStackView.
  3. Give the volume buttons stack view a width. You also update any pending layout changes in samplingPad.

Execute to the last of line code, and you’ll see this:

img

Now, add the following code to customize the volume control’s stack view layout:

//: ### 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()

With this code, you push the custom volume controls inward using spacer views, 8 points from the left and the right.

Execute to the last line of code, and you’ll see this:

img

To notify the sampling pad of a MusicButton touch action, add the following code to the end of the page:

//: ### 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)
  }
}

Here, you extend SamplingPad to handle setting up the audio player for the music buttons. You also adjust the audio player’s volume back to 1 anytime a music button triggers didTouchUpInside(_:).

Add the following code to the end of the page:

//: ### Set up `SamplingPad` with  `MusicButtonDelegate` adoption.
[rockMusicButton, jazzMusicButton, popMusicButton]
  .forEach { $0.delegate = samplingPad }

This code sets the delegate of all rock, jazz and pop music buttons to samplingPad.

Execute to the last line of code. Click a music button in the live view, and you’ll hear some music playing.

You’re getting close, but you’re not done yet, so add the following code to the end of the page:

// ### 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)

Now, when you slide the volume knob across the slider, the sample pad’s audio player will adjust its volume accordingly.

Next, add the following code to the end of the page:

// ### 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
  }
}

Here, you extend SamplingPad to make VolumeButton increase or decrease the volumeSlider’s value and audioPlayer’s volume. The change in volume and slider is either an increase or decrease of 0.2 depending on the volume button’s type.

Below the code you just added, add the following:

[increaseVolumeButton, decreaseVolumeButton].forEach {
  $0.addTarget(
    samplingPad,
    action: #selector(
      SamplingPad.volumeButtonDidTouchUpInside(_:)),
    for: .touchUpInside) }

This code sets the volume button’s delegate to samplingPad, which enables the volume increase/decrease features using the plus or minus volume buttons in samplingPad.

Finally, execute to the last line of code — and there you have it, your own sampling pad. Go crazy, mix-up some sick tracks and blow some minds!

You’ve gone a long way in this chapter. You started by prototyping a simple magenta view. You then took what you learned early on and developed a sampling pad — and you’ve done it all using playgrounds.

Playgrounds help you get your ideas quickly off the ground. They also let you run code line-by-line, get responsive and immediate feedback and help you to create wonderful documentation for your code, which is perhaps one of the most overlooked gems in a development process. Documentation is vital for clear communication, productivity, code sharing.

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.