Contents

pimcoumans/didupdate

SwiftUI inspired state observing without SwiftUI

📦 Installation

To add this dependency to your Xcode project, select File -> Add Package and enter this repository’s URL: https://github.com/PimCoumans/DidUpdate

🤷 But, why?

SwiftUI is great, but for now I feel more comfortable using plain old UIKit for the more complex parts of my apps. I do love how SwiftUI lets you define state and have it automatically update all your views when anything changes. I wanted that, but not with the overhead of importing SwiftUI or Combine and using a bunch of publishers, or learning a whole new reactive library.

So I reverse-over-engineered the parts I liked and introduced the ability to add update handlers to your bindings (ValueProxy in DidUpdate land).

Now you can have a tiny reactive-ish architecture for your UIKit views too!

↔️ What does it do exactly?

The two main features are

  • Inform you when a specific property in your model class has been updated. If your value conforms to Equatable you’ll know when its value was actually changed.
  • Pass along two-way binding property wrappers that can read and update properties on your model class, making sure its didSet { } is called as well. There’s also the convenient availability to create bindings to nested properties using KeyPath subscripts (like $viewModel.someFrame.size.width).

✨ How can I do this?

To enable this magic, make sure your model object conforms to ObservableState and hold onto it using the @ObservedState property wrapper in your view (controller). For all your model’s properties use @ObservedValue when you want these to be observable. Take another gander at the example above to see how it all fits together.

Handling updates/changes

On all value properties you get a bunch of didUpdate methods, allowing you to provide update handlers that are executed when the property is updated.

let observer = $viewModel.username.didUpdate { username in
    print("Username updated to: \(username)")
}

or when you have a @ValueProxy set in some other view:

let observer = $username.didUpdate { username in
    print("Username updated to: \(username)")
}

Ideally you’d store those returned observers in an array, much like [AnyCancellable]:

var observers: [StateValueObserver] = []
func addObservers() {
    $username.didUpate { newValue in
        // ...
    }.add(to: &observers)
}

Besides didUpdate there’s also didChange indicating the value has actually changed (meaning not considered equal when conforming to Equatable):

let observer = $viewModel.username.didChange { username in
    print("Username has changed to: \(username)")
}

and didChange(comparing:) to compare the values at a given key path:

// Update handler only called when username.isEmpty changes 
let observer = $viewModel.username.didChange(comparing: \.isEmpty) { username in
    if !username.isEmpty {
        print("Username no longer empty")
    } else {
        print("Username empty again")
    }
}

Two-way binding (value proxies)

To pass around two-way bindings to these values, you can create a ValueProxy by accessing the projected value (with $) of your object’s property wrapper:

class SubView: UIView {
    @ValueProxy var username: String
    init(username: ValueProxy<String>) {
        _username = username
    }
}
// in your main view, access the projected value using the `$` prefix 
let someSubView = SubView(username: $viewModel.username)

Changing the username property in SubView in this example would automatically update the property in your viewModel. Reading the username property in SubView would give you the actual up-to-date value, even when changed from somewhere else (just like you’d expect from @Binding).

❓That’s it?

That’s about it! Please let me know if you have any questions.

Package Metadata

Repository: pimcoumans/didupdate

Default branch: main

README: README.md