ios - 绑定(bind)模型和 View : how to observe object properties

标签 ios swift architecture rx-swift observer-pattern

我有一个结构类似于创建模型对象的表单的 View 。我正在尝试将表单元素( UIControl )绑定(bind)到模型属性,以便 View 在其相应的模型属性更改时自动更新,并在控件更改时更新模型(双向绑定(bind))。模型可以在 View 不知道的情况下更改,因为多个 View 可以链接到一个相同的模型属性。

方法 1:普通 Swift

我的问题如下:为了观察模型属性的变化,我尝试使用 KVO in Swift ,特别是 observe(_:changeHandler:)方法。

class Binding<View: NSObject, Object: NSObject, ValueType> {
    weak var object: Object?
    weak var view: View?

    var objectToViewObservation: NSKeyValueObservation?
    var viewToObjectObservation: NSKeyValueObservation?

    private var objectKeyPath: WritableKeyPath<Object, ValueType>
    private var viewKeyPath: WritableKeyPath<View, ValueType>

    init(betweenObject objectKeyPath: WritableKeyPath<Object, ValueType>,
         andView viewKeyPath: WritableKeyPath<View, ValueType>) {
        self.objectKeyPath = objectKeyPath
        self.viewKeyPath = viewKeyPath
    }

    override func bind(_ object: Object, with view: View) {
        super.bind(object, with: view)
        self.object = object
        self.view = view

        // initial value from object to view
        self.view![keyPath: viewKeyPath] = self.object![keyPath: objectKeyPath]

        // object --> view
        objectToViewObservation = object.observe(objectKeyPath) { _, change in
            guard var view = self.view else {
                // view doesn't exist anymore
                self.objectToViewObservation = nil
                return
            }

            guard let value = change.newValue else { return }
            view[keyPath: self.viewKeyPath] = value
        }

        // view --> object
        viewToObjectObservation = view.observe(viewKeyPath) { _, change in
            guard var object = self.object else {
                // object doesn't exist anymore
                self.viewToObjectObservation = nil
                return
            }

            guard let value = change.newValue else { return }
            object[keyPath: self.objectKeyPath] = value
        }
    }
}

但是我的模型的一些属性有类型 CustomEnum , CustomClass , Bool? , 和 ClosedRange<Int> ,并且要使用观察,我必须将它们标记为 @objc dynamic ,这产生了错误:
Property cannot be marked @objc because its type cannot be represented in Objective-C
方法二:使用 RxSwift rx.observe
我转向 RxSwift 和 rx.observe方法认为我可以解决这个问题,但同样的事情发生了(这次在运行时)。
// In some generic bridge class between the view and the model
func bind(to object: SomeObjectType) {
    object.rx
        .observe(SomeType.self, "someProperty")
        .flatMap { Observable.from(optional: $0) }
        .bind(to: self.controlProperty)
        .disposed(by: disposeBag)
}

方法 3:使用 RxSwift BehaviorRelays?

这是我第一次使用 RxSwift,我知道我应该为我的模型使用 BehaviorRelay,但是我不想更改我的所有模型属性,因为我的模型对象正在使用其他框架。我可以尝试实现一个桥,将模型属性转换为 BehaviorRelay,但我会遇到同样的问题:如何监听模型变化 .

In this question ,没有关于如何在不将所有模型属性重构为 RxSwift 的 Variable 的情况下监听属性更改的答案。 (目前已弃用)。

方法四:使用didSet swift 属性(property)观察员?
didSetwillSet普通 Swift 中的属性观察器允许监听变化,但是这需要用这些观察器标记模型中的所有属性,我觉得这很不方便,因为我的模型对象有很多属性。如果有办法在运行时添加这些观察者,这将解决我的问题。

我相信我想要实现的目标很常见,有一组修改模型对象的 View ,但是我找不到将模型正确链接到 View 的方法,以便在需要时自动更新。

基本上,我正在寻找以下问题之一的答案:
  • 有没有我忽略的东西,有没有更好的方法来实现我想要的?
  • 或者如何克服“属性不能被标记@objc”的问题?
  • 或如何在不更改模型的情况下将我的模型对象桥接到 BehaviorRelay?
  • 或如何添加 didSet运行时的观察者?
  • 最佳答案

    你说:

    I believe that what I am trying to achieve is quite common, having a set of views that modify a model object, however I can't find a way to properly link the model to the view, so that both auto-update when needed.



    其实这根本不常见。您没有提到的一个想法是将整个模型包装到行为中继中。然后这组 View 可以修改您的模型对象。

    反过来,您的每个 View 都可以观察行为中继中的模型并相应地更新。例如,这是 Redux 模式的基础。

    您还可以使用您的方法 #3 并使用属性包装器使代码更简洁:
    @propertyWrapper
    struct RxPublished<Value> {
        private let relay: BehaviorRelay<Value>
        public init(wrappedValue: Value) {
            self.relay = BehaviorRelay(value: wrappedValue)
        }
    
        var wrappedValue: Value {
            get { relay.value }
            set { relay.accept(newValue) }
        }
    
        var projectedValue: Observable<Value> {
            relay.asObservable()
        }
    }
    

    但是要明白,你遇到这个问题的全部原因不是因为 Rx 本身,而是因为你试图混合范式。您正在增加代码的复杂性。希望这只是重构期间的临时增加。

    旧答案

    你说你想让它“以便 View 在其相应的模型属性更改时自动更新,并在控件更改时更新模型(双向绑定(bind))。”

    IMO,这种思考问题的方式是不正确的。最好是独立于所有其他输出检查每个输出并直接处理它。为了解释我的意思,我将使用将°F转换为°C并返回的示例......

    这听起来像是使用 2-way binding 的一个很好的理由,但让我们看看?
    // the chain of observables represents a view model
    celsiusTextField.rx.text           // • this is the input view
        .orEmpty                       // • these next two convert
        .compactMap { Double($0) }     //   the view into an input model
        .map { $0 * 9 / 5 + 32 }       // • this is the model
        .map { "\($0)" }               // • this converts the model into a view
        .bind(to: fahrenheitTextField) // • this is the output view
        .disposed(by: disposeBag)
    
    fahrenheitTextField.rx.text
        .orEmpty
        .compactMap { Double($0) }
        .map { ($0 - 32) * 5 / 9 }
        .map { "\($0)" }
        .bind(to: celsiusTextField.rx.text)
        .disposed(by: disposeBag)
    

    上面的代码在没有双向绑定(bind)的情况下处理了文本字段之间的双向通信。它通过使用两个单独的 View 模型来做到这一点( View 模型是 text Observable 和 text Observer 之间的代码,如注释中所述。)

    我们可以看到很多重复。我们可以稍微干燥一下:
    extension ControlProperty where PropertyType == String? {
        func viewModel(model: @escaping (Double) -> Double) -> Observable<String> {
            orEmpty
                .compactMap { Double($0) }
                .map(model)
                .map { "\($0)" }
        }
    }
    

    您可能更喜欢与我上面使用的不同的错误处理策略。我力求简单,因为这是一个例子。

    但关键是每个可观察链都应该以特定效果为中心。它应该结合所有导致该效果的原因,在输入上实现某种逻辑,然后发出该效果所需的输出。如果您对每个输出单独执行此操作,您会发现根本不需要双向绑定(bind)。

    关于ios - 绑定(bind)模型和 View : how to observe object properties,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/62378497/

    相关文章:

    asp.net-mvc - 发送电子邮件属于应用程序的表示层还是业务层?

    ios - connectionDidFinishLoading 没有被调用,为什么?

    swift - Ubuntu 上的 PerfectLib : module does seem to reference obsolete generated interface

    ios - CoreData 测试实体是否为空

    swift - 获取日期之间的月份列表

    web-services - 移动和网络用户认证/授权架构

    Maven 多模块按功能但共享 webapp

    ios - (XCode 7.1.1) 如何让 ImageView 居中?

    ios - 当我的单元格中没有显示右箭头时

    RLMLinkingObjects 的 iOS 谓词( Realm )