用RxSwift实现SwiftUI中类似@State,@Binding效果

1,431 阅读2分钟

第一步实现@State,@Binding一样声明属性

我们可以使用Swwift新出的@propertyWrapper来实现代码如下

@propertyWrapper
struct Behavior<Value> {
    var wrappedValue:Value {
        get{
           try! _subject.value()
        }
        set{
            _subject.onNext(newValue)
        }
    }
    private let _subject:BehaviorSubject<Value>
    init(wrappedValue: Value){
        _subject = BehaviorSubject(value: wrappedValue)
    }
    init(initialValue: Value){
        _subject = BehaviorSubject(value: initialValue)
    }
}
struct Test {
    @Behavior var user:Bool = true
}

第二步实现@State,@Binding一样用$加属性名称可以获取属性包装本身

申明一个 projectedValue可读属性就可以实现。返回值类型就是$可以获取到的类型

var projectedValue:Behavior<Value> { self }

第三步实现ObservableType协议,扩展ObservableType实现几个绑定方法

extension ObservableType {

    func bind(to relays: Behavior<Element>...) -> Disposable {
        bind(to: relays)
    }

    func bind(to relays: Behavior<Element?>...) -> Disposable {
        map { $0 as Element? }.bind(to: relays)
    }
    private func bind(to relays: [Behavior<Element>]) -> Disposable {
        subscribe { e in
            switch e {
            case let .next(element):
                relays.forEach {
                    $0.wrappedValue = element
                }
            case let .error(error):
                print("Behavior error to publish relay: \(error)")
            case .completed:
                break
            }
        }
    }

}

上面代码会爆一个错误$0.wrappedValue = element Behavior是一个结构体,遍历时候是不可变的当给他属性赋值的时候会 Cannot assign to property: '$0' is immutable。 我们用nonmutating,来告诉编译器不会修改实例内部的值,也就是set时,不会改变任何其他的变量。

var wrappedValue:Value {
        get{
           try! _subject.value()
        }
        nonmutating set{
            _subject.onNext(newValue)
        }
}

现在只剩最后一步,我们先看一下SwiftUI中的有段代码

@State var size = CGSize()
func test(){
    let height:Binding<CGFloat> = $size.height
}

仔细观察发现$size.height也是包装器类型,我们现在来实现它 我们需要知道这个@dynamicMemberLookup它是swift4.2出的,具体详情问度。 在我们案例中实现如下

    private let disposeBag = DisposeBag()
    subscript<Subject>(dynamicMember keyPath: WritableKeyPath<Value, Subject>)->Behavior<Subject>{
        var recursive = true
        let behavior = Behavior<Subject>(wrappedValue: wrappedValue[keyPath:keyPath])
        self.map{ $0[keyPath:keyPath] }
            .filter({ (obj) -> Bool in
                if recursive {
                    return false
                }
                recursive = true
                return true
            })
            .bind(to: behavior)
            .disposed(by: disposeBag)
        
        behavior.filter({ (obj) -> Bool in
            if recursive {
                recursive = false
                return false
            }
            recursive = true
            return true
        }).withLatestFrom(self) { (obj1, obj2) -> Value in
            var value = obj2
            value[keyPath:keyPath] = obj1
            return value
        }
        .bind(to: self)
        .disposed(by: disposeBag)
        
        return behavior
    }

使用recursive是防止递归

最后我们使用它做简单用户资料提交案例

界面如下

案例完整的代码如下

class ViewController: UITableViewController {

    
    @IBOutlet weak var submitButton: UIButton!
    @IBOutlet weak var mobileTextField: UITextField!
    @IBOutlet weak var hobbyTextField: UITextField!
    @IBOutlet weak var ageTextField: UITextField!
    @IBOutlet weak var nameTextField: UITextField!
    let disposeBag = DisposeBag()
    override func viewDidLoad() {
        super.viewDidLoad()
        
        mobileTextField.rx.shouldChangeCharacters = { (textField,range,string) ->Bool in
            ///这里可以做一些判断输入规则
            return true
        }
        
        
        nameTextField.rx.text <-> $user.name
        ageTextField.rx.text <-> $user.age
        hobbyTextField.rx.text <-> $user.hobby
        mobileTextField.rx.text <-> $user.mobile
        $user.map{
            if let name = $0.name,name.count > 0,
                let age = $0.age,age.count > 0,
                let hobby = $0.hobby,hobby.count > 0,
                let mobile = $0.mobile,mobile.count > 0{
                return true
            }
            return false
        }
        .bind(to: submitButton.rx.isEnabled)
        .disposed(by: disposeBag)
        
        $user.subscribe { (event) in
            print(event)
        }.disposed(by: disposeBag)
        
    }
    @Behavior var user = User()
    struct User:Codable {
        var name:String?
        var age:String?
        var hobby:String?
        var mobile:String?
    }
}



class TextFieldDelegateProxy: DelegateProxy<UITextField,UITextFieldDelegate>,DelegateProxyType,UITextFieldDelegate
{
    init(textField:ParentObject) {
        super.init(parentObject: textField, delegateProxy: TextFieldDelegateProxy.self)
    }
    static func registerKnownImplementations() {
        self.register{ TextFieldDelegateProxy(textField: $0) }
    }
    static func currentDelegate(for object: UITextField) -> UITextFieldDelegate? {
        object.delegate
    }
    
    static func setCurrentDelegate(_ delegate: UITextFieldDelegate?, to object: UITextField) {
        object.delegate = delegate
    }
    typealias ShouldChangeCharactersBlock = (UITextField,NSRange,String)->Bool
    var shouldChangeCharactersIn:ShouldChangeCharactersBlock?
    func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
        shouldChangeCharactersIn?(textField,range,string) ?? true
    }
}

extension Reactive where Base:UITextField {
    typealias ShouldChangeCharactersBlock = (UITextField,NSRange,String)->Bool
    var shouldChangeCharacters:ShouldChangeCharactersBlock?{
        set{
            TextFieldDelegateProxy.proxy(for: base).shouldChangeCharactersIn = newValue
        }
        get{
            TextFieldDelegateProxy.proxy(for: base).shouldChangeCharactersIn
        }
    }
}
infix operator <-> : DefaultPrecedence
@discardableResult
func <-> (property: ControlProperty<String?>, relay: Behavior<String?>) ->Disposable {
    let bindToUIDisposable = property.orEmpty.filter{ $0.count > 0 }
        .bind(to: relay)
    let bindToRelay = relay.bind(to: property)
    return Disposables.create(bindToUIDisposable, bindToRelay)
}
@discardableResult
func <-> (property: ControlProperty<String?>, relay: Behavior<String>) ->Disposable {
    let bindToUIDisposable = property.orEmpty.filter{ $0.count > 0 }
        .bind(to: relay)
    let bindToRelay = relay.bind(to: property)
    return Disposables.create(bindToUIDisposable, bindToRelay)
}