Video #17: Styling with Overture
Episode: Video #17 Date: May 28, 2018 Access: Members Only 🔒 URL: https://www.pointfree.co/episodes/ep17-styling-with-overture

Description
We revisit an old topic: styling UIKit components. Using some of the machinery we have built from previous episodes, in particular setters and function composition, we refactor a screen’s styles to be more modular and composable.
Video
Cloudflare Stream video ID: 5c3d69c3e560fe126f2db1a329abcfec Local file: video_17_styling-with-overture.mp4 *(download with --video 17)*
Transcript
— 0:05
Today’s episode is a new format where we take some real-world code and chip away at it to make it a little bit nicer. In this episode we’re going to focus on styling, a topic we’ve covered before , but we’re going to build on it with concepts from other, more recent episodes. In that episode, we showed how to style UIKit components using simple functions, and how function composition allowed us to layer these styles and compose them together. Later, we did a series of episodes on setters ( 1 , 2 , 3 ) where we showed that setters are wonderful, composable units that play nicely with Swift language-level features. Even later, we covered composition without operators , where we focus on composition as such an essential feature of functional programming that we shouldn’t let custom operators get in the way of embracing them.
— 1:04
We’re going to bring all of these ideas into one big refactor of a screen from the hypothetical, TBD, far-future Point-Free iOS app. Pre-refactor
— 1:41
The screen contains a single table view controller with a list of Point-Free episodes. It’s coded in a very straightforward way, with basically no abstractions added. It starts with a custom, subscription “call-out” UITableViewCell , which asks the user to consider subscribing to our video series. It contains a bunch of subviews that are styled and laid out inline in an initializer. final class SubscribeCalloutCell: UITableViewCell { private let bodyLabel = UILabel() private let buttonsStackView = UIStackView() private let containerView = UIView() private let loginButton = UIButton() private let orLabel = UILabel() private let rootStackView = UIStackView() private let subscribeButton = UIButton() private let titleLabel = UILabel() override init( style: UITableViewCellStyle, reuseIdentifier: String? ) { super.init(style: style, reuseIdentifier: reuseIdentifier) self.selectionStyle = .none self.contentView.layoutMargins = .init( top: 24, left: 24, bottom: 24, right: 24 ) self.titleLabel.text = "Subscribe to Point-Free" self.titleLabel.font = UIFont.preferredFont(forTextStyle: .title3) self.bodyLabel.text = """ 👋 Hey there! See anything you like? \ You may be interested in subscribing so that you \ get access to these episodes and all future ones. """ self.bodyLabel.numberOfLines = 0 self.bodyLabel.font = UIFont.preferredFont( forTextStyle: UIFontTextStyle.subheadline ) self.containerView.backgroundColor = UIColor( white: 0.96, alpha: 1.0 ) self.containerView.layoutMargins = .init( top: 24, left: 24, bottom: 24, right: 24 ) self.containerView.translatesAutoresizingMaskIntoConstraints = false self.contentView.addSubview(self.containerView) self.rootStackView.alignment = .leading self.rootStackView.spacing = 24 self.rootStackView.translatesAutoresizingMaskIntoConstraints = false self.rootStackView.axis = .vertical self.rootStackView.layoutMargins = .init( top: 24, left: 24, bottom: 24, right: 24 ) self.rootStackView.isLayoutMarginsRelativeArrangement = true self.rootStackView.addArrangedSubview(self.titleLabel) self.rootStackView.addArrangedSubview(self.bodyLabel) self.rootStackView.addArrangedSubview(self.buttonsStackView) self.contentView.addSubview(self.rootStackView) self.orLabel.text = "or" self.orLabel.font = UIFont.preferredFont( forTextStyle: .subheadline ) self.subscribeButton.setTitle( "See subscription options", for: .normal ) self.subscribeButton.setTitleColor(.white, for: .normal) self.subscribeButton.titleLabel?.font = UIFont.preferredFont( forTextStyle: .subheadline ) .bolded self.subscribeButton.setBackgroundImage( .from(color: .pf_purple), for: .normal ) self.subscribeButton.layer.cornerRadius = 6 self.subscribeButton.layer.masksToBounds = true self.subscribeButton.contentEdgeInsets = .init( top: 8, left: 16, bottom: 8, right: 16 ) self.loginButton.setTitle("Login", for: .normal) self.loginButton.setTitleColor(.black, for: .normal) self.loginButton.titleLabel?.font = UIFont.preferredFont( forTextStyle: .subheadline ) .bolded self.buttonsStackView.spacing = 8 self.buttonsStackView.alignment = .firstBaseline self.buttonsStackView.addArrangedSubview(self.subscribeButton) self.buttonsStackView.addArrangedSubview(self.orLabel) self.buttonsStackView.addArrangedSubview(self.loginButton) NSLayoutConstraint.activate([ self.rootStackView.leadingAnchor.constraint( equalTo: self.contentView.layoutMarginsGuide.leadingAnchor ), self.rootStackView.topAnchor.constraint( equalTo: self.contentView.layoutMarginsGuide.topAnchor ), self.rootStackView.trailingAnchor.constraint( equalTo: self.contentView.layoutMarginsGuide.trailingAnchor ), self.rootStackView.bottomAnchor.constraint( equalTo: self.contentView.layoutMarginsGuide.bottomAnchor ), self.containerView.leadingAnchor.constraint( equalTo: self.rootStackView.leadingAnchor ), self.containerView.topAnchor.constraint( equalTo: self.rootStackView.topAnchor ), self.containerView.trailingAnchor.constraint( equalTo: self.rootStackView.trailingAnchor ), self.containerView.bottomAnchor.constraint( equalTo: self.rootStackView.bottomAnchor ), ]) } required init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } }
— 2:16
Then we have a custom UITableViewCell for episodes, which is configured in a very similar way. final class EpisodeCell: UITableViewCell { private let blurbLabel = UILabel() private let contentStackView = UIStackView() private let posterImageView = UIImageView() private let rootStackView = UIStackView() private let sequenceAndDateLabel = UILabel() private let titleLabel = UILabel() private let watchNowButton = UIButton() override init( style: UITableViewCellStyle, reuseIdentifier: String? ) { super.init(style: style, reuseIdentifier: reuseIdentifier) self.blurbLabel.numberOfLines = 0 self.blurbLabel.font = UIFont.preferredFont( forTextStyle: .subheadline ) self.contentStackView.axis = .vertical self.contentStackView.layoutMargins = .init( top: 24, left: 24, bottom: 32, right: 24 ) self.contentStackView.isLayoutMarginsRelativeArrangement = true self.contentStackView.spacing = 12 self.contentStackView.alignment = .leading self.contentStackView.addArrangedSubview( self.sequenceAndDateLabel ) self.contentStackView.addArrangedSubview(self.titleLabel) self.contentStackView.addArrangedSubview(self.blurbLabel) self.contentStackView.addArrangedSubview(self.watchNowButton) self.rootStackView.translatesAutoresizingMaskIntoConstraints = false self.rootStackView.axis = .vertical self.rootStackView.addArrangedSubview(self.posterImageView) self.rootStackView.addArrangedSubview(self.contentStackView) self.sequenceAndDateLabel.font = UIFont.preferredFont(forTextStyle: .caption1).smallCaps self.titleLabel.font = UIFont.preferredFont(forTextStyle: .title2) self.watchNowButton.setTitle("Watch episode →", for: .normal) self.watchNowButton.setTitleColor(.pf_purple, for: .normal) self.watchNowButton.titleLabel?.font = UIFont.preferredFont(forTextStyle: .callout).bolded self.contentView.addSubview(self.rootStackView) NSLayoutConstraint.activate([ self.rootStackView.leadingAnchor.constraint( equalTo: self.contentView.leadingAnchor ), self.rootStackView.trailingAnchor.constraint( equalTo: self.contentView.trailingAnchor ), self.rootStackView.topAnchor.constraint( equalTo: self.contentView.topAnchor ), self.rootStackView.bottomAnchor.constraint( equalTo: self.contentView.bottomAnchor ), self.posterImageView.widthAnchor.constraint( equalTo: self.posterImageView.heightAnchor, multiplier: 16/9 ), ]) } required init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") }
— 2:34
At the end of the cell we have a configure function that, given an Episode model, configures a cell. func configure(with episode: Episode) { self.titleLabel.text = episode.title self.blurbLabel.text = episode.blurb let formattedDate = episodeDateFormatter.string( from: episode.publishedAt ) self.sequenceAndDateLabel.text = "#\(episode.sequence) • \(formattedDate)" URLSession.shared.dataTask( with: URL(string: episode.posterImageUrl)! ) { data, _, _ in DispatchQueue.main.async { self.posterImageView.image = data.flatMap(UIImage.init(data:)) } } .resume() } }
— 2:40
That Episode model is a simple struct, and we have an array of values to play around with for displaying our table view.. public struct Episode { public let blurb: String public let posterImageUrl: String public let publishedAt: Date public let sequence: Int public let title: String } public let episodes: [Episode] = […]
— 2:49
The episode cell’s configure method assigns this data to some labels and quickly fetches a poster image from a URL. Loading the image inline like this may not be the best way of writing this kind of code, but it’s definitely the simplest and helps us get up and running quickly.
— 3:04
Finally, we come to the EpisodeListViewController , which is the entire view controller powering our screen. It sets some basic properties to allow for auto-sizing cells and defines a data source. final class EpisodeListViewController: UITableViewController { override func viewDidLoad() { super.viewDidLoad() self.tableView.estimatedRowHeight = 400 self.tableView.rowHeight = UITableViewAutomaticDimension } override func tableView( _ tableView: UITableView, cellForRowAt indexPath: IndexPath ) -> UITableViewCell { if indexPath.row == 0 { return SubscribeCalloutCell( style: .default, reuseIdentifier: nil ) } let cell = EpisodeCell(style: .default, reuseIdentifier: nil) cell.configure(with: episodes[indexPath.row - 1]) return cell } override func tableView( _ tableView: UITableView, numberOfRowsInSection section: Int ) -> Int { return episodes.count + 1 } }
— 3:24
Finally we do a little bit of playground work to hook up the live view. let vc = EpisodeListViewController() vc.preferredContentSize = .init(width: 376, height: 1000) PlaygroundPage.current.liveView = vc
— 3:30
What we like about this is there aren’t really any layers of abstraction. It’s so simple that it may be what you’re naturally led to if you were to only follow Apple’s documentation and sample code. Most folks could walk into a code base in this style and follow it without having to wade through any additional code.
— 3:47
However, that doesn’t mean we can’t improve these views and view controller to make things a bit more reusable, particularly the styling. As it stands, we have a lot of values and configuration just sprinkled about inline, let’s wrangle things in and extract things out by creating a more composable and reusable system. Magic numbers
— 4:08
Let’s start by addressing a bunch of magic numbers we use throughout. For example, when we configure the margins and spacing of a particular stack view, we have a bunch of 24 s, a 32 , and a 12 just assigned everywhere. self.contentStackView.layoutMargins = .init( top: 24, left: 24, bottom: 32, right: 24 ) … self.contentStackView.spacing = 12
— 4:17
Our designer has laid out this view using a grid system. Rather than using raw, fixed-point values, they use a multiple of a fixed-point value, in our case it looks like all of our layout numbers are divisible by 4, meaning our designer has used a 4-point grid.
— 4:35
Let’s replace these magic numbers using a helper function that keeps us a bit more honest. It’ll simplify things and let us talk about our layouts in terms of grid units rather than absolute points.
— 4:47
Let’s extend CGFloat with a static function that, given a grid unit, returns the corresponding CGFloat value. extension CGFloat { public static func pf_grid(_ n: Int) -> CGFloat { return CGFloat(n) * 4.0 } } We’ve decided to prefix it with a pf_ namespace as a precaution to avoid any collisions, though it may not be necessary in practice.
— 5:24
Now we can replace all of those absolute values with grid units. For example, our earlier stack view layout margins and spacing goes from this: self.contentStackView.layoutMargins = .init( top: 24, left: 24, bottom: 32, right: 24 ) … self.contentStackView.spacing = 12 To this: self.contentStackView.layoutMargins = .init( top: .pf_grid(6), left: .pf_grid(6), bottom: .pf_grid(8), right: .pf_grid(6) ) … self.contentStackView.spacing = .pf_grid(3)
— 6:08
By replacing these absolute points, we’ve applied our grid system to our entire view controller, which helps ensure that elements line up nicely and consistently. It’s a small change but will pay off over time. Reusable margins
— 6:37
Let’s now address some of the duplication we see throughout our various views. For example, we inset a lot of the stack views on this screen using .pf_grid(6) , and we can imagine wanting to do so on other screens, as well, to standardize how much padding we give our root elements throughout the application. What we want to do is extract this into a styling function so we don’t have to think about it all the time.
— 7:13
Our playground is embedded in a workspace alongside our Overture library . We’re going to use it to help build up these styling functions. import Overture With Overture imported, we can start cooking up a style guide, starting with a function that describes applying these margins to stack views and other elements. let generousMargins = mut( \UIView.layoutMargins, .init( top: .pf_grid(6), left: .pf_grid(6), bottom: .pf_grid(6), right: .pf_grid(6) ) ) We used mut , a helper that, given a key path and a value, produces a setter function that we can use and reuse throughout our application. In this case we’ve produced a generousMargins function that, given a UIView , will mutate its layoutMargins to 6 grid units.
— 8:08
This is a really lightweight way of describing and building up these kinds of reusable functions, and we can apply them right away. For example, this configuration: self.cardView.layoutMargins = .init( top: .pf_grid(6), left: .pf_grid(6), bottom: .pf_grid(6), right: .pf_grid(6) ) Becomes this: with(self.cardView, generousMargins) The with function also comes from Overture, which is a non-operator version of left-to-right function application.
— 8:49
We can apply this same function to a couple other stack views, but one of them has a slightly more generous bottom margin. self.contentStackView.layoutMargins = .init( top: .pf_grid(6), left: .pf_grid(6), bottom: .pf_grid(8), right: .pf_grid(6) )
— 9:20
Luckily, we have another means of composition from Overture: concat , that allows us to first apply our generousMargins before also applying an extra generous bottom margin. with(self.contentStackView, concat( generousMargins, mut(\.layoutMargins.bottom, .pf_grid(8) ))
— 9:41
It’s really cool that even when we had a style that was slightly different, we were still able to leverage the base generousMargins style and further tweak it a bit from there. This is what’s nice about function composition. Reusable stack view styles
— 10:05
There are some other properties our stack views share. They’re configured with a vertical axis, consistent spacing, and set isLayoutMarginsRelativeArrangement to true . We should be able to pull these details out into a shared styling function.
— 10:30
What’s nice about functions is we can define them inline, next to our existing code. Starting where our first cell’s root stack view is configured, we can define a base stack view style. let baseStackViewStyle = concat( mut(\.spacing, .pf_grid), mut(\.axis, .vertical), mut(\.isLayoutMarginsRelativeArrangement, true), mut(\.translatesAutoresizingMaskIntoConstraints, false) ) We’ve also extracted translatesAutoresizingMaskIntoConstraints , since our root stack views are typically constrained to their superviews using auto-layout.
— 11:39
We were able to cut and paste our original configuration, make a few lightweight, cosmetic changes from mutable assignment to mut , and we came out the other end with a reusable unit of code! What’s more, these base stack views should probably be configured to apply our generous margins, and we’re able to do so by just adding one line. let baseStackViewStyle = concat( generousMargins, mut(\.spacing, .pf_grid), mut(\.axis, .vertical), mut(\.isLayoutMarginsRelativeArrangement, true), mut(\.translatesAutoresizingMaskIntoConstraints, false) ) … with(self.rootStackView, baseStackViewStyle)
— 11:54
We’ve defined this styling function inline, but styling functions can live anywhere! It’s a cut-paste away from file scope, where we can reuse it in our other cells. This speaks to how lightweight functions are in Swift! We can now swap out generousMargins in our other stack views to use baseStackViewStyle , instead, and we get to delete a lot of previously redundant code.
— 12:40
There’s another stack view on this screen that is configured with a vertical axis and auto-layout, but does not have any margins. It might be nice to factor this configuration out using styling functions, as well.
— 13:07
Let’s start with translatesAutoresizingMaskIntoConstraints , which is a bit of a mouthful. Rather than use mut to set this on our baseStackViewStyle , but can extract it to its own, more descriptive helper. let autoLayoutStyle = mut( \UIView.translatesAutoresizingMaskIntoConstraints, false ) let baseStackViewStyle = concat( generousMargins, mut(\.spacing, .pf_grid), mut(\.axis, .vertical), mut(\.isLayoutMarginsRelativeArrangement, true), autoLayoutStyle )
— 13:38
We also typically set our stack view axis to be vertical, so let’s extract that out, as well. let verticalStackView = mut(\UIStackView.axis, .vertical) let baseStackViewStyle = concat( generousMargins, mut(\.spacing, .pf_grid), verticalStackView, mut(\.isLayoutMarginsRelativeArrangement, true), autoLayoutStyle )
— 14:00
Now we can take that root stack view and configure it with these reusable helpers. with(self.rootStackView, concat( autoLayoutStyle, verticalStackView )) Overture makes this easy! We’re free to, in a single line, pluck out specific kinds of styling, rename them more memorably, and reuse them throughout our app! Reusable button styles
— 14:36
Let’s address some more duplication: how we’re styling our buttons.
— 14:45
We have a few buttons on the screen: a filled-in button that conveys a primary action; a secondary, text-only button; and a third text-only button with a primary color. They all share the same font and weight, while everything else is a little different. We should be able to compose a set of styling functions to do this work and make it more reusable.
— 15:14
Let’s start with a base, styling function that sets up the font. let baseTextButtonStyle = mut( \UIButton.titleLabel.font, UIFont.preferredFont(forTextStyle: .subheadline) ) This unfortunately doesn’t compile. Expression type ‘ReferenceWritableKeyPath<UIButton, UIFont!>’ is ambiguous without more context. The problem is that titleLabel is an optional property, for certain UIButtonType values, like .contactAdd and .detailDisclosure . We’re dealing with base text buttons, though, so we can force-unwrap this key path to get things compiling. let baseTextButtonStyle = mut( \UIButton.titleLabel!.font, UIFont.preferredFont(forTextStyle: .subheadline) )
— 16:12
We’ve added a helper for bolding a font, and can tack this onto the end of our mutation. let baseTextButtonStyle = mut( \UIButton.titleLabel!.font, UIFont.preferredFont(forTextStyle: .subheadline).bolded )
— 16:24
It might be nicer, though, to factor this transformation out into its own styling function and make it more reusable. let bolded: (inout Font) -> Void = { $0 = $0.bolded } Now we can build baseTextButtonStyle out of smaller, composed units. We’ll use mver instead of mut , since bolded describes a transformation of our font, not a value to replace it with. let baseTextButtonStyle = concat( mut( \UIButton.titleLabel!.font, .preferredFont(forTextStyle: .subheadline) ), mver(\UIButton.titleLabel!.font!, bolded) ) For some reason, the font on UILabel is an implicitly-unwrapped optional, UIFont! , so we also have to force-unwrap it.
— 17:38
Now let’s layer on the styling of our secondary text button. We want to start with baseTextButtonStyle and then update the title color. The API to do this isn’t a property, like many of our other styling functions, so mut and a key path won’t do here. Luckily, these functions compose nicely and simply, and all we have to do is open up a closure and configure the title color inline. let secondaryTextButtonStyle = concat( baseTextButtonStyle, { $0.setTitleColor(.black, for: .normal) } )
— 18:25
Now we have the primary text button, and it’s configured in much the same way, but we’re going to use a static helper on UIColor describing Point-Free purple. let primaryTextButtonStyle = concat( baseTextButtonStyle, { $0.setTitleColor(.pf_purple, for: .normal) } )
— 18:48
Now we can apply these styles and remove some of that duplicated code. Our login button configuration changes from this: self.loginButton.setTitleColor(.black, for: .normal) self.loginButton.titleLabel?.font = UIFont .preferredFont(forTextStyle: .subheadline).bolded To this: with(self.loginButton, secondaryTextButtonStyle)
— 19:03
And our “watch episode” button changes from this: self.watchNowButton.setTitleColor(.pf_purple, for: .normal) self.watchNowButton.titleLabel?.font = UIFont .preferredFont(forTextStyle: .callout).bolded To this: with(self.watchNowButton, primaryTextButtonStyle)
— 19:20
We’ve taken care of the text buttons, so let’s attack the filled-in button. Some of the changes we notice are that its background is filled in, and it has some content insets to give the text a bit of padding.
— 19:45
Let’s start by defining a base button style. It’ll start from our base text button style and add some insets. let baseButtonStyle = concat( baseTextButtonStyle, mut( \.contentEdgeInsets, .init( top: .pf_grid(2), left: .pf_grid(4), bottom: .pf_grid(2), right: .pf_grid(4) ) ) )
— 20:25
The button also has rounded corners, so let’s cook up a rounded styling function. We can define one that works on any view. func roundedStyle(cornerRadius: CGFloat) -> (UIView) -> Void { return concat( mut(\.layer.cornerRadius, cornerRadius), mut(\.layer.masksToBounds, true) ) } It takes configuration, so we’ve wrapped it up in a function that takes a corner radius.
— 21:04
It’s kind of cool! We didn’t have to open up a closure that took a view. We were able to piece together some small units that we’re getting to know really well.
— 21:13
From here, we can define a base rounded style, which uses a default radius that we’ll use throughout our app. let baseRoundedStyle = roundedStyle(cornerRadius: 6)
— 21:28
With these pieces, we can build a base filled button style. let baseFilledButtonStyle = concat( baseButtonStyle, baseRoundedStyle )
— 21:57
From this, we can finally derive our primary button style, where we concatenate the base filled button style with some color configuration. let primaryButtonStyle = concat( baseFilledButtonStyle, { $0.setBackgroundImage(.from(color: .pf_purple), for: .normal) }, { $0.setTitleColor(.white, for: .normal) } )
— 22:42
Now we’ve built up a hierarchy of styles for buttons that we can reuse freely throughout out app, and we’ve derived reusable styling functions for other views along the way!
— 22:52
This work pays off when we apply it to our button. All of this: self.subscribeButton.setTitleColor(.white, for: .normal) self.subscribeButton.titleLabel?.font = UIFont .preferredFont(forTextStyle: .subheadline).bolded self.subscribeButton.setBackgroundImage( .from(color: .pf_purple), for: .normal ) self.subscribeButton.layer.cornerRadius = 6 self.subscribeButton.layer.masksToBounds = true self.subscribeButton.contentEdgeInsets = .init( top: .pf_grid(2), left: 16, bottom: 8, right: 16 ) Becomes this: with(self.subscribeButton, primaryButtonStyle)
— 23:11
This was a lot of code to clean up, and every future view and screen that uses primary buttons gets to take advantage of it.
— 23:36
It’s worth noting that we didn’t have to create a bunch of UIButton subclasses, puzzle through inheritance chains. We were able to take really small functions and compose them together.
— 23:47
The one weird thing we occasionally encountered were ad hoc closures and curly braces for non-property outliers, like setTitleColor(for:) and friends. Most of our styling simply uses mut and key paths, so these exceptions sure do stick out.
— 24:05
Key paths get generated for us automatically for any property on a type. This means we can define our own properties and get some key paths for free. Let’s define one for a button’s title color. extension UIButton { var normalTitleColor: UIColor? { get { return self.titleColor(for: .normal) } set { self.setTitleColor(newValue, for: .normal) } } } This is a simple computed property that merely calls the getter and setter methods under the hood, keyed to UIControlState.normal .
— 24:58
This means we can change this: let secondaryTextButtonStyle = concat( baseTextButtonStyle, { $0.setTitleColor(.black, for: .normal) } ) To this: let secondaryTextButtonStyle = concat( baseTextButtonStyle, mut(\.normalTitleColor, .black) )
— 25:15
It’s a small change, but it’s shorter and more consistent with the key path-based styling functions we generally reach for when building these styling functions.
— 25:40
We can also add a quick property for setting a button’s background image. extension UIButton { var normalBackgroundImage: UIImage? { get { return self.backgroundImage(for: .normal) } set { self.setBackgroundImage(newValue, for: .normal) } } }
— 25:56
Now this: let primaryButtonStyle: (UIButton) -> Void = concat( baseFillButtonStyle, { $0.setTitleColor(.white, for: .normal) }, { $0.setBackgroundImage(.from(color: .pf_purple), for: .normal) } ) Becomes this: let primaryButtonStyle: (UIButton) -> Void = concat( baseFillButtonStyle, mut(\.normalTitleColor, .white), mut(\.normalBackgroundImage, .from(color: .pf_purple)) )
— 26:07
This shows that we use these APIs on our own terms! We’re free to define these properties as we see fit to make the APIs we work with a bit nicer. What’s the point?
— 26:24
Now on our series we typically end episodes by asking, “What’s the point?” This entire episode, however, was an exploration of “the point.” We took a bunch of concepts that we’ve looked at over the course of the series, sometimes very abstractly, and grounded them in a real-world refactoring exercise.
— 26:53
Let’s take a moment to breathe and look at all that we’ve accomplished in this episode. Here we have everything that we defined above our first cell: extension CGFloat { static func pf_grid(_ n: Int) -> CGFloat { return CGFloat(n) * 4 } } let generousMargins = mut(\UIView.layoutMargins, .init( top: .pf_grid(6), left: .pf_grid(6), bottom: .pf_grid(6), right: .pf_grid(6) )) let autoLayoutStyle = mut( \UIView.translatesAutoresizingMaskIntoConstraints, false ) let verticalStackView = mut(\UIStackView.axis, .vertical) let baseStackViewStyle = concat( generousMargins, mut(\UIStackView.spacing, .pf_grid(3)), verticalStackView, mut(\.isLayoutMarginsRelativeArrangement, true), autoLayoutStyle ) let bolded: (inout UIFont) -> Void = { $0 = $0.bolded } let baseTextButtonStyle = concat( mut( \UIButton.titleLabel!.font, UIFont.preferredFont(forTextStyle: .subheadline) ), mver(\UIButton.titleLabel!.font!, bolded) ) extension UIButton { var normalTitleColor: UIColor? { get { return self.titleColor(for: .normal) } set { self.setTitleColor(newValue, for: .normal) } } } let secondaryTextButtonStyle = concat( baseTextButtonStyle, mut(\.normalTitleColor, .black) ) let primaryTextButtonStyle = concat( baseTextButtonStyle, mut(\.normalTitleColor, .pf_purple) ) let baseButtonStyle = concat( baseTextButtonStyle, mut(\.contentEdgeInsets, .init( top: .pf_grid(2), left: .pf_grid(4), bottom: .pf_grid(2), right: .pf_grid(4) )) ) func roundedStyle(cornerRadius: CGFloat) -> (UIView) -> Void { return concat( mut(\.layer.cornerRadius, cornerRadius), mut(\.layer.masksToBounds, true) ) } let baseRoundedStyle = roundedStyle(cornerRadius: 6) let baseFilledButtonStyle = concat( baseButtonStyle, baseRoundedStyle ) extension UIButton { var normalBackgroundImage: UIImage? { get { return self.backgroundImage(for: .normal) } set { self.setBackgroundImage(newValue, for: .normal) } } } let primaryButtonStyle = concat( baseFilledButtonStyle, mut(\.normalBackgroundImage, .from(color: .pf_purple)), mut(\.normalTitleColor, .white) ) This is all reusable code! That means it’s code that we should extract into its own style guide file or framework! It’s standalone code that’s different than all of your other app code and it can live on its own anywhere. It sets up your grid system, your layout margins, your fonts and typography, your button styles.
— 27:33
And by keeping it in its own framework, you prevent it from becoming entangled with other application code. It forces you to focus on small, composable components, which leads to a lot of benefits down the road.
— 27:49
We also love playgrounds, and extracting code to frameworks allows us to import it into frameworks, while application code doesn’t have this ability. This means we can create living, breathing style guide that visually documents all the styles of an app.
— 28:10
We use this in our everyday code and feel that it’s a great way to style your UI components. It doesn’t fight UIKit like some abstractions, and it’s so lightweight that even if we decided to try something else in the future, it wouldn’t take any work to unroll it and apply styles inline again. All we did was put functions in front of this styling configuration so that we could compose them.
— 28:46
Before we wrap up, let’s give one more shout-out to Overture . This very tiny library is what made our styling functions so easy to build. With mut , mver , and concat , we have all the basic building blocks we need to pluck styling functions out of thin air and compose them together. Downloads Sample code 0017-styling-pt2 Point-Free A hub for advanced Swift programming. Brought to you by Brandon Williams and Stephen Celis . Content Become a member The Point-Free Way Beta previews Gifts Videos Collections Free clips Blog More About Us Community Slack Mastodon Twitter BlueSky GitHub Contact Us Privacy Policy © 2026 Point-Free, Inc. All rights are reserved for the videos and transcripts on this site. All other content is licensed under CC BY-NC-SA 4.0 , and the underlying source code to run this site is licensed under the MIT License .