DidUpdate

0.11.0

SwiftUI inspired state observing, without SwiftUI
PimCoumans/DidUpdate

What's New

v0.11.0

2024-03-18T15:28:40Z

Fixed issue with multiple update handlers on single compound ReadOnlyProxy.

Full Changelog: 0.10.0...0.11.0

DidUpdate

SwiftUI inspired state observing without SwiftUI

So, like ObservableObject but without any of that SwiftUI or Combine stuff

class MyView: UIView {
    /// Conform your model classes to `ObservableState`
    class ViewModel: ObservableState {
        /// Use `ObservedValue` for your model’s properties
        @ObservedValue var count: Int = 0
    }

    class StepperView: UIView {
        /// Store passed through bindings with `ValueProxy`
        @ValueProxy var count: Int

        lazy var minusButton = UIButton(frame: .zero, primaryAction: UIAction { [unowned self] _ in
            count -= 1
        })
        lazy var plusButton = UIButton(frame: .zero, primaryAction: UIAction { [unowned self] _ in
            count += 1
        })

        init(count: ValueProxy<Int>) {
            self._count = count
            super.init(frame: .zero)
            self.addSubview(minusButton)
            self.addSubview(plusButton)
        }
    }

    @ObservedState var viewModel = ViewModel()
    var observers: [StateValueObserver] = []

    lazy var countLabel = UILabel()
    // Pass value proxy to ViewModel’s count property
    lazy var stepper = StepperView(count: $viewModel.count)

    func setupView() {
        addSubview(stepper)
        // Use an update handler to set the label’s text when count updates
        $viewModel.count.didUpdate { [weak self] count in
            self?.countLabel.text = "\(count)"
        }.add(to: &observers)
    }
}

(basic counter sample code demonstrating updating a ValueProxy and didUpdate logic)

📦 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.

Description

  • Swift Tools 5.7.0
View More Packages from this Author

Dependencies

  • None
Last updated: Sun Oct 20 2024 02:37:08 GMT-0900 (Hawaii-Aleutian Daylight Time)