视图(UI)和数据(Data)的绑定从来都是软件开发中的重中之重。Swift 5.1 中的 @propertyWrapper
和 @dynamicMemberLookup
带来了一些更好的特性,使我们可以更优雅的实现如下效果。
import UIKit
import Extend
class ViewController: UIViewController {
@IBOutlet weak var label:UILabel!
// @propertyWrapper 包装的可监听的属性
@Observable var index:Int = 9
@Observable var textValue:String = "Hello word"
@Observable var textAlignment:NSTextAlignment = .left
override func viewDidLoad() {
super.viewDidLoad()
// 绑定 UILable 的 textAlignment 属性和值 textAlignment
label.bind.textAlignment = _textAlignment
// 绑定 UILabel 的 text 属性
label.bind.text = "结果:[\(_index)] \(_textValue)"
}
@IBAction func onTapped() {
// 改变可监听的属性值,UI就自动变化了
textValue = "UI 更新了"
textAlignment = .center
index = 10
}
}
下面附图
点击前 | 点击后 |
---|---|
代码详解
label.bind
是我们扩展出来的bind
属性
extension NSObjectProtocol {
public var bind:Bind<Self> { return Bind<Self>(wrappedValue: self) }
}
其中的Bind
是我们利用@dynamicMemberLookup
特性实现的一个struct
@propertyWrapper
@dynamicMemberLookup
public struct Bind<T:AnyObject> {
public var wrappedValue: T
public init(wrappedValue: T) {
self.wrappedValue = wrappedValue
}
public subscript<Subject>(dynamicMember keyPath: WritableKeyPath<T, Subject>) -> Observable<Subject> {
nonmutating set {
let this = wrappedValue
let updateBlock:(Observed<T, Subject>) -> Void = { [weak this] changed in
this?[keyPath: keyPath] = changed.new
}
newValue.notify(this, didChange: updateBlock)
updateBlock(Observed<T, Subject>(setValue:newValue.wrappedValue, notify: this))
}
get {
let this = wrappedValue
let value = wrappedValue[keyPath: keyPath]
let observerValue = Observable<Subject>(wrappedValue: value)
observerValue.notify(this) { [weak this] changed in
this?[keyPath: keyPath] = changed.new
}
return observerValue
}
}
}
这样,Bind属性就可以使用.
语法让编译器自动查找所有绑定对象的属性, 从而通过@Observable
包装的属性来绑定keyPath
更新值。
struct Observable
网上有很多类似的例子,我这里实现的和网上的可能有点不一样,但其原理相似,因为个人时间紧张,这里就不过多阐述了,感兴趣的朋友可以访问github下载源码查看
字符串插值
通过上面的方式就实现了任意属性和Observable
变量的绑定。但字符串通常特殊,很多时候不是单一的值决定,甚至是多个变量或值共同决定,所以需要用到ExpressibleByStringInterpolation
协议
时间关系,这里先不阐述原理,反正网上一大堆。直接放代码
// 这里只保留关键代码 为了适应更多类型和可选值,还是要多加几个方法的
public struct ObservableString: ExpressibleByStringInterpolation, ExpressibleByStringLiteral, CustomStringConvertible {
public struct Delegate: CustomStringConvertible {
let getValue:() -> String
public let notify:(AnyObject, @escaping () -> Void) -> Void
public var description: String { return getValue() }
public init<V>(_ storage:Observable<V?>, `default` defaultValue: V) where V : CustomStringConvertible {
notify = { (target, callback) in
storage.notify(target) { _ in callback() }
}
getValue = { storage.value?.description ?? defaultValue.description }
}
}
public enum StringComment {
case text(String)
case observable(Delegate)
}
public struct StringInterpolation: StringInterpolationProtocol {
public var comments:[StringComment] = []
/// 分配足够的空间来容纳双倍文字的文本
public init(literalCapacity: Int, interpolationCount: Int) {
comments.reserveCapacity(interpolationCount)
}
/// 增加普通拼接文本
public mutating func appendLiteral(_ literal: String) {
comments.append(.text(literal))
}
/// 加入可改变的值
public mutating func appendInterpolation<V>(_ storage: Observable<V?>, `default` value:V) {
comments.append(.observable(Delegate(storage, default: value)))
}
}
public let comments:[StringComment]
public init(stringInterpolation: StringInterpolation) {
comments = stringInterpolation.comments
}
public init(stringLiteral value: StringLiteralType) {
comments = [.text(value)]
}
public var description: String {
return comments.joined(separator: "") { comment in
switch comment {
case .text(let value): return value
case .observable(let delegate): return delegate.description
}
}
}
然后修改之前的Bind
额外加一个dynamicMemberLookup
的实现方法
public subscript(dynamicMember keyPath: WritableKeyPath<T, String?>) -> ObservableString {
nonmutating set {
let this = wrappedValue
let updateBlock:() -> Void = { [weak this] in
this?[keyPath: keyPath] = newValue.description
}
for comment in newValue.comments {
if case .observable(let delegate) = comment {
delegate.notify(this, updateBlock)
}
}
updateBlock()
}
get {
let this = wrappedValue
let value = wrappedValue[keyPath: keyPath] ?? ""
let observerValue = Observable<String>(wrappedValue: value)
observerValue.notify(this) { [weak this] changed in
this?[keyPath: keyPath] = changed.new
}
return "\(observerValue)"
}
}
这样就可以做到开篇中的效果。
// 绑定 UILabel 的 text 属性
label.bind.text = "结果:[\(_index)] \(_textValue)"
时间关系,暂时就不详细阐述,感兴趣的可以下载github代码详细分析,也可以star
后用 SwiftPackageManage
直接使用