提供基础文章:"闭包"
在代码重用和可配置性之间找到一个很好的平衡点通常是很有挑战性的。虽然理想情况下,我们希望避免重复代码并意外地创建多个真实源,但我们需要配置的各种对象和值的许多方式往往取决于它们所使用的上下文。
本周,让我们看看几种不同的技术,这些技术可以让我们实现这种平衡-通过构建轻量级抽象,使我们能够封装配置代码,以及如何在代码库之间共享这些抽象,以提高其一致性。
点击此处即可获取精选面试资料哦
构建组件,而不是屏幕
在进行任何类型的软件开发时,通常会将程序分割成不同的部分,以便能够将它们作为单独的单元处理。对于用户界面密集的应用程序,如iOS和Mac应用程序,通常很容易根据构成应用程序的各种屏幕进行这种分割。例如,一个购物应用程序可能有一个产品屏幕,一个列表屏幕,一个搜索屏幕,等等。
虽然这种屏幕级切片从高层次的角度看很有意义(尤其是因为它与我们倾向于与其他协作者(比如测试人员和设计人员)讨论我们的应用程序的方式相匹配),但它往往会导致需要对每个屏幕进行大量配置的UI代码。
拿着这个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实现确实受到缺乏结构的影响。因为我们所有的配置都发生在一个地方,所以变量很容易在错误的上下文中被意外地使用,并且随着时间的推移,我们的代码变得越来越复杂。
就像我们看了看“编写自记录的SWIFT代码”,缓解上述问题的一种方法是简单地将配置代码的不同部分划分为不同的方法,其中viewDidLoad然后可以呼叫:
private extension ProductViewController {
func setupBuyButton() {
let buyButton = UIButton(type: .custom)
...
}
func setupProductDetailView() {
let productDetailView = UIView()
...
}
func setupRelatedProductsView() {
let relatedProductsView = UIView()
...
}
}
而上面的方法确实解决了我们的结构问题,并且肯定地使我们的代码更多。自证阅读起来更容易,它仍然将我们各自的视图组件与它们的呈现容器紧密地结合在一起-ProductViewController在这种情况下。
对于目前只在单个视图控制器中使用的一次性视图来说,这可能不是一个问题,但是对于更通用的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和语法一样,都是关于思维方式的。
你认为如何?当前如何配置视图和其他值和对象?让我知道-连同你的问题,请通过加我们的交流群 点击此处进交流群 ,来一起交流或者发布您的问题,意见或反馈