UIKit Styling with Functions

Episode #3 • Feb 12, 2018 • Free Episode

We bring tools from previous episodes down to earth and apply them to an everyday task: UIKit styling. Plain functions unlock worlds of composability and reusability in styling of UI components. Have we finally solved the styling problem?

This episode is free for everyone.

Subscribe to Point-Free

Access all past and future episodes when you become a subscriber.

See plans and pricing

Already a subscriber? Log in

Introduction

The past two episodes have been a little abstract. We’ve been exploring composition in a way that may not seem relevant and even at-hand in our day-to-day code. Let’s see how we can apply composition to something more concrete: 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),
      ])
  }
}

A common problem we face when building UIs is reusable styling. This code makes no attempt to make styles reusable yet. It merely configures each view inline after instantiation. A lot of these components have overlapping styles, so let’s try to find a nicer way of styling components without so much duplication.

UIAppearance

First let’s explore UIAppearance, and API Apple provides to help tackle reusable styling. UIAppearance is a protocol with a few static methods, primarily one called appearance. These methods return a proxy view that can be configured just like a regular view. Once this proxy is configured, any view of its kind added to the view hierarchy will be configured the same way.

All of our buttons have the same content edge insets and font, so let’s try to move this configuration to use UIAppearance.

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

If we remove the corresponding lines from our button configuration, things look mostly the same.

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)

// …

But we notice a difference: the font’s a little smaller. What’s going on? We’re using the .system button type, which provides some nice styling to the button title, which might be getting in the way. On closer inspection, though, we’re reaching two layers deep through the proxy object, attempting to update the font through the title label. UIAppearance changes only work on direct view properties. One workaround would be to extend our view and expose this configuration as a new property, but that’s a bit of work.

Another issue with UIAppearance is that it works on the class level. We’ve already hit our limit of reusable configuration for UIButton, so in order to build more kinds of reusable buttons, our main path forward is to subclass.

Subclassing

Subclassing is a popular way of managing reusable styles throughout an app. Let’s start by defining a base button class and overriding its initializer to style it.

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

We were also forced by the compiler to define that extra initializer.

Now we can instantiate our buttons as BaseButton

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

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

But the font’s looking wrong again. Button subclasses don’t play nicely with the .system type, so we’re going to have to give up the things that we get from that button type for now.

let gitHubButton = BaseButton()
// …

let loginButton = BaseButton()
// …

Now the font’s rendering as it should.

We’re now using this BaseButton class directly, but whenever we prefix a class name with Base, we’re usually working with some abstract functionality that gets further subclassed for actual use.

What are some more direct button styles we see on our screen? We have a “filled” button style, a “border” button style, and a “text” button style. Let’s start by defining a FilledButton subclass.

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

We again, need to provide the extra init(coder:) initializer to satisfy the compiler, but now have a new class we can use.

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

These styles have been grouped together by reuse, and it looks like we’ve skipped ahead here. We have a couple lines that round our button edges, shared with the sign-in button. Sounds like we need another subclass.

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

Now we’re forced to think of the order of operations. Both FilledButton and RoundedButton inherit from BaseButton. For now, it seems that FilledButton should inherit from RoundedButton to ensure that filled buttons are rounded.

class FilledButton: RoundedButton {
  // …
}

We could continue along these lines and define BorderButton and TextButton subclasses and figure out where they fit in the hierarchy. Where they fit may be easy to figure out for this small screen, but over time we’ll find that we have disconnected subclasses that need to share the same logic.

This is the diamond inheritance problem: given a base class with two subclasses, we may want another subclass that inherits from both of these subclasses. Swift doesn’t support diamond inheritance, so let’s explore some other alternate ways of sharing logic.

Object Composition

We have an old adage in our industry: “prefer composition over inheritance.” What does “composition” mean in this sense?

Instead of using a subclass, let’s define a static property for creating a button with a base style.

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

We can create another helper for our filled button style.

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

This is where the “composition” comes in. We were able to call self.base to derive a button with our base styles before further applying out filled styles on top of it.

We can then add a rounded style.

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

Now our GitHub button can use one of these static properties instead of a subclass.

let gitHubButton = UIButton.filled

Whoops! Our button is filled, but not rounded. What happened? Looks like our order of operations is wrong. It looks like this version has the same issues as inheritance, though it’s still a bit nicer. Defining styles directly on UIButton is definitely more succinct, and we didn’t have to define those noisy init(coder:) initializers. Let’s see if we can find a less confusing way to compose our styles.

Functions

Let’s turn to our old, familiar friend, function. We can define a baseButtonStyle function that styles a given button with our base styles.

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

Nice and succinct.

What about a filledButtonStyle?

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

Now our filled style doesn’t need to inherit or invoke the base style.

How about a roundedButtonStyle?

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

There we are! Three styling functions that don’t need to worry about a hierarchy of inheritance or invocation between them. They do one thing and they do it well. Let’s use them.

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

This is seeming a little messy. There are a lot of moving parts in this configuration and it seems like it would be easy to miss a step. We’re also applying baseButtonStyle directly, which seems like something that we do want our other styles to use. It’d be nice to be able to combine these styles into less haphazard units.

Function Composition

In our episode on Side Effects, we introduced <> for single type composition. We defined it twice: once for functions that take As as input and return As as output, and once for value mutation with functions that go from inout A to 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)
  }
}

Our styling functions are living in the land of reference type mutation, so let’s see if we can reuse this (inout A) -> Void shape.

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

We’ve constrained our generic A to AnyObject, a protocol that all reference types conform to. In the process, we were able to remove our inout annotations.

Now we can compose our filledButtonStyle from our baseButtonStyle.

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

Let’s do the same with our roundedButtonStyle.

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

Our GitHub button no longer needs to be configured with baseButtonStyle. Let’s finally make a decision about our hierarchy of styling, though, and make filledButtonStyle compose from roundedButtonStyle.

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

This kind of composition feels a lot more flexible. We can mix, match, and extract styles quickly as we go, and each function itself is responsible for just its styling and doesn’t need to call out to other styling functions: we combine styles through function composition.

We’re also now able to explore reusable styles that would’ve been impossible in the class and static property world! Our roundedButtonStyle is a function that modifies properties that exist on all UIViews, not just buttons. In fact, our UITextFields are configured the same way.

We can now write a roundedStyle that works on all UIViews.

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

And our roundedButtonStyle merely needs to compose with this more reusable function.

let roundedButtonStyle =
  baseButtonStyle
    <> roundedStyle

We can reuse this roundedStyle with our text fields by creating a baseTextFieldStyle.

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
}

And we can apply this style to both of our text fields and get rid of a lot of code.

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

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

More reuse is peeking through: our baseTextFieldStyle configures its border in the same way that our border button does. We should be able to write a borderStyle that both can use.

If we write a styling function in a very basic way, we run into a problem:

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

We have configuration here that is different in each case: our border button has a thick, black border, while our text field has a thin, gray border. Let’s bring this configuration into the function.

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

In order to compose, we need our function to take configuration up front and return a brand new function that styles a view.

Let’s try using it with 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
}

We have a small gotcha.

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

We’re composing a bunch of small functions that style UIViews generally into a function that expects UITextFields specifically and the type system has hit its inference limit between subtypes, supertypes, and multiline closures. This kind of error should be rare, but it’s easy to fix. We need to be explicit with the closure’s type signature.

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
}

And that works pretty nicely.

Now we have a borderStyle that should make it easy to create a borderButtonStyle!

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

Another example of working fluidly with styling functions that work on any UIView. We’ve been able to solve our diamond inheritance problem with a diamond <>.

What does it look like if we take things further and write styling functions for everything?

// 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),
      ])
  }
}

We were able to derive a lot of interesting, reusable styles! We have an autolayoutStyle that might be a bit more easier to read and remember than translatesAutoresizingMaskIntoConstraints = false. We have an aspectRatioStyle for maintaining the aspect ratio of an image view with auto-layout.

We have an imageButtonStyle that can compose with borderButtonStyle or filledButtonStyle depending on our needs.

We also have a rootStackViewStyle that allows us to maintain consistent margins and spacing across our app.

All of these styling functions exist outside the controller, free to use and reuse anywhere.

What about our view controller? Well, it’s become really concise! Each view is instantiated and then styled on a single line.

What’s the point?

Looking at a real-world example makes it a bit easier to ask ourselves: what’s the point? We’ve leveraged <> in an everyday use case and been able to wrangle the problem of UIKit styling in a really powerful and flexible way. Our functions aren’t fighting UIKit. In fact, they play rather nicely!

We can even use UIAppearance with our styling functions!

baseButtonStyle(UIButton.appearance())

Or, if you’re feeling fancy:

baseButtonStyle(.appearance())

This is the beauty of simple functions. They’re flexible and leave us open to use them however we want to use them. We’re not working with a layer of abstraction that prevents us from seeing what’s going on.

This is something we use all the time. It’s something you can introduce to your code base with no dependencies simply.

This episode is free for everyone.

Subscribe to Point-Free

Access all past and future episodes when you become a subscriber.

See plans and pricing

Already a subscriber? Log in

Downloads