在代码重用和可配置性之间找到一个很好的平衡点一般是颇有挑战性的。虽然理想状况下,咱们但愿避免重复代码并意外地建立多个真实源,但咱们须要配置的各类对象和值的许多方式每每取决于它们所使用的上下文。面试
本周,让咱们看看几种不一样的技术,这些技术可让咱们实现这种平衡-经过构建轻量级抽象,使咱们可以封装配置代码,以及如何在代码库之间共享这些抽象,以提升其一致性。编程
在进行任何类型的软件开发时,一般会将程序分割成不一样的部分,以便可以将它们做为单独的单元处理。对于用户界面密集的应用程序,如iOS和Mac应用程序,一般很容易根据构成应用程序的各类屏幕进行这种分割。例如,一个购物应用程序可能有一个产品屏幕,一个列表屏幕,一个搜索屏幕,等等。swift
虽然这种屏幕级切片从高层次的角度看颇有意义(尤为是由于它与咱们倾向于与其余协做者(好比测试人员和设计人员)讨论咱们的应用程序的方式相匹配),但它每每会致使须要对每一个屏幕进行大量配置的UI代码。bash
拿着这个ProductViewController例如,它包含一个Buy按钮,以及用于显示每一个产品的详细信息和相关项目的视图-全部这些都是在视图控制器的viewDidLoad方法:闭包
class ProductViewController: UIViewController {
let product: Product
...
override func viewDidLoad() {
super.viewDidLoad()
// Buy button
let buyButton = UIButton(type: .custom)
buyButton.setImage(.buy, for: .normal)
buyButton.backgroundColor = .systemGreen
buyButton.addTarget(self,
action: #selector(buyButtonTapped),
for: .touchUpInside
)
view.addSubview(buyButton)
// Product detail view
let productDetailView = UIView()
...
// Related products view
let relatedProductsView = UIView()
...
}
}
复制代码
尽管咱们试图经过在每一个配置块以前添加一个注释来使上面的代码更容易阅读,但咱们当前的viewDidLoad实现确实受到缺少结构的影响。由于咱们全部的配置都发生在一个地方,因此变量很容易在错误的上下文中被意外地使用,而且随着时间的推移,咱们的代码变得愈来愈复杂。app
就像咱们看了看“编写自记录的SWIFT代码”,缓解上述问题的一种方法是简单地将配置代码的不一样部分划分为不一样的方法,其中viewDidLoad而后能够呼叫:框架
private extension ProductViewController {
func setupBuyButton() {
let buyButton = UIButton(type: .custom)
...
}
func setupProductDetailView() {
let productDetailView = UIView()
...
}
func setupRelatedProductsView() {
let relatedProductsView = UIView()
...
}
}
复制代码
而上面的方法确实解决了咱们的结构问题,而且确定地使咱们的代码更多。自证阅读起来更容易,它仍然将咱们各自的视图组件与它们的呈现容器紧密地结合在一块儿-ProductViewController在这种状况下。ide
对于目前只在单个视图控制器中使用的一次性视图来讲,这可能不是一个问题,可是对于更通用的UI代码来讲,若是咱们可以轻松地在代码库中重用咱们的各类配置,那就太好了。函数
一种不须要定义任何新类型的方法是使用静态工厂法-使咱们可以以既易于定义又易于使用的方式封装配置每一个视图的方式:布局
extension UIView {
static func buyButton(withTarget target: Any, action: Selector) -> UIButton {
let button = UIButton(type: .custom)
button.setImage(.buy, for: .normal)
button.backgroundColor = .systemGreen
button.addTarget(target, action: action, for: .touchUpInside)
return button
}
}
复制代码
静态工厂方法的优势在于,它们使咱们可以以相似枚举的方式调用API-使用SWIFT很是轻量级的点语法..若是咱们也定义相似的方法来建立一个购买按钮,而后咱们就能够viewDidLoad简单地看上去以下所示的实现:
class ProductViewController: UIViewController {
let product: Product
...
override func viewDidLoad() {
super.viewDidLoad()
view.addSubview(.buyButton(
withTarget: self,
action: #selector(buyButtonTapped)
))
view.addSubview(.productDetailView(
for: product
))
view.addSubview(.relatedProductsView(
for: product.relatedProducts,
delegate: self
))
}
}
复制代码
真干净!局部变量已经消失,咱们仍然能够将全部视图设置代码都放在一个方法中,同时也给了咱们更大程度的封装和彻底的可重用性,由于咱们如今能够在须要的地方轻松地构造上述类型的视图。
虽然上面的方法对于UI配置代码很是有用,在理想状况下应该在整个代码库中保持不变,好比设置公共组件,可是咱们还常常须要以一种更加特定于上下文的方式扩展这些配置。
例如,咱们可能须要对视图应用某种形式的布局,更新或绑定某些状态到视图,或者根据它们所使用的特性定制它们的行为或外观。
为了更容易地作到这一点,让咱们扩展UIView使用方便的API-在将给定视图添加为子视图后执行闭包,以下所示:
extension UIView {
@discardableResult
func add<T: UIView>(_ subview: T, then closure: (T) -> Void) -> T {
addSubview(subview)
closure(subview)
return subview
}
}
复制代码
有了上述方法,咱们如今能够继续使用漂亮的点语法来建立视图,同时仍然容许咱们应用特定于上下文的配置,例如,为了添加一组自动布局约束:
class ProductViewController: UIViewController {
...
override func viewDidLoad() {
super.viewDidLoad()
view.add(.buyButton(
withTarget: self,
action: #selector(buyButtonTapped)
), then: {
NSLayoutConstraint.activate([
$0.topAnchor.constraint(equalTo: view.topAnchor),
$0.trailingAnchor.constraint(equalTo: view.trailingAnchor)
...
])
})
...
}
}
复制代码
虽然上面的语法可能须要一段时间才能适应,但它确实给了咱们两个世界中最好的东西-咱们如今可以彻底封装咱们的全局和本地配置,同时也强制执行必定程度的结构。它还容许咱们在不一样的屏幕之间轻松地共享视图组件,而无需定义任何新的视图组件。UIView子类。
上述方法的另外一个有趣之处在于它如何开始使基于UIKit的命令式代码更具声明性,由于咱们再也不继续在视图控制器中设置咱们的各类视图,而是声明咱们但愿使用什么样的配置。这让咱们更接近于斯威夫特,这将有助于咱们在将来更好地过渡到那个新的世界。
只是比较一下咱们ProductViewController若是将其表示为SwiftUI视图,那么在结构上,它可能与咱们上面基于UIKit的方法很是类似:
struct ProductView: View {
var product: Product
var body: some View {
VStack {
BuyButton {
// Handling code
...
}
ProductDetailView(product: product)
RelatedProductsView(products: product.relatedProducts) {
// Handling code
...
}
}
}
}
复制代码
固然,这并不意味着咱们已经自动地使基于UIKit的代码SwiftUI兼容,只须要修改它的结构-可是经过使用相似的思惟方式来组织咱们的各类视图配置,咱们至少能够开始对愈来愈多的声明式编码风格更加熟悉。
虽然咱们在开发基于UI的应用程序时编写的大部分配置代码倾向于以视图层为中心,但咱们的代码库的其余部分也须要大量配置,特别是直接在系统API之上编写的逻辑。
例如,假设咱们正在构建一个类型,用于解析字符串中的某种形式的元数据,而且咱们但愿使用一个共享DateFormatter在这种类型的全部实例中。为此,咱们可能定义一个私有静态属性,该属性使用自动关闭:
struct MetadataParser {
private static let dateFormatter: DateFormatter = {
let formatter = DateFormatter()
formatter.dateFormat = "yyyy-MM-dd HH:mm"
formatter.timeZone = TimeZone(secondsFromGMT: 0)
return formatter
}()
func metadata(from string: String) throws -> Metadata {
...
}
}
复制代码
虽然自动执行闭包很是方便,但使用它们来配置属性经常会“推”类型的核心功能-这反过来又会使您更难快速了解类型实际上在作什么。为了缓解这个问题,让咱们看看咱们是否可以在不牺牲可读性的状况下,使这种配置闭包尽量紧凑。
让咱们首先定义一个名为configure,它只接受任何对象或值,并容许咱们在闭包中应用任何类型的突变,使用inout关键字-以下所示:
func configure<T>(_ object: T, using closure: (inout T) -> Void) -> T {
var object = object
closure(&object)
return object
}
配置共享DateFormatter对于咱们的元数据解析器,咱们如今能够简单地将它传递给上面的函数,并使用$0闭包参数简写-留给咱们更紧凑的代码,同时仍然保持可读性:
struct MetadataParser {
private static let dateFormatter = configure(DateFormatter()) {
$0.dateFormat = "yyyy-MM-dd HH:mm"
$0.timeZone = TimeZone(secondsFromGMT: 0)
}
func metadata(from string: String) throws -> Metadata {
...
}
}
复制代码
以上配置属性的方法能够说比自动执行闭包更容易理解,由于经过将调用添加到configure,咱们很是清楚地代表,伴随闭包的目的其实是配置传递给它的实例。
就像任何与代码样式和结构相关的主题同样,如何最好地配置对象和值也极可能始终是一个趣味问题。然而,无论咱们其实是如何配置代码的-若是咱们可以以一种彻底封装的方式来配置代码,那么这些配置就更容易重用和管理。
开始采用愈来愈多的声明式编码样式和模式还能够进一步帮助咱们轻松地过渡到SwiftUI并结合起来,即便咱们可能预计须要一两年时间才能真正开始采用这些框架。能够说,声明式编程与API和语法同样,都是关于思惟方式的。
你认为如何?当前如何配置视图和其余值和对象?让我知道-连同你的问题,请经过加咱们的交流群 点击此处进交流群 ,来一块儿交流或者发布您的问题,意见或反馈