可以说,为大多数平台构建应用程序最具挑战性的方面之一是确保我们呈现给用户的UI始终与我们的底层数据模型及其相关逻辑保持同步。遇到导致过时数据呈现的错误或由于UI状态与应用程序逻辑的其余部分之间发生冲突而发生的错误是很常见的。
因此,发明了如此多的不同模式和技术,以便更容易确保UI在其基础模型发生变化时保持最新状态 - 从通知,代理到可观察的所有内容,这并不奇怪。本周,我们来看看一种这样的技术 - 包括将我们的模型值绑定到UI。
同时小编这里有些书籍和面试资料哦(点击下载)

不断更新
确保我们的UI始终呈现最新可用数据的一种常见方法是在UI即将在屏幕上呈现(或重新呈现)时简单地重新加载底层模型。例如,如果我们正在为某种形式的社交网络应用构建一个配置文件屏幕,我们可能会重新加载User
每次viewWillAppear
调用时我们的配置文件ProfileViewController
:
class ProfileViewController: UIViewController {
private let userLoader: UserLoader
private lazy var nameLabel = UILabel()
private lazy var headerView = HeaderView()
private lazy var followersLabel = UILabel()
init(userLoader: UserLoader) {
self.userLoader = userLoader
super.init(nibName: nil, bundle: nil)
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
// Here we always reload the logged in user every time
// our view controller is about to appear on the screen.
userLoader.load { [weak self] user in
self?.nameLabel.text = user.name
self?.headerView.backgroundColor = user.colors.primary
self?.followersLabel.text = String(user.followersCount)
}
}
}
上述方法没有什么问题,但有一些事情可能会有所改进:
- 我们总是必须在视图控制器上保持对各种视图的引用作为属性,因为在我们加载视图控制器的模型之前,我们无法分配UI属性。
- 当使用基于闭包的API来访问加载的模型时,我们必须弱引用
self
(或显式捕获每个视图)以避免保留周期。 - 每次我们的视图控制器出现在屏幕上时,我们都会重新加载模型,即使我们上次这样做了几秒钟,即使另一个视图控制器也同时重新加载相同的模型 - 可能导致浪费,或至少不必要的网络电话。
解决上述一些问题的一种方法是使用不同类型的抽象来让我们的视图控制器访问其模型。就像我们在“处理Swift中的可变模型”中看到的那样,我们可以使用像a这样的东西来传递我们的核心模型周围的可观察包装器,而不是让视图控制器本身加载它的模型。UserHolder``User
通过这样做,我们可以封装我们的重新加载逻辑,并在一个地方完成所有必需的更新,远离我们的视图控制器 - 从而简化了ProfileViewController
实现:
class ProfileViewController: UIViewController {
private let userHolder: UserHolder
private lazy var nameLabel = UILabel()
private lazy var headerView = HeaderView()
private lazy var followersLabel = UILabel()
init(userHolder: UserHolder) {
self.userHolder = userHolder
super.init(nibName: nil, bundle: nil)
}
override func viewDidLoad() {
super.viewDidLoad()
// Our view controller now only has to define how it'll
// *react* to a model change, rather than initiating it.
userHolder.addObserver(self) { vc, user in
vc.nameLabel.text = user.name
vc.headerView.backgroundColor = user.colors.primary
vc.followersLabel.text = String(user.followersCount)
}
}
}
要了解有关上述版本的观察者模式的更多信息 - 请查看“在Swift中处理可变模型”,或者由两部分组成的文章“Swift中的观察者”。
虽然以上是对我们原始实现的一个很好的改进,但让我们看看我们是否可以采取进一步的措施 - 特别是当涉及到我们向视图控制器公开的API时 - 而是将我们的模型值直接绑定到我们的UI。
从可观察到可绑定
不需要每个视图控制器观察其模型并定义关于应如何处理每个更新的明确规则,值绑定背后的想法是使我们能够通过简单地将每个模型数据与一个模型数据相关联来编写自动更新UI代码。 UI属性,以更具说明性的方式。
为了实现这一目标,我们首先要UserHolder
使用泛型Bindable
类型替换之前的类型。这种新类型将使任何值都绑定到任何UI属性,而无需为每个模型构建特定的抽象。让我们首先声明Bindable
和定义属性以跟踪其所有观察结果,并使其能够缓存通过它的最新值,如下所示:
class Bindable<Value> {
private var observations = [(Value) -> Bool]()
private var lastValue: Value?
init(_ value: Value? = nil) {
lastValue = value
}
}
接下来,让我们Bindable
观察,就像UserHolder
之前一样 - 但是关键的区别是我们会将观察方法保密:
private extension Bindable {
func addObservation<O: AnyObject>(
for object: O,
handler: @escaping (O, Value) -> Void
) {
// If we already have a value available, we'll give the
// handler access to it directly.
lastValue.map { handler(object, $0) }
// Each observation closure returns a Bool that indicates
// whether the observation should still be kept alive,
// based on whether the observing object is still retained.
observations.append { [weak object] value in
guard let object = object else {
return false
}
handler(object, value)
return true
}
}
}
请注意,我们现在不会使我们的观察处理代码在线程安全 - 因为它主要在UI层中使用 - 但是有关如何执行此操作的提示,请查看“避免Swift中的竞争条件”。
最后,我们需要一种方法来Bindable
在新模型可用时更新实例。为此我们将添加一个update
方法来更新可绑定的lastValue
并调用每个观察filter
,以便删除所有已经过时的观察:
extension Bindable {
func update(with value: Value) {
lastValue = value
observations = observations.filter { $0(value) }
}
}
可能有人认为,使用filter
应用副作用(就像我们上面做的那样)在理论上是不正确的,至少不是从严格的函数式编程角度来看,但在我们的例子中,它确实正是我们正在寻找的 - 而且因为我们“不依赖于操作的顺序,使用filter
是非常好的匹配,并且使我们免于基本上自己编写完全相同的代码。
有了上述内容,我们现在可以开始使用我们的新Bindable
类型了。我们首先向我们注入一个Bindable<User>
实例ProfileViewController
,而不是使用我们的视图控制器上的属性设置我们的每个视图,我们将在我们调用的专用方法中完成所有单独的设置viewDidLoad
:
class ProfileViewController: UIViewController {
private let user: Bindable<User>
init(user: Bindable<User>) {
self.user = user
super.init(nibName: nil, bundle: nil)
}
override func viewDidLoad() {
super.viewDidLoad()
addNameLabel()
addHeaderView()
addFollowersLabel()
}
}
我们的视图控制器已经开始看起来更简单了,我们现在可以自由地构建我们的视图设置代码,但是我们还是喜欢 - 因为通过值绑定,我们的UI更新不再需要在同一个方法中定义。
绑定值
到目前为止,我们已经定义了为实际开始将值绑定到UI而需要的所有底层基础架构 - 但要做到这一点,我们需要一个API来调用。addObservation
之前我们保密的原因是,我们将公开一个KeyPath
基于API的API,我们将能够使用它直接将每个模型属性与其相应的UI属性相关联。
就像我们在“Swift中关键路径的强大功能”中看到的那样,关键路径可以使我们构建一些非常好的API,使我们可以动态访问对象的属性,而无需使用闭包。让我们首先Bindable
使用API 扩展,让我们将模型的关键路径绑定到视图的关键路径:
extension Bindable {
func bind<O: AnyObject, T>(
_ sourceKeyPath: KeyPath<Value, T>,
to object: O,
_ objectKeyPath: ReferenceWritableKeyPath<O, T>
) {
addObservation(for: object) { object, observed in
let value = observed[keyPath: sourceKeyPath]
object[keyPath: objectKeyPath] = value
}
}
}
由于我们有时希望将值绑定到可选属性(例如text
on UILabel
),因此我们还需要一个额外的bind
重载,它接受objectKeyPath
一个可选的T
:
extension Bindable {
func bind<O: AnyObject, T>(
_ sourceKeyPath: KeyPath<Value, T>,
to object: O,
// This line is the only change compared to the previous
// code sample, since the key path we're binding *to*
// might contain an optional.
_ objectKeyPath: ReferenceWritableKeyPath<O, T?>
) {
addObservation(for: object) { object, observed in
let value = observed[keyPath: sourceKeyPath]
object[keyPath: objectKeyPath] = value
}
}
}
有了上述内容,我们现在可以开始将模型值绑定到UI,例如直接将我们的用户name
与将其呈现的text
属性相关联UILabel
:
private extension ProfileViewController {
func addNameLabel() {
let label = UILabel()
user.bind(\.name, to: label, \.text)
view.addSubview(label)
}
}
太酷了!也许更酷的是,因为我们将绑定API基于关键路径,所以我们完全免费获得对嵌套属性的支持。例如,我们现在可以轻松地将嵌套colors.primary
属性绑定到标题视图backgroundColor
:
private extension ProfileViewController {
func addHeaderView() {
let header = HeaderView()
user.bind(\.colors.primary, to: header, \.backgroundColor)
view.addSubview(header)
}
}
上述方法的优点在于,我们将能够更加有力地保证我们的UI将始终呈现我们模型的最新版本,而不需要我们的视图控制器真正做任何额外的工作。通过用关键路径替换闭包,我们还实现了更具声明性的API,并且如果我们忘记在设置模型观察时忘记捕获视图控制器作为弱参考,那么也消除了引入保留周期的风险。
变换
到目前为止,我们所有的模型属性都与UI对应的类型相同,但情况并非总是如此。例如,在我们之前的实现中,我们必须将用户的followersCount
属性转换为字符串,以便能够使用UILabel
- 来呈现它- 那么我们如何使用新的值绑定方法实现相同的功能呢?
一种方法是引入另一个bind
重载,它添加一个transform
参数,包含一个将值T
转换为所需结果类型的R
函数 - 然后在我们的观察中使用该函数来执行转换,如下所示:
extension Bindable {
func bind<O: AnyObject, T, R>(
_ sourceKeyPath: KeyPath<Value, T>,
to object: O,
_ objectKeyPath: ReferenceWritableKeyPath<O, R?>,
transform: @escaping (T) -> R?
) {
addObservation(for: object) { object, observed in
let value = observed[keyPath: sourceKeyPath]
let transformed = transform(value)
object[keyPath: objectKeyPath] = transformed
}
}
}
使用上面的转换API,我们现在可以通过传递变换轻松地将我们的followersCount
属性绑定到a :UILabel``String.init
private extension ProfileViewController {
func addFollowersLabel() {
let label = UILabel()
user.bind(\.followersCount, to: label, \.text, transform: String.init)
view.addSubview(label)
}
}
另一种方法将一直以引入更专门的版本的bind
直接之间进行转换Int
和String
特性,或者将它基于所述CustomStringConvertible
协议(这Int
和许多其它类型的符合) -但与上述的方法,我们可以灵活地改变任何值无论如何我们认为合适。
自动更新
虽然我们的新Bindable
类型使用关键路径实现了相当优雅的UI代码,但引入它的主要目的是确保我们的UI在底层模型被更改时自动保持最新,所以让我们看一下另一面 - 如何实际触发模型更新。
在这里,我们的核心User
模型由模型控制器管理,模型控制器每次应用程序变为活动时都会将模型与我们的服务器同步 - 然后调用update
它Bindable<User>
在整个应用程序的UI中传播任何模型更改:
class UserModelController {
let user: Bindable<User>
private let syncService: SyncService<User>
init(user: User, syncService: SyncService<User>) {
self.user = Bindable(user)
self.syncService = syncService
}
func applicationDidBecomeActive() {
syncService.sync(then: user.update)
}
}
以上内容真正令人高兴的是,我们UserModelController
可以完全不了解其用户数据的消费者,反之亦然 - 因为我们Bindable
充当双方的抽象层,既可以实现更高程度的可测试性,又可以实现一个更加分离的系统整体。
结论
通过将我们的模型值直接绑定到我们的UI,我们最终可以得到更简单的UI配置代码,消除常见错误(例如意外强烈捕获观察闭包中的视图对象),并确保所有UI值保持最新-date作为其基础模型的变化。通过引入诸如的抽象Bindable,我们还可以更清楚地将UI代码与核心模型逻辑分开。
在这篇文章中提出的观点在很大程度上受到官能团反应性编程的影响,而更完整的玻璃钢实现(如RxSwift)采取的值绑定的想法多通过引入双向绑定,并实现可观测值的建设进一步(例如如果我们只需要简单的单向绑定,那么像Bindable类型这样的东西可以使用更薄的抽象来完成我们需要的所有事情。
在以后的文章中,我们肯定会回到功能反应式编程和声明式UI编码风格这两个主题。在那之前,你怎么看?你有没有实现类似Bindable以前的东西,或者你会试试吗?请通过加我们的交流群 点击此处进交流群 ,来一起交流或者发布您的问题,意见或反馈。
谢谢阅读~点个赞再走呗!🚀
原文地址 https://www.swiftbysundell.com/posts/bindable-values-in-swift
网友评论