我有一个结构类似于创建模型对象的表单的 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)观察员?didSet
和 willSet
普通 Swift 中的属性观察器允许监听变化,但是这需要用这些观察器标记模型中的所有属性,我觉得这很不方便,因为我的模型对象有很多属性。如果有办法在运行时添加这些观察者,这将解决我的问题。我相信我想要实现的目标很常见,有一组修改模型对象的 View ,但是我找不到将模型正确链接到 View 的方法,以便在需要时自动更新。
基本上,我正在寻找以下问题之一的答案:
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/