Search code examples
swiftreactivekit

How to turn a standard model class/struct into observable properties?


I am using ReactiveKit with their Bond extension, and I can't really figure out how to do something that feels kind of basic.

Let's say I have a User model in my app. Something like this.

class User: Codable {
  var id: String
  var firstName: String?
  var avatar: String?
}

The content comes from a remote API, by making it confirm to Codable everything works nice and easy.

But let's also say for example that I would like a bidirectional binding from a model property to some UI state; that is not possible since none of my properties confirm to the BindableProtocol. Or if I want to observe changes to my model's properties, this is not possible either of course.

So my question is: how do I turn my model properties in actual observable Properties, without breaking the existing User model and behavior? For example, it still needs to be Codable. Do I make a second ObservableUser class, and then have didSet in all properties on the User model to write changes to the ObservableUser? And something similar on the ObservableUser to write changes back to the User? That seems horrible and hopefully not the recommenced way forward.


Solution

  • You don't need to create another object to make model properties observable. But there is definitely an overhead of implementing the decoder & encoder methods and coding keys enum as below which is also a standard practice in many cases,

    class User: Codable {
        var id: String = ""
        var firstName = Observable<String?>(nil)
        var avatar = Observable<String?>(nil)
    
        private enum CodingKeys: String, CodingKey {
            case id, firstName, avatar
        }
    
        required init(from decoder: Decoder) throws {
            let container = try decoder.container(keyedBy: CodingKeys.self)
            self.id = try container.decode(String.self, forKey: .id)
            self.firstName.value = try container.decode(String.self, forKey: .firstName)
            self.avatar.value = try container.decode(String.self, forKey: .avatar)
        }
    
        func encode(to encoder: Encoder) throws {
            var container = encoder.container(keyedBy: CodingKeys.self)
            try container.encode(self.id, forKey: .id)
            try container.encode(self.firstName.value, forKey: .firstName)
            try container.encode(self.avatar.value, forKey: .avatar)
        }
    }
    

    Now you are able to bind any UIView element.


    Normally, with reactive approach, ViewModel is where you create bindable properties that provide value from your model to view and update model property by keeping the model invisible by the view.