Swift Talk # 51

iOS at Kickstarter: Playground-Driven Development

with special guest Brandon Williams

This episode is freely available thanks to the support of our subscribers

Subscribers get exclusive access to new and all previous subscriber-only episodes, video downloads, and 30% discount for team members. Become a Subscriber

Brandon from Kickstarter demos how the company uses playgrounds to prototype and style individual view controllers.

00:06 Today we're joined by Brandon from Kickstarter once again. Together we'll build an interface in a playground and then move it into a framework that can be used in an actual application. At Kickstarter, the developers only construct a basic view hierarchy in Interface Builder, whereas all of its styling is done in a playground.

00:47 We've prepared a view controller that will serve as a sign-up form. It features a header, three input fields, and a submit button, all contained in a root UIStackView:

import UIKit
import PlaygroundSupport

class MyViewController: UIViewController {
  let rootStackView = UIStackView()
  let titleLabel = UILabel()
  let emailLabel = UILabel()
  let emailTextField = UITextField()
  let emailStackView = UIStackView()
  let nameLabel = UILabel()
  let nameTextField = UITextField()
  let nameStackView = UIStackView()
  let passwordLabel = UILabel()
  let passwordTextField = UITextField()
  let passwordStackView = UIStackView()
  let submitButton = UIButton()

  override func viewDidLoad() {
    super.viewDidLoad()

    self.view.backgroundColor = UIColor(white: 0.95, alpha: 1)

    self.rootStackView.translatesAutoresizingMaskIntoConstraints = false
    self.rootStackView.axis = .vertical
    
    self.titleLabel.text = "Sign up"
    self.titleLabel.textAlignment = .center
    self.titleLabel.textColor = UIColor.init(white: 0.2, alpha: 1)

    self.nameLabel.text = "Name"
    self.nameStackView.axis = .vertical
    self.nameStackView.addArrangedSubview(self.nameLabel)
    self.nameStackView.addArrangedSubview(self.nameTextField)

    self.emailLabel.text = "Email"
    self.emailStackView.axis = .vertical
    self.emailStackView.addArrangedSubview(self.emailLabel)
    self.emailStackView.addArrangedSubview(self.emailTextField)

    self.passwordLabel.text = "Password"
    self.passwordStackView.axis = .vertical
    self.passwordStackView.addArrangedSubview(self.passwordLabel)
    self.passwordStackView.addArrangedSubview(self.passwordTextField)

    self.submitButton.setTitle("Submit", for: .normal)
    
    self.view.addSubview(self.rootStackView)
    self.rootStackView.addArrangedSubview(self.titleLabel)
    self.rootStackView.addArrangedSubview(self.nameStackView)
    self.rootStackView.addArrangedSubview(self.emailStackView)
    self.rootStackView.addArrangedSubview(self.passwordStackView)
    self.rootStackView.addArrangedSubview(self.submitButton)

    NSLayoutConstraint.activate([
      self.rootStackView.leadingAnchor.constraint(equalTo: self.view.leadingAnchor),
      self.rootStackView.trailingAnchor.constraint(equalTo: self.view.trailingAnchor),
      self.rootStackView.topAnchor.constraint(equalTo: self.view.topAnchor),
      self.rootStackView.bottomAnchor.constraint(lessThanOrEqualTo: self.view.bottomAnchor),
      ])
  }
}

01:23 We import PlaygroundSupport and set the view controller's view as the playground's live view in order to immediately see any changes we make:

let vc = MyViewController()
PlaygroundPage.current.liveView = vc

Styling the View Controller

01:36 Our first improvement of the view controller is to style the submit button, because it's barely visible now. To do this, we'll give it a background color and make the corners rounded. These are pretty simple changes, but it's nice to see the live view updating. In viewDidLoad, we add the following:

self.submitButton.backgroundColor = .blue
self.submitButton.layer.cornerRadius = 6
self.submitButton.layer.masksToBounds = true

02:25 Next we'll tweak the form's text field labels to improve the UI's hierarchy, because it'd be better if the input labels' fonts are smaller than the title font. Before, we'd set the font size explicitly with UIFont.systemFont(ofSize:), but it's better to use UIFont.preferredFont(forTextStyle:) and choose a semantic text style:

self.nameLabel.font = UIFont.preferredFont(forTextStyle: .caption1)

03:33 With another overload of this method, we can request the font that's suited to the view controller's current trait collection, so it'll adapt to screen size, orientation, accessibility settings, etc.:

self.nameLabel.font = UIFont.preferredFont(forTextStyle: .caption1, compatibleWith: self.traitCollection)

04:23 We give the text fields a border style:

self.nameTextField.borderStyle = .roundedRect

05:15 The form elements are too tight. The rootStackView holds all the elements, so we can simply set spacing on it:

self.rootStackView.spacing = 16

06:00 We paste in the remaining styles: a preferred font for the title label and the submit button, and a different text color for the button's highlighted state. Because we're working in a playground, we can actually click the button and see it changing colors:

self.submitButton.titleLabel?.font = UIFont.preferredFont(forTextStyle: .callout, compatibleWith: self.traitCollection)
self.submitButton.setTitleColor(UIColor(white: 1, alpha: 0.5), for: .highlighted)

Testing Trait Collections

06:20 The real power of working in a playground is the ability to dynamically swap the view controller's trait collection. This allows us to test the view controller with the characteristics of different devices and orientations. We wrap the view controller in a parent view controller, which can override the trait collection. The playground's live view should now be the parent view. We also paste in some standard layout code, making the child view fill the parent view:

let parent = UIViewController()
let vc = MyViewController()
parent.addChildViewController(vc)

parent.view.translatesAutoresizingMaskIntoConstraints = false
parent.view.addSubview(vc.view)

NSLayoutConstraint.activate([
    vc.view.leadingAnchor.constraint(equalTo: parent.view.leadingAnchor),
    vc.view.trailingAnchor.constraint(equalTo: parent.view.trailingAnchor),
    vc.view.topAnchor.constraint(equalTo: parent.view.topAnchor),
    vc.view.bottomAnchor.constraint(equalTo: parent.view.bottomAnchor)
    ])

PlaygroundPage.current.liveView = parent

07:18 We'll let the parent view controller feed a trait collection to the child view controller. We can initialize a UITraitCollection for a particular trait, like contentSizeCategory, sizeClass, or forceTouchCapability. We can also pass in an array of trait collections, which is composed to emulate a specific device. We create a trait collection emulating an iPhone 6:

let traits = UITraitCollection(
    traitsFrom: [
        UITraitCollection(verticalSizeClass: .regular),
        UITraitCollection(horizontalSizeClass: .compact),
        UITraitCollection(preferredContentSizeCategory: .extraExtraLarge)
    ]
)

parent.setOverrideTraitCollection(traits, forChildViewController: vc)

09:08 Finally, we can set the parent's preferred content size, which will be useful later when we want to emulate an iPad with a larger size:

parent.preferredContentSize = .init(width: 320, height: 568)

10:07 The developers at Kickstarter wrote a helper method called playgroundWrapper. This method takes a child view controller and some parameters and returns a parent view controller configured with the correct size and trait collection. We simply use a couple enums to describe the configuration we want to emulate:

let anotherVc = MyViewController()
let anotherParent = playgroundWrapper(child: anotherVc, device: .phone4inch, orientation: .portrait, contentSizeCategory: .accessibilityExtraExtraExtraLarge)

PlaygroundPage.current.liveView = anotherParent

12:10 It's easy to change parameters and test a smaller contentSizeCategory or set the device to .pad. The live view automatically updates with the correct layout.

Using a Framework

12:58 In order to use this code in an actual app, we need to create a framework. In the project settings in Xcode, we add a new Cocoa Touch framework to the targets.

13:46 This adds a directory to the project outline. We've prepared a view controller and a storyboard based on the code above, so we drag these two files into the framework directory and make sure to "Copy items if needed."

14:12 We've prepared a version of the signup view controller class with outlets for the UI components and everything made public. The storyboard has the most basic setup of the view hierarchy, without styling or even names put in the labels, which is something that's all done in the view controller.

14:43 Now we want to use the framework's view controller from within the playground, but before we can do so, we have to build the framework.

14:58 We can now import the framework in a blank playground page. We need access to the Storyboard, from which we'll instantiate the signup view controller. We can initialize a UIStoryboard from both its name and a Bundle:

import UIKit
import PlaygroundSupport
import MyFramework

let bundle = Bundle(for: StoryboardSignupViewController.self)
let storyboard = UIStoryboard(name: "StoryboardSignupViewController", bundle: bundle)

let vc = storyboard.instantiateInitialViewController()!

16:21 We use the playground helper to create a parent, which will be shown in the playground's live view:

let parent = playgroundWrapper(child: vc, device: .phone4_7inch, orientation: .landscape, contentSizeCategory: .large)
PlaygroundPage.current.liveView = parent

17:23 As an additional UI element, the storyboard has a disclaimer label for terms and conditions. We can use this to see how styling works in the current setup. We go back into the framework's view controller and style the label:

self.disclaimerLabel.font = UIFont.preferredFont(forTextStyle: .caption2, compatibleWith: self.traitCollection)
self.disclaimerLabel.textAlignment = .center

18:09 With that, we build the framework. Switching back to the playground, we see the updated layout of the view controller. It's important to remember to build the framework so that the playground has the newest code.

Kickstarter Example

18:44 Kickstarter does playground-driven development in its real app. By using this technique, Kickstarter can provide some entry points to customizing interfaces. This allows project managers or designers who haven't worked on the interface to test and play with the view controller while tweaking its parameters.

19:03 We look at a playground that shows off the user's dashboard of the Kickstarter app, which features various stats of the user's projects. The playground has some top-level variables that can be used to see how different parameters affect the interface:

19:51 It's even possible to switch the UI language — e.g. to German, which typically has words which are 30 percent longer than in English — in order to quickly check how the interface behaves in different localizations.

20:49 Playground-driven development allows us to see how the interface works in various configurations. As it's interactive, we can click our interface's buttons in the playground and see the various button states.

20:56 We've been playing around with playground-driven development and we're impressed with how smart and fun it is.

Resources

  • Sample Project

    Written in Swift 3.1

  • Episode Video

    Become a subscriber to download episode videos.

In Collection

See All Collections

Episode Details

Recent Episodes

See All

Unlock Full Access

Subscribe to Swift Talk

  • Watch All Episodes

    A new episode every week

  • icon-benefit-download Created with Sketch.

    Download Episodes

    Take Swift Talk with you when you're offline

  • Support Us

    With your help we can keep producing new episodes