1. Introduction
今天的节目是一种新的形式,我们将一些真实世界的代码进行修改,使其变得更好。在那一集中,我们展示了如何使用简单的函数来样式化UIKit组件,以及函数组合如何允许我们将这些样式分层并将它们组合在一起。之后,我们做了一系列关于setter(1、2、3)的章节,在这些章节中,我们展示了setter是很棒的、可组合的单位,能够很好地与Swift语言级功能配合。稍后,我们还介绍了不使用操作符的组合,我们将重点放在组合上,因为组合是函数式编程的基本特性,我们不应该让定制操作符阻碍使用组合。
我们将把所有这些想法整合到一个大的屏幕重构中,这个屏幕重构来自一个假设的、待定的、遥远未来的Point-Free iOS应用程序。
2. Pre-refactor
屏幕包含一个带有Point-Free剧集列表的表格视图控制器。 它以一种非常直接的方式进行编码,基本上没有添加任何抽象。它以一个自定义的、订阅的“call-out”UITableViewCell开始,它要求用户考虑订阅我们的视频系列。它包含一组子视图,这些子视图在初始化式中内联布局。
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")
}
}
然后我们有一个自定义的UITableViewCell,它以非常类似的方式配置。
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")
}
在单元格的末尾,我们有一个configure函数,给定一个Episode模型,它配置一个单元格。
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()
}
}
Episode模型是一个简单的结构体,我们有一个值数组来显示我们的表视图。
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] = [
// …
]
cell的configure方法将此数据分配给一些标签,并从URL快速获取海报图像。像这样内联加载图像可能不是编写这类代码的最佳方式,但它绝对是最简单的,并帮助我们快速启动和运行。
最后,我们来到了EpisodeListViewController,它是驱动我们屏幕的整个视图控制器。它设置一些基本属性以允许自动调整单元格大小,并定义数据源。
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
}
}
最后,我们做了一些工作来连接实时视图。
let vc = EpisodeListViewController()
vc.preferredContentSize = .init(width: 376, height: 1000)
PlaygroundPage.current.liveView = vc
我们喜欢的是它没有任何抽象的层次。 它非常简单,如果你只遵循苹果的文档和示例代码,你可能会自然而然地得到它。大多数人都可以使用这种风格的代码库并遵循它,而无需费力地阅读任何额外的代码。
然而,这并不意味着我们不能改进这些视图和视图控制器,使其更具可重用性,特别是样式。 现在,我们有很多关于内联的值和配置,让我们通过创建一个更可组合和可重用的系统来处理和提取一些东西。
3. Magic numbers
让我们从一堆我们经常使用的神奇数字开始。例如,当我们配置一个特定堆栈视图的边距和间距时,我们有一串24s,一个32,和一个12分配到各处。
self.contentStackView.layoutMargins = .init(
top: 24,
left: 24,
bottom: 32,
right: 24
)
// …
self.contentStackView.spacing = 12
我们的设计师使用网格系统设计了这个视图。他们使用的不是原始的定点值,而是一个定点值的倍数,在我们的例子中,看起来我们所有的布局数字都能被4整除,这意味着我们的设计师使用了一个4点网格。
让我们使用一个帮助函数来替换这些神奇的数字,这让我们更诚实一点。这将简化事情,让我们从网格单位而不是绝对点的角度来讨论布局。
让我们使用一个静态函数来扩展CGFloat,给定一个网格单元,该函数返回相应的CGFloat值。
extension CGFloat {
public static func pf_grid(_ n: Int) -> CGFloat {
return CGFloat(n) * 4.0
}
}
我们决定使用pf_命名空间作为前缀,以避免任何冲突,尽管在实践中可能不是必需的。
现在我们可以用网格单位来替换所有这些绝对值。例如,我们之前的堆栈视图布局边距和间距是这样的:
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)
通过替换这些绝对点,我们将网格系统应用到整个视图控制器中,这有助于确保元素整齐一致地排列。这是一个很小的改变,但会随着时间的推移而得到回报。
4. Reusable margins
现在让我们来解决我们在各种视图中看到的一些重复。 例如,我们使用.**pf_grid(6)**在这个屏幕上插入了很多堆栈视图,我们可以想象也希望在其他屏幕上这样做,以标准化在整个应用程序中根元素的填充量。我们想做的是把它提取到一个样式函数中,这样我们就不用一直想它了。
我们的playground嵌入 Overture GitHub 库。我们将用它来帮助构建这些样式函数。
import Overture
导入Overture后,我们可以开始编写样式指南,从描述将这些边距应用到堆栈视图和其他元素的函数开始。
let generousMargins = mut(
\UIView.layoutMargins,
.init(
top: .pf_grid(6),
left: .pf_grid(6),
bottom: .pf_grid(6),
right: .pf_grid(6)
)
)
我们使用了mut,这是一个助手,给定键路径和值,它会生成一个setter函数,我们可以在整个应用程序中使用和重用该函数。在这种情况下,我们已经产生了一个慷慨的margin函数,给定一个UIView,将其layoutmargin突变为6个网格单元。
这是描述和构建这些可重用功能的一种非常轻量级的方法,我们可以立即应用它们。例如,下面的配置:
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)
with函数也来自Overture,这是一个从左到右的函数应用程序的非操作符版本。
我们可以将这个函数应用到其他几个堆栈视图,但其中一个视图的底距稍微大一些。
self.contentStackView.layoutMargins = .init(
top: .pf_grid(6),
left: .pf_grid(6),
bottom: .pf_grid(8),
right: .pf_grid(6)
)
幸运的是,我们在Overture中还有另一种组合方式:concat,它允许我们在使用额外的底部边距之前先使用我们的generousMargins。
with(self.contentStackView, concat(
generousMargins,
mut(\.layoutMargins.bottom, .pf_grid(8)
))
很酷的是,即使我们有一个稍微不同的风格,我们仍然能够利用基本的generousMargins风格,并从那里进一步调整它。这就是函数复合的优点。
5. Reusable stack view styles
我们的堆栈视图还共享其他一些属性。 它们配置了一个垂直轴,一致的间距,并设 isLayoutMarginsRelativeArrangement为true。我们应该能够将这些细节提取到一个共享的样式函数中。
函数的好处在于,我们可以在现有代码的旁边内联地定义它们。从配置第一个单元格的根堆栈视图的位置开始,我们可以定义一个基本堆栈视图样式。
let baseStackViewStyle = concat(
mut(\.spacing, .pf_grid),
mut(\.axis, .vertical),
mut(\.isLayoutMarginsRelativeArrangement, true),
mut(\.translatesAutoresizingMaskIntoConstraints, false)
)
我们还提取了translatesAutoresizingMaskIntoConstraints,因为我们的根堆栈视图通常被限制到使用自动布局的父视图。
我们能够剪切和粘贴我们的原始配置,从可变赋值到mutt做一些轻量级的修饰性更改,我们最终得到了一个可重用的代码单元!
更重要的是,这些基本堆栈视图可能应该配置为应用我们的generous边距,我们可以通过添加一行来实现。
let baseStackViewStyle = concat(
generousMargins,
mut(\.spacing, .pf_grid),
mut(\.axis, .vertical),
mut(\.isLayoutMarginsRelativeArrangement, true),
mut(\.translatesAutoresizingMaskIntoConstraints, false)
)
// …
with(self.rootStackView, baseStackViewStyle)
我们已经内联定义了这个样式函数,但是样式函数可以存在于任何地方! 它是一个远离文件作用域的剪切粘贴,我们可以在其他单元格中重用它。这说明了Swift中的函数是多么的轻量级!
我们现在可以交换出我们其他堆栈视图中的generousMargins来使用baseStackViewStyle,取而代之的是,我们可以删除许多以前冗余的代码。
这个屏幕上还有另一个堆栈视图,它配置了垂直轴和自动布局,但没有任何边距。 也可以使用样式函数来分解这个配置。
让我们从translatesAutoresizingMaskIntoConstraints开始,这有点拗口。而不是使用mut来设置我们的baseStackViewStyle,但可以提取它自己的,更具描述性的助手。
let autoLayoutStyle = mut(\UIView.translatesAutoresizingMaskIntoConstraints, false)
let baseStackViewStyle = concat(
generousMargins,
mut(\.spacing, .pf_grid),
mut(\.axis, .vertical),
mut(\.isLayoutMarginsRelativeArrangement, true),
autoLayoutStyle
)
我们通常还将堆栈视图轴设置为垂直的,我们也把它提取出来。
let verticalStackView = mut(\UIStackView.axis, .vertical)
let baseStackViewStyle = concat(
generousMargins,
mut(\.spacing, .pf_grid),
verticalStackView,
mut(\.isLayoutMarginsRelativeArrangement, true),
autoLayoutStyle
)
现在,我们可以使用这个根堆栈视图并使用这些可重用的助手对其进行配置。
with(self.rootStackView, concat(
autoLayoutStyle,
verticalStackView
))
Overture让这一切变得简单! 我们可以自由地,在一行中,挑选出特定类型的样式,重命名他们更令人难忘,并在我们的应用程序中重用他们!
6. Reusable button styles
让我们来解决更多的重复问题:如何设置按钮的样式。
我们在屏幕上设置了一些按键:传达主要操作的填充按钮;一个次要的、仅限文本的按钮;第三个纯文本按钮,原色。它们都有相同的字体和重量,而其他的一切都有一些不同。我们应该能够组合一组样式函数来完成这项工作,并使其更具可重用性。
让我们从设置字体的基本样式函数开始。
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.
问题是titleLabel是一个可选的属性,对于某些UIButtonType值,如.** contactadd和. detaildisclosure**。我们处理的是基本文本按钮,所以我们可以强制展开这个键路径来进行编译。
let baseTextButtonStyle = mut(\UIButton.titleLabel!.font, UIFont.preferredFont(forTextStyle: .subheadline))
我们已经添加了一个用于粗体字体的助手,并可以将其添加到突变的末尾。
let baseTextButtonStyle = mut(\UIButton.titleLabel!.font, UIFont.preferredFont(forTextStyle: .subheadline).bolded)
不过,更好的做法可能是将这种转换分解到它自己的样式函数中,使其更易于重用。
let bolded: (inout Font) -> Void = { $0 = $0.bolded }
现在我们可以用更小的组合单元来构建baseTextButtonStyle。 我们将使用mver而不是mut,因为bolded描述的是对字体的转换,而不是用来替换它的值。
let baseTextButtonStyle = concat(
mut(\UIButton.titleLabel!.font, .preferredFont(forTextStyle: .subheadline),
mver(\UIButton.titleLabel!.font!, bolded)
)
出于某些原因,UILabel上的字体是一个隐式打开的可选字体,UIFont!,所以我们还必须强行打开它。
现在让我们对二级文本按钮的样式进行分层。 我们想从baseTextButtonStyle开始,然后更新标题颜色。执行此操作的API不是一个属性,与我们的许多其他样式函数不同,因此mut和key路径在这里不适用。 幸运的是,这些函数组合得很好,也很简单,我们所要做的就是打开一个闭包并内联配置标题颜色。
let secondaryTextButtonStyle = concat(
baseTextButtonStyle,
{ $0.setTitleColor(.black, for: .normal) }
)
现在我们有了主文本按钮,它的配置方式基本相同,但我们将在UIColor上使用一个静态助手描述紫色。
let primaryTextButtonStyle = concat(
baseTextButtonStyle,
{ $0.setTitleColor(.pf_purple, for: .normal) }
)
现在我们可以应用这些样式并删除一些重复的代码。我们的登录按钮配置改变如下:
self.loginButton.setTitleColor(.black, for: .normal)
self.loginButton.titleLabel?.font = UIFont.preferredFont(forTextStyle: .subheadline).bolded
To this:
with(self.loginButton, secondaryTextButtonStyle)
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)
我们已经处理了文本按钮,所以让我们填充按钮。我们注意到的一些变化是它的背景是填充的,它有一些内容插入给文本一点填充。
让我们从定义基本按钮样式开始。它将从我们的基本文本按钮样式开始,并添加一些插入。
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)
)
}
它需要configuration,所以我们把它包裹在有一个角半径的函数。
这很酷!我们不需要打开一个封闭的视图。我们能够拼凑出一些我们非常熟悉的小单元。
从这里,我们可以定义一个基本的圆角样式,它使用一个默认的半径,我们将在整个应用程序中使用。
let baseRoundedStyle = roundedStyle(cornerRadius: 6)
通过这些部件,我们可以构建一个基础填充按钮样式。
let baseFilledButtonStyle = concat(
baseButtonStyle,
baseRoundedStyle
)
由此,我们最终可以派生出主按钮样式,在这里我们将基本填充的按钮样式与一些颜色配置连接起来。
let primaryButtonStyle = concat(
baseFilledButtonStyle,
{ $0.setBackgroundImage(.from(color: .pf_purple), for: .normal) },
{ $0.setTitleColor(.white, 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: .pf_grid(2), left: 16, bottom: 8, right: 16
)
Becomes this:
with(self.subscribeButton, primaryButtonStyle)
这需要清理大量代码,未来每个使用主按钮的视图和屏幕都可以利用它。
值得注意的是,我们不需要创建一堆UIButton子类,通过继承链来迷惑。我们可以把非常小的函数组合在一起。
我们偶尔会遇到一件奇怪的事情,非属性异常值的临时闭包和花括号,比如setTitleColor(for:)和friends。我们的大多数样式只使用mut和key路径,所以这些异常确实很突出。
为类型上的任何属性自动生成关键路径。这意味着我们可以定义自己的属性并免费获得一些关键路径。让我们定义一个按钮的标题颜色。
extension UIButton {
var normalTitleColor: UIColor? {
get { return self.titleColor(for: .normal) }
set { self.setTitleColor(newValue, for: .normal) }
}
}
这是一个简单的计算属性,它只是在底层调用getter和setter方法,关键字是UIControlState.normal。
这意味着我们可以改变这一点:
let secondaryTextButtonStyle = concat(
baseTextButtonStyle,
{ $0.setTitleColor(.black, for: .normal) }
)
To this:
let secondaryTextButtonStyle = concat(
baseTextButtonStyle,
mut(\.normalTitleColor, .black)
)
这是一个很小的变化,但它更短,而且与我们在构建这些样式函数时通常使用的基于路径的关键样式函数更一致。
我们还可以添加一个快速属性来设置按钮的背景图像。
extension UIButton {
var normalBackgroundImage: UIImage? {
get { return self.backgroundImage(for: .normal) }
set { self.setBackgroundImage(newValue, for: .normal) }
}
}
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))
)
这表明我们是按照自己的方式使用这些api的! 我们可以自由地定义这些属性,以使我们使用的api更好一些。
7. What’s the point?
在我们的节目中,我们通常会在每集的结尾问:“这有什么意义?” 然而,这整个episode都是对“the point”的探索。我们采用了一系列在本系列课程中研究过的概念,有时非常抽象,并将它们作为现实世界重构练习的基础。
让我们花点时间来呼吸一下,看看我们在这一集里完成了什么。这里我们有我们在第一个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)
)
这些都是可重用的代码! 这意味着我们应该将这些代码提取到它自己的样式指南文件或框架中! 它是独立的代码,不同于你所有的其他应用程序代码,它可以在任何地方独立存在。它设置你的网格系统,你的布局边距,你的字体和排版,你的按钮样式。
通过将它保存在自己的框架中,可以防止它与其他应用程序代码纠缠在一起。它迫使您专注于小型的、可组合的组件,这将带来许多好处。
我们也喜欢游乐场,将代码提取到框架中可以将其导入到框架中,而应用程序代码则没有这种能力。 这意味着我们可以创建生动的、呼吸式的风格指南,直观地记录应用程序的所有风格。
我们在日常代码中使用它,觉得这是设计UI组件的好方法。它不像一些抽象的东西一样对抗UIKit,而且它是如此轻量级,以至于即使我们决定在未来尝试其他东西,它也不需要任何工作来展开它并再次内联应用样式。我们所做的只是将函数放在样式配置的前面,以便我们可以组合它们。