添加链接
link管理
链接快照平台
  • 输入网页链接,自动生成快照
  • 标签化管理网页链接

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

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.