Swift 5.1 更优雅的绑定数据和UI

2,721

视图(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
    }
    
}


下面附图

点击前 点击后
alt
alt

代码详解

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直接使用