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
Equatableyou’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