1. Introduction

过去的两集有点抽象。 我们一直在日常代码中以一种看似无关的方式探索合成。让我们看看如何将合成应用到更具体的东西上:UIKit。

import UIKit

final class SignInViewController: UIViewController {
  override func viewDidLoad() {
    super.viewDidLoad()

    self.view.backgroundColor = .white

    let gradientView = GradientView()
    gradientView.fromColor = UIColor(red: 0.5, green: 0.85, blue: 1, alpha: 0.85)
    gradientView.toColor = .white
    gradientView.translatesAutoresizingMaskIntoConstraints = false

    let logoImageView = UIImageView(image: UIImage(named: "logo"))
    logoImageView.widthAnchor.constraint(equalTo: logoImageView.heightAnchor, multiplier: logoImageView.frame.width / logoImageView.frame.height).isActive = true

    let gitHubButton = UIButton(type: .system)
    gitHubButton.contentEdgeInsets = UIEdgeInsets(top: 12, left: 16, bottom: 12, right: 16)
    gitHubButton.titleLabel?.font = .systemFont(ofSize: 16, weight: .medium)
    gitHubButton.clipsToBounds = true
    gitHubButton.layer.cornerRadius = 6
    gitHubButton.backgroundColor = .black
    gitHubButton.tintColor = .white
    gitHubButton.imageEdgeInsets = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 16)
    gitHubButton.setImage(UIImage(named: "github"), for: .normal)
    gitHubButton.setTitle("Sign in with GitHub", for: .normal)

    let orLabel = UILabel()
    orLabel.font = .systemFont(ofSize: 14, weight: .medium)
    orLabel.textAlignment = .center
    orLabel.textColor = UIColor(white: 0.625, alpha: 1)
    orLabel.text = "or"

    let emailField = UITextField()
    emailField.clipsToBounds = true
    emailField.layer.cornerRadius = 6
    emailField.layer.borderColor = UIColor(white: 0.75, alpha: 1).cgColor
    emailField.layer.borderWidth = 1
    emailField.borderStyle = .roundedRect
    emailField.heightAnchor.constraint(equalToConstant: 44).isActive = true
    emailField.keyboardType = .emailAddress
    emailField.placeholder = "blob@pointfree.co"

    let passwordField = UITextField()
    passwordField.clipsToBounds = true
    passwordField.layer.cornerRadius = 6
    passwordField.layer.borderColor = UIColor(white: 0.75, alpha: 1).cgColor
    passwordField.layer.borderWidth = 1
    passwordField.borderStyle = .roundedRect
    passwordField.heightAnchor.constraint(equalToConstant: 44).isActive = true
    passwordField.isSecureTextEntry = true
    passwordField.placeholder = "••••••••••••••••"

    let signInButton = UIButton(type: .system)
    signInButton.contentEdgeInsets = UIEdgeInsets(top: 12, left: 16, bottom: 12, right: 16)
    signInButton.titleLabel?.font = .systemFont(ofSize: 16, weight: .medium)
    signInButton.clipsToBounds = true
    signInButton.layer.cornerRadius = 6
    signInButton.layer.borderColor = UIColor.black.cgColor
    signInButton.layer.borderWidth = 2
    signInButton.setTitleColor(.black, for: .normal)
    signInButton.setTitle("Sign in", for: .normal)

    let forgotPasswordButton = UIButton(type: .system)
    forgotPasswordButton.contentEdgeInsets = UIEdgeInsets(top: 12, left: 16, bottom: 12, right: 16)
    forgotPasswordButton.titleLabel?.font = .systemFont(ofSize: 16, weight: .medium)
    forgotPasswordButton.setTitleColor(.black, for: .normal)
    forgotPasswordButton.setTitle("I forgot my password", for: .normal)

    let legalLabel = UILabel()
    legalLabel.font = .systemFont(ofSize: 11, weight: .light)
    legalLabel.numberOfLines = 0
    legalLabel.textAlignment = .center
    legalLabel.textColor = UIColor(white: 0.5, alpha: 1)
    legalLabel.text = "By signing into Point-Free you agree to our latest terms of use and privacy policy."

    let rootStackView = UIStackView(arrangedSubviews: [
      logoImageView,
      gitHubButton,
      orLabel,
      emailField,
      passwordField,
      signInButton,
      forgotPasswordButton,
      legalLabel,
      ])

    rootStackView.axis = .vertical
    rootStackView.isLayoutMarginsRelativeArrangement = true
    rootStackView.layoutMargins = UIEdgeInsets(top: 32, left: 16, bottom: 32, right: 16)
    rootStackView.spacing = 16
    rootStackView.translatesAutoresizingMaskIntoConstraints = false

    self.view.addSubview(gradientView)
    self.view.addSubview(rootStackView)

    NSLayoutConstraint.activate([
      gradientView.topAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.topAnchor),
      gradientView.leadingAnchor.constraint(equalTo: self.view.leadingAnchor),
      gradientView.trailingAnchor.constraint(equalTo: self.view.trailingAnchor),
      gradientView.bottomAnchor.constraint(equalTo: self.view.centerYAnchor),

      rootStackView.topAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.topAnchor),
      rootStackView.leadingAnchor.constraint(equalTo: self.view.leadingAnchor),
      rootStackView.trailingAnchor.constraint(equalTo: self.view.trailingAnchor),
      ])
  }
}

我们在构建UI时遇到的一个常见问题是可重用样式。这段代码还没有尝试让样式可重用。它只是在实例化后内联地配置每个视图。 很多组件都有重叠的样式,所以让我们尝试找到一种更好的方式来样式化组件,而不会有太多重复。

2. UIAppearance

首先,让我们探讨一下UIAppearanceApple提供的帮助解决可重用样式的API。UIAppearance是一个带有一些静态方法的协议,主要是一个叫做appearance的方法。这些方法返回一个可以像配置常规视图一样配置的代理视图。一旦配置了这个代理,添加到视图层次结构中的任何同类视图都将以同样的方式进行配置。

我们所有的按钮都有相同的内容边框insets和字体,所以让我们尝试移动这个配置来使用UIAppearance

UIButton.appearance().contentEdgeInsets = UIEdgeInsets(top: 12, left: 16, bottom: 12, right: 16)
UIButton.appearance().titleLabel?.font = .systemFont(ofSize: 16, weight: .medium)

如果我们从按钮配置中删除相应的行,事情看起来基本上是一样的。

let gitHubButton = UIButton(type: .system)
// gitHubButton.contentEdgeInsets = UIEdgeInsets(top: 12, left: 16, bottom: 12, right: 16)
// gitHubButton.titleLabel?.font = .systemFont(ofSize: 16, weight: .medium)

// …

let loginButton = UIButton(type: .system)
// loginButton.contentEdgeInsets = UIEdgeInsets(top: 12, left: 16, bottom: 12, right: 16)
// loginButton.titleLabel?.font = .systemFont(ofSize: 16, weight: .medium)

// …

但是我们注意到一个区别:字体小了一点。 这是怎么了? 我们使用.system按钮类型,它为按钮标题提供了一些很好的样式,这可能会碍事。 不过,仔细检查一下,我们通过代理对象深入了两层,试图通过titlelabel标签更新字体。UIAppearance的改变只对直接视图的属性起作用。 一个解决方案是扩展我们的视图并将此配置作为一个新属性公开,但这需要一点工作。

UIAppearance的另一个问题是它在类级别上工作。 我们已经达到了UIButton可重用配置的极限,所以为了构建更多种类的可重用按钮,我们的主要路径是子类化。

3. Subclassing

子类化是在整个应用程序中管理可重用样式的流行方法。让我们首先定义一个基按钮类,并覆盖它的初始化式来样式化它。

class BaseButton: UIButton {
  override init(frame: CGRect) {
    super.init(frame: frame)
    self.contentEdgeInsets = UIEdgeInsets(top: 12, left: 16, bottom: 12, right: 16)
    self.titleLabel?.font = .systemFont(ofSize: 16, weight: .medium)
  }

  required init?(coder aDecoder: NSCoder) {
    fatalError("init(coder:) has not been implemented")
  }
}

编译器还强迫我们定义那个额外的初始化式。

现在我们可以将按钮实例化为BaseButton

let gitHubButton = BaseButton(type: .system)
// …

let loginButton = BaseButton(type: .system)
// …

但是字体看起来又不对了。按钮子类和.system类型不兼容,所以我们现在要放弃从那个按钮类型得到的东西。

let gitHubButton = BaseButton()
// …

let loginButton = BaseButton()
// …

现在字体已经正常呈现了。

我们现在直接使用这个BaseButton类,但每当我们在类名前加上Base前缀时,我们通常是在处理一些抽象的功能,这些功能会进一步子类化以供实际使用。

我们在屏幕上看到的更直接的按钮样式是什么? 我们有一个“填充”按钮样式,一个“边框”按钮样式,和一个“文本”按钮样式。让我们从定义一个fillledbutton子类开始。

class FilledButton: BaseButton {
  override init(frame: CGRect) {
    super.init(frame: frame)
    self.backgroundColor = .black
    self.tintColor = .white
  }

  required init?(coder aDecoder: NSCoder) {
    fatalError("init(coder:) has not been implemented")
  }
}

同样,我们需要提供额外的*init(coder:)*初始化式来满足编译器的要求,但是现在我们有了一个可以使用的新类。

let gitHubButton = FilledButton()
// gitHubButton.contentEdgeInsets = UIEdgeInsets(top: 12, left: 16, bottom: 12, right: 16)
// gitHubButton.titleLabel?.font = .systemFont(ofSize: 16, weight: .medium)
gitHubButton.clipsToBounds = true
gitHubButton.layer.cornerRadius = 6
// gitHubButton.backgroundColor = .black
// gitHubButton.tintColor = .white

这些样式通过重用被组合在一起,看起来我们在这里跳过了。 我们有几行环绕按钮边缘的线,与登录按钮共享。听起来我们需要另一个子类。

class RoundedButton: BaseButton {
  override init(frame: CGRect) {
    super.init(frame: frame)
    self.clipsToBounds = true
    self.layer.cornerRadius = 6
  }

  required init?(coder aDecoder: NSCoder) {
    fatalError("init(coder:) has not been implemented")
  }
}

现在我们不得不考虑操作的顺序。FillledbuttonRoundedButton都继承自BaseButton。现在,Fillledbutton应该继承RoundedButton以确保填充的按钮是圆角的。

class FilledButton: RoundedButton {
  // …
}

我们可以继续沿着这些行定义BorderButtonTextButton子类,并找出它们在层次结构中的位置。在这个小屏幕上很容易找到它们的位置,但随着时间的推移,我们会发现我们有需要共享相同逻辑的分离的子类。

这就是菱形继承问题:给定一个有两个子类的基类,我们可能想要另一个继承这两个子类的子类。 Swift不支持菱形继承,所以让我们探索一些其他共享逻辑的替代方法。

4. Object Composition

在我们的行业里有句古老的格言:“比起继承,更喜欢组合。(prefer composition over inheritance)”在这个意义上,“合成”是什么意思?

我们不使用子类,而是定义一个静态属性来创建具有基样式的按钮。

extension UIButton {
  static var base: UIButton {
    let button = UIButton()
    button.contentEdgeInsets = UIEdgeInsets(top: 12, left: 16, bottom: 12, right: 16)
    button.titleLabel?.font = .systemFont(ofSize: 16, weight: .medium)
    return button
  }
}

我们可以为填充按钮样式创建另一个helper。

extension UIButton {
  static var filled: UIButton {
    let button = self.base
    button.backgroundColor = .black
    button.tintColor = .white
    return button
  }
}

这就是“composition”的作用所在。我们在进一步应用filled 样式之前,可以调用self.base使用我们的基本样式派生一个按钮。

然后我们可以添加一个圆形样式。

extension UIButton {
  static var rounded: UIButton {
    let button = self.filled
    button.clipsToBounds = true
    button.layer.cornerRadius = 6
    return button
  }
}

现在我们的GitHub按钮可以使用这些静态属性之一而不是子类。

let gitHubButton = UIButton.filled

哎呀! 我们的按钮是填充的,但不是四舍五入的。发生了什么事? 看来我们的行动顺序错了。看起来这个版本有与继承相同的问题,尽管它仍然更好一些。直接在UIButton上定义样式绝对更简洁,我们不需要定义那些嘈杂的*init(coder:)*初始化器。让我们看看是否能找到一种不那么混乱的方式来构建我们的样式。

5. Functions

让我们回到我们熟悉的老朋友,函数。我们可以定义一个baseButtonStyle函数,用我们的基样式给给定的按钮设置样式。

func baseButtonStyle(_ button: UIButton) {
  button.contentEdgeInsets = UIEdgeInsets(top: 12, left: 16, bottom: 12, right: 16)
  button.titleLabel?.font = .systemFont(ofSize: 16, weight: .medium)
}

又好又简洁。

What about a filledButtonStyle?

func filledButtonStyle(_ button: UIButton)  {
  button.backgroundColor = .black
  button.tintColor = .white
}

现在我们的填充样式不需要继承或调用基样式。

How about a roundedButtonStyle?

func roundedButtonStyle(_ button: UIButton)  {
  button.clipsToBounds = true
  button.layer.cornerRadius = 6
}

我们有! 三个样式函数不需要担心它们之间的继承或调用层次结构。他们只做一件事,而且做得很好。让我们使用它们。

let gitHubButton = UIButton(type: .system)
baseButtonStyle(gitHubButton)
roundedButtonStyle(gitHubButton)
filledButtonStyle(gitHubButton)

这看起来有点乱。在这个配置中有很多活动部件,似乎很容易错过一个步骤。我们还直接应用了baseButtonStyle,这看起来像是我们想让其他样式使用的东西。如果能够将这些风格组合成不那么随意的单位就好了。

6. Function Composition

在我们的“Side Effects”一集中,我们介绍了<>用于单一类型的合成。我们定义了两次:一次用于接受As作为输入并返回As作为输出的函数,一次用于从inout A到Void的值突变函数。

func <> <A>(f: @escaping (A) -> A, g: @escaping (A) -> A) -> (A) -> A {
  return f >>> g
}
func <> <A>(f: @escaping (inout A) -> Void, g: @escaping (inout A) -> Void) -> (inout A) -> Void {
  return { a in
    f(&a)
    g(&a)
  }
}

我们的样式函数生活在引用类型突变的世界里,所以让我们看看是否可以重用这个*(inout A) -> Void*形状。

func <> <A: AnyObject>(f: @escaping (A) -> Void, g: @escaping (A) -> Void) -> (A) -> Void {
  return { a in
    f(a)
    g(a)
  }
}

我们将泛型A限制为AnyObject,这是所有引用类型都遵循的协议。在这个过程中,我们可以删除inout注释。

现在我们可以从我们的baseButtonStyle组合我们的fillledbuttonstyle

let filledButtonStyle =
  baseButtonStyle
    <> {
      $0.backgroundColor = .black
      $0.tintColor = .white
}

让我们对roundedButtonStyle做同样的事情

let roundedButtonStyle =
  baseButtonStyle
    <> {
      $0.clipsToBounds = true
      $0.layer.cornerRadius = 6
}

我们的GitHub按钮不再需要配置baseButtonStyle。 让我们最终决定样式的层次结构,让fillledbuttonstyleroundedButtonStyle组成。

let filledButtonStyle =
  roundedButtonStyle
    <> {
      $0.backgroundColor = .black
      $0.tintColor = .white
}

这种构图感觉起来灵活多了。我们可以快速混合、匹配和提取样式,每个函数本身只负责它的样式,不需要调用其他样式函数: 我们通过函数组合来组合样式。

我们现在还可以探索可重用的样式,这在类和静态属性世界中是不可能的! 我们的roundedButtonStyle是一个函数,它可以修改存在于所有UIView上的属性,而不仅仅是按钮。事实上,我们的UITextFields也是以同样的方式配置的。

我们现在可以编写一个在所有UIView上工作的rounddedstyle

let roundedStyle: (UIView) -> Void = {
  $0.clipsToBounds = true
  $0.layer.cornerRadius = 6
}

而我们的roundedButtonStyle只需要用这个更可重用的功能来组合。

let roundedButtonStyle =
  baseButtonStyle
    <> roundedStyle

我们可以通过创建baseTextFieldStyle来重用这个roundedStyle

let baseTextFieldStyle: (UITextField) -> Void =
  roundedStyle
    <> {
      $0.layer.borderColor = UIColor(white: 0.75, alpha: 1).cgColor
      $0.layer.borderWidth = 1
      $0.borderStyle = .roundedRect
      $0.heightAnchor.constraint(equalToConstant: 44).isActive = true
}

我们可以把这个样式应用到两个文本框中,然后去掉很多代码。

let emailTextField = UITextField()
baseTextFieldStyle(emailTextField)
// …

let passwordTextField = UITextField()
baseTextFieldStyle(passwordTextField)
// …

更多的重用是通过观察来实现的:我们的baseTextFieldStyle以与边框按钮相同的方式配置边框。 我们应该能够编写一个双方都可以使用的borderStyle

如果我们以非常基本的方式编写样式函数,就会遇到一个问题:

let borderStyle: (UIView) -> Void = {
  $0.layer.borderColor = // ???
  $0.layer.borderWidth = // ???
}

这里的配置在每种情况下都是不同的:我们的边框按钮有一个粗的黑色边框,而文本框有一个细的灰色边框。 让我们把这个配置带入函数。

func borderStyle(color: UIColor, width: CGFloat) -> (UIView) -> Void {
  return { view in
    view.layer.borderColor = color.cgColor
    view.layer.borderWidth = width
  }
}

为了进行组合,我们需要我们的函数先进行配置,然后返回一个全新的函数来样式化视图。

让我们试着用baseTextFieldStyle:

let baseTextFieldStyle: (UITextField) -> Void =
  roundedStyle
    <> borderStyle(color: UIColor(white: 0.75, alpha: 1), width: 1)
    <> {
      $0.borderStyle = .roundedRect
      $0.heightAnchor.constraint(equalToConstant: 44).isActive = true
}

我们有个小问题。

Value of type 'UIView' has no member 'borderStyle'

我们组合了一堆小函数,这些小函数通常会将UIView样式转换成一个函数,该函数特别需要UITextFields,类型系统已经达到了子类型、超类型和多行闭包之间的推断限制。这种错误应该很少见,但很容易修复。我们需要显式地使用闭包的类型签名。

let baseTextFieldStyle: (UITextField) -> Void =
  roundedStyle
    <> borderStyle(color: UIColor(white: 0.75, alpha: 1), width: 1)
    <> { (tf: UITextField) in
      tf.borderStyle = .roundedRect
      tf.heightAnchor.constraint(equalToConstant: 44).isActive = true
}

这很有效。

现在我们有了一个borderStyle,可以很容易地创建一个borderButtonStyle!

let borderButtonStyle =
  roundedStyle
    <> borderStyle(color: .black, width: 2)

这是在任何UIView上流畅地使用样式函数的另一个例子。我们已经能够用diamond <>来解决我们的diamond继承问题。

如果我们进一步为所有东西编写样式函数,它会是什么样子?

// base

func autolayoutStyle<V: UIView>(_ view: V) -> Void {
  view.translatesAutoresizingMaskIntoConstraints = false
}

func aspectRatioStyle<V: UIView>(size: CGSize) -> (V) -> Void {
  return {
    $0.widthAnchor
      .constraint(equalTo: $0.heightAnchor, multiplier: size.width / size.height)
      .isActive = true
  }
}

func implicitAspectRatioStyle<V: UIView>(_ view: V) -> Void {
  aspectRatioStyle(size: view.frame.size)(view)
}

func roundedRectStyle<View: UIView>(_ view: View) {
  view.clipsToBounds = true
  view.layer.cornerRadius = 6
}

// buttons

let baseButtonStyle: (UIButton) -> Void = {
  $0.contentEdgeInsets = UIEdgeInsets(top: 12, left: 16, bottom: 12, right: 16)
  $0.titleLabel?.font = .systemFont(ofSize: 16, weight: .medium)
}

let roundedButtonStyle =
  baseButtonStyle
    <> roundedRectStyle

let filledButtonStyle =
  roundedButtonStyle
    <> {
      $0.backgroundColor = .black
      $0.tintColor = .white
}

let borderButtonStyle =
  roundedButtonStyle
    <> {
      $0.layer.borderColor = UIColor.black.cgColor
      $0.layer.borderWidth = 2
      $0.setTitleColor(.black, for: .normal)
}

let textButtonStyle =
  baseButtonStyle <> {
    $0.setTitleColor(.black, for: .normal)
}

let imageButtonStyle: (UIImage?) -> (UIButton) -> Void = { image in
  return {
    $0.imageEdgeInsets = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 16)
    $0.setImage(image, for: .normal)
  }
}

let gitHubButtonStyle =
  filledButtonStyle
    <> imageButtonStyle(UIImage(named: "github"))

// text fields

let baseTextFieldStyle: (UITextField) -> Void =
  roundedRectStyle
    <> {
      $0.borderStyle = .roundedRect
      $0.heightAnchor.constraint(equalToConstant: 44).isActive = true
      $0.layer.borderColor = UIColor(white: 0.75, alpha: 1).cgColor
      $0.layer.borderWidth = 1
}

let emailTextFieldStyle =
  baseTextFieldStyle
    <> {
      $0.keyboardType = .emailAddress
      $0.placeholder = "blob@pointfree.co"
}

let passwordTextFieldStyle =
  baseTextFieldStyle
    <> {
      $0.isSecureTextEntry = true
      $0.placeholder = "••••••••••••••••"
}

// labels

func fontStyle(ofSize size: CGFloat, weight: UIFont.Weight) -> (UILabel) -> Void {
  return {
    $0.font = .systemFont(ofSize: size, weight: weight)
  }
}

func textColorStyle(_ color: UIColor) -> (UILabel) -> Void {
  return {
    $0.textColor = color
  }
}

let centerStyle: (UILabel) -> Void = {
  $0.textAlignment = .center
}

// hyper-local

let orLabelStyle: (UILabel) -> Void =
  centerStyle
    <> fontStyle(ofSize: 14, weight: .medium)
    <> textColorStyle(UIColor(white: 0.625, alpha: 1))

let finePrintStyle: (UILabel) -> Void =
  centerStyle
    <> fontStyle(ofSize: 14, weight: .medium)
    <> textColorStyle(UIColor(white: 0.5, alpha: 1))
    <> {
      $0.font = .systemFont(ofSize: 11, weight: .light)
      $0.numberOfLines = 0
}

let gradientStyle: (GradientView) -> Void =
  autolayoutStyle <> {
    $0.fromColor = UIColor(red: 0.5, green: 0.85, blue: 1, alpha: 0.85)
    $0.toColor = .white
}

// stack views

let rootStackViewStyle: (UIStackView) -> Void =
  autolayoutStyle
    <> {
      $0.axis = .vertical
      $0.isLayoutMarginsRelativeArrangement = true
      $0.layoutMargins = UIEdgeInsets(top: 32, left: 16, bottom: 32, right: 16)
      $0.spacing = 16
}

final class SignInViewController: UIViewController {
  override func viewDidLoad() {
    super.viewDidLoad()

    self.view.backgroundColor = .white

    let gradientView = GradientView()
    gradientStyle(gradientView)

    let logoImageView = UIImageView(image: UIImage(named: "logo"))
    implicitAspectRatioStyle(logoImageView)

    baseButtonStyle(.appearance())

    let gitHubButton = UIButton(type: .system)
    gitHubButton.setTitle("Sign in with GitHub", for: .normal)
    gitHubButtonStyle(gitHubButton)

    let orLabel = UILabel()
    orLabelStyle(orLabel)
    orLabel.text = "or"

    let emailField = UITextField()
    emailTextFieldStyle(emailField)

    let passwordField = UITextField()
    passwordTextFieldStyle(passwordField)

    let signInButton = UIButton(type: .system)
    signInButton.setTitle("Sign in", for: .normal)
    borderButtonStyle(signInButton)

    let forgotPasswordButton = UIButton(type: .system)
    forgotPasswordButton.setTitle("I forgot my password", for: .normal)
    textButtonStyle(forgotPasswordButton)

    let legalLabel = UILabel()
    legalLabel.text = "By signing into Point-Free you agree to our latest terms of use and privacy policy."
    finePrintStyle(legalLabel)

    let rootStackView = UIStackView(arrangedSubviews: [
      logoImageView,
      gitHubButton,
      orLabel,
      emailField,
      passwordField,
      signInButton,
      forgotPasswordButton,
      legalLabel,
      ])
    rootStackViewStyle(rootStackView)

    self.view.addSubview(gradientView)
    self.view.addSubview(rootStackView)

    NSLayoutConstraint.activate([
      gradientView.topAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.topAnchor),
      gradientView.leadingAnchor.constraint(equalTo: self.view.leadingAnchor),
      gradientView.trailingAnchor.constraint(equalTo: self.view.trailingAnchor),
      gradientView.bottomAnchor.constraint(equalTo: self.view.centerYAnchor),

      rootStackView.topAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.topAnchor),
      rootStackView.leadingAnchor.constraint(equalTo: self.view.leadingAnchor),
      rootStackView.trailingAnchor.constraint(equalTo: self.view.trailingAnchor),
      ])
  }
}

我们能够衍生出许多有趣的、可重用的风格! 我们有一个autolayoutStyle,它可能比translatesAutoresizingMaskIntoConstraints = false更容易阅读和记忆。我们有一个aspectRatioStyle维护图像视图的纵横比与自动布局。

我们有一个imageButtonStyle,它可以根据我们的需要用borderButtonStylefillledbuttonstyle来组合。

我们也有一个rootStackViewStyle,允许我们保持一致的边距和间距在我们的应用程序。

所有这些样式函数都存在于控制器之外,可以在任何地方自由使用和重用。

我们的视图控制器呢?嗯,它变得非常简洁! 每个视图都被实例化,然后在一行中设置样式。

7. What’s the point?

看一个真实世界的例子会让我们更容易地问自己:这有什么意义? 我们已经在日常用例中利用了<>,并且能够以一种真正强大而灵活的方式解决UIKit样式化的问题。我们的函数没有与UIKit战斗。事实上,他们玩得还不错!]

我们甚至可以使用带有样式功能的UIAppearance !

baseButtonStyle(UIButton.appearance())

或者,如果你感觉不错:

baseButtonStyle(.appearance())

这就是简单函数的美妙之处。它们是灵活的,让我们可以随意使用它们。我们并不是在用一层抽象来阻止我们看到正在发生的事情。

这是我们一直在用的东西。您可以简单地将它引入到代码库中,而无需依赖。