The binding of views (UI) and Data (Data) has always been a top priority in software development. The @propertyWrapper and @dynamicMemberLookup in Swift 5.1 bring some better features that allow us to more elegantly implement the following.

    
import UIKit
import Extend

class ViewController: UIViewController {
    
    @IBOutlet weak var label:UILabel!

    // @propertyWrapper wrapper to listen to the property
    @Observable var index:Int = 9
    @Observable var textValue:String = "Hello word"
    @Observable var textAlignment:NSTextAlignment=.left
        
    override func viewDidLoad(a) {
        super.viewDidLoad()
        
        // Bind the textAlignment property of UILable to the value textAlignment
        label.bind.textAlignment = _textAlignment
        // Bind the text property of UILabel
        label.bind.text = Results: [\(_index)] \(_textValue)"
    }

    @IBAction func onTapped(a) {
        // Change the value of the listening property, and the UI changes automatically
        textValue = "UI has been updated."
        textAlignment = .center
        index = 10}}Copy the code

The following chart

Click on the former After clicking on

Code,

Label. bind is our extended bind attribute


extension NSObjectProtocol {
    public var bind:Bind<Self> { return Bind<Self>(wrappedValue: self)}}Copy the code

The Bind is a struct that we implement using the @dynamicMemberLookup feature


@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 inthis? [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 inthis? [keyPath: keyPath] = changed.new }return observerValue
        }
    }
}
Copy the code

In this way, the Bind attribute is available. The syntax lets the compiler automatically look up all the properties of the bound object and update the value by binding the keyPath to the @Observable wrapped property.

Struct Observable has many similar examples on the website. The implementation here may be a little different from the one on the Internet, but the principle is similar. Due to personal time constraints, I will not elaborate more here

String interpolation

This allows you to bind any property to an Observable variable. String is usually a special, most of the time is not a single value of the decision, and even to multiple variables or values, so you need to use ExpressibleByStringInterpolation agreement

Time, not here to explain the principle, anyway, a lot of online. Put code directly


To accommodate more types and optional values, add a few more methods
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) { _ incallback() } } getValue = { storage.value? .description ?? defaultValue.description } } }public enum StringComment {
        case text(String)
        case observable(Delegate)}public struct StringInterpolation: StringInterpolationProtocol {
        
        public var comments:[StringComment] = []
        
        /// Allocate enough space to hold double text
        public init(literalCapacity: Int, interpolationCount: Int) {
            comments.reserveCapacity(interpolationCount)
        }
        
        /// Add plain concatenation text
        public mutating func appendLiteral(_ literal: String) {
            comments.append(.text(literal))
        }
        
        /// add changeable values
        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
            }
        }
    }
Copy the code

Then add a dynamicMemberLookup implementation method to Bind


    public subscript(dynamicMember keyPath: WritableKeyPath<T.String? - > >)ObservableString {
        nonmutating set {
            let this = wrappedValue
            let updateBlock:() -> Void= {[weak this] inthis? [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 inthis? [keyPath: keyPath] = changed.new }return "\(observerValue)"}}Copy the code

This will do the opening scene.

        // Bind the text property of UILabel
        label.bind.text = Results: [\(_index)] \(_textValue)"
Copy the code

Time relation, will not elaborate for the moment, interested can download Github code for detailed analysis, or can be directly used after star with SwiftPackage age