I don't think this is true for examples I'm thinking of. We can take an example straight from Apple's code samples. In the "Working with UI Controls" sample there is a section ("Delay Edit Propagation") that details how to put a form in "edit" mode so that you can make edits, which you can either save or cancel out of.
If we were to approach this in a declarative way I think we'd want to hold onto some optional draft state in our view:
struct ProfileHost: View {
@State var draftProfile: Profile?
@EnvironmentObject var currentProfile: Profile
}
So that when it's non-nil (meaning the draft is active) we show a view that represents an editable form of the profile data, and when it's nil (the draft is not active) we show the current profile info in a non-editable style:
if let draft = self.$draftProfile.unwrap() {
ProfileEditor(profile: draft)
} else {
ProfileSummary(profile: self.currentProfile)
}
That is very declarative to me because we are allowing the optionality of the Profile state to dictate what view is shown and hidden, and the ProfileEditor naturally gets its binding from the state.
And the draft obtained from the unwrap() operation is definitely connected to the original self.$draftProfile binding. Any mutations made to it in the child view will be instantly seen in the parent.
However, this is not possible with SwiftUI right now, so instead Apple suggests holding onto some additional state (in this case its an editMode, but might as well be a boolean):
struct ProfileHost: View {
@Environment(\.editMode) var mode
@State var draftProfile = Profile.default
@EnvironmentObject var currentProfile: Profile
}
There's a few things not right about this domain modeling. First we have a boolean value and a profile value. That's more data than is necessary. Also, since we can't use an optional Profile? we are forced to provide a fallback value of User.default, even though that value should never be displayed. It should only ever start the current profile. This is a strange choice we are being forced to make, and we shouldn't have to.
And then that domain modeling leads one to the following kind of view hierarchy:
if self.mode?.wrappedValue == .active {
ProfileEditor(profile: self.$draftProfile)
} else {
ProfileSummary(profile: self.currentProfile)
}
This is not as declarative to me because we need some secondary data to determine which view is displayed, and then use completely unrelated data to pass down to the child view.
The code sample I shared about using unwrap() shows the similarities with the alert, sheet and navigation APIs. It is allowing optional state to drive the presence and dismissal of a child view. It is not true that the only reasonable thing to do is dismiss. We can also make mutations to the binding while it is present.
Yeah absolutely, and I think that if you were to try to improve the fallback solution to allow for that placeholder logic you would invariably be led back to an operator that looks like unwrap.