SwiftUI: Things Change
This year at WWDC 2019, Apple announced SwiftUI, a brand new way to build user interfaces on Apple devices. This framework is a complete departure from UIKit, and I โ like many others โ have been eager to experiment with this new tool.
SwiftUI allows you to describe your views using a declarative syntax as opposed to imperative. You specify which subviews are displayed in a view, which data those subviews rely on, and any modifiers to apply such as how they are positioned, sized, and styled. The following is an example of a simple view in SwiftUI.
Note: Much has already been written about SwiftUI in the few weeks since the release of Xcode 11 beta 1. This post is not intended to be a complete introduction to SwiftUI.
Another key feature of SwiftUI โ and the subject of this post โ is the BindableObject protocol. The docs offer the following brief summary of BindableObject:
An object that serves as a viewโs model.
Those who are familiar with MVVM can think of this object as the โview model.โ Types that implement this protocol provide data that is needed by the view and provide a mechanism for informing the view when the data has changed. This mechanism is called a Publisher and is part of yet another brand new Apple framework called Combine. As you can see, there was no shortage of new frameworks at this yearโs conference!
I wonโt go into specifics about Combine in this post, but if you are familiar with RxSwift โ or Reactive Programming in general โ know this is the Apple-sanctioned framework for building Reactive apps for Apple platforms. Shai Mishali has published a handy โCheat Sheetโ to document analogs between RxSwift and Combine.
I mentioned before that a BindableObject informs a view when the model data has changed, but this is not strictly true anymore. Before the release of Xcode 11 beta 4, BindableObject declared the following property:
var didChange: Self.PublisherType { get }
BindableObjects were expected to emit an event on this Publisher after the model data had changed (as the past-tense property name implied). As of beta 4, however, this name โ and subsequently the implication โ has changed. It is now called willChange.
We made a significant change to BindableObject: the requirement is now to publish a willChange *before* making any mutation to properties that you might read in body.
— Luca Bernardi (@luka_bernardi) July 17, 2019
As before you should publish (and mutate) on the main thread.
This may seem a bit confusing and unintuitive to you, as it does to me. The requirement of didChange is clear: events are Published after the model has changed to indicate that the view should be redrawn. Now with willChange, itโs not self-evidently clear exactly when events should be emitted on this Publisher. Should an event be emitted synchronously before the model is updated, such as from the willSet property observer of a state variable? Or perhaps it should emit even earlier, such as when the user signals intent to update state by, say, tapping a refresh button?
The following explanation is offered:
I do agree it might not map directly to the typical completion block mental model, but you can think of it as the begin of an update transaction.
— Luca Bernardi (@luka_bernardi) July 17, 2019
We made the change because it allows us to do a better job in coalescing updates; especially in the presence of animations.
This lends support for the willSet strategy. Indeed, while offering a critique of anotherโs code, Luca provides explicit validation of this approach:
This specific code is not correct because if you dispatch async the call to send() it will happen after you did the mutation so effectively is not a willChange anymore.
— Luca Bernardi (@luka_bernardi) July 17, 2019
The willSet approach is perfectly fine.
Others have speculated how this might be implemented under the hood, with one person drawing an analogy between the willChange Publisher and UIView.setNeedsLayout().
Based on my interpretation of this new information, Iโve implemented a Property Wrapper which stores a model and publishes events at the appropriate times. The type is called NextValue:
NextValue exposes a projectedValue property which is a Publisher. This property is accessed using the $ prefix and can be used as the willChange property on a BindableObject:
class ContentViewModel: BindableObject {
@NextValue var model = MyModel(name: "Dalton")
var willChange: NextValue<String>.Publisher { $model }
}
Things are changing with every new Xcode beta. This is just one example of the many additions, changes, and deprecations we have seen so far in these four beta versions. While at times these changes can be frustrating and confusing, it is motivating to see Apple frameworks engineers engaging in discussions with the Swift community and to see community members trading strategies, tools, and help with one another. These discussions undoubtedly lead to better APIs, better tooling, and a better community as a whole. Change is good.