The dollar is not a prefix, it seems to be a generated code for property wrapper, and each kind of property wrapper can determine which value it return via this dollar sign
State and ObservedObject are popular property wrappers in SwiftUI
/// A wrapper of the underlying `ObservableObject` that can create /// `Binding`s to its properties using dynamic member lookup. @dynamicMemberLookup publicstructWrapper{
/// Creates a `Binding` to a value semantic property of a /// reference type. /// /// If `Value` is not value semantic, the updating behavior for /// any views that make use of the resulting `Binding` is /// unspecified. publicsubscript<Subject>(dynamicMember keyPath: ReferenceWritableKeyPath<ObjectType, Subject>) -> Binding<Subject> { get } }
publicinit(initialValue: ObjectType)
publicinit(wrappedValue: ObjectType)
publicvar wrappedValue: ObjectType
publicvar projectedValue: ObservedObject<ObjectType>.Wrapper { get } }
Derived State
If we have a struct with @State in a SwiftUI view, we can access its property as Binding. This derived state mechanism is done via Dynamic keypath member lookup feature of Swift 5.1
/// A value and a means to mutate it. @available(iOS 13.0, OSX10.15, tvOS 13.0, watchOS 6.0, *) @propertyWrapper @dynamicMemberLookup publicstructBinding<Value> { /// Creates a new `Binding` focused on `Subject` using a key path. publicsubscript<Subject>(dynamicMember keyPath: WritableKeyPath<Value, Subject>) -> Binding<Subject> { get } }
So if use $viewModel.image we can access its property as Binding
1 2 3
viewModel.image // UIImage? $viewModel.image // Binding<UIImage?> $viewModel.$image // Value of type 'ViewModel' has no member '$image'
Conclusion
So now we know how to get Binding from State and ObservableObject, and the mysterious dollar sign. These are both convenient but confusing at first, but if we use it more, it will make more sense and hopefully we can learn to do the same for our own property wrappers
In case we have to modify state when another state is known, we can encapsulate all those states in ObservableObject and use onReceive to check the state we want to act on.
If we were to modify state from within body function call, we will get warnings
Modifying state during view update, this will cause undefined behavior.
This is similar to the warning when we change state inside render in React
For example, when we get an image, we want to do some logic based on that image and modify result state. Here we use var objectWillChange = ObservableObjectPublisher() to notify state change, and because onReceive requires Publisher, we use let imagePublisher = PassthroughSubject<UIImage, Never>()
Note that we use $ prefix from a variable to form Binding
classContact: ObservableObject{ @Publishedvar name: String @Publishedvar age: Int
init(name: String, age: Int) { self.name = name self.age = age }
funchaveBirthday() -> Int { age += 1 return age } }
let john = Contact(name: "John Appleseed", age: 24) john.objectWillChange.sink { _inprint("\(john.age) will change") } print(john.haveBirthday()) // Prints "24 will change" // Prints "25"
We should use @Published
1 2 3 4 5 6 7
classViewModel: ObservableObject{ var objectWillChange = ObservableObjectPublisher()
In Beta 5 ObjectBinding is now defined in Combine as ObservableObject (the property wrapper is now @ObservedObject). There is also a new property wrapper @Published where we automatically synthesize the objectWillChange publisher and call it on willSet.
It’ll objectWillChange.send() in the property willSet it’s defined on. It just removes the boilerplate that you had to write before but otherwise behaves the same.
State vs ObservedObject
If we were to use @State instead of @ObservedObject, it still compiles, but after we pick an image, which should change the image property of our viewModel, the view is not reloaded.
‘wrappedValue’ is unavailable: @Published is only available on properties of classes
@State is for internal usage within a view, and should use struct and primitive data structure. SwiftUI keeps @State property in a separate memory place to preserve it during many reload cycles.
@Observabled is meant for sharing reference objects across views
To to use @State we should use struct, and to use onReceive we should introduce another Publisher like imagePublisher
The dollar sign for State to access nested properties, like $viewModel.image is called derived Binding, and is achieved via Keypath member lookup feature of Swift 5.1.
Take a look at projectedValue: Binding<Value> from State and subscript<Subject>(dynamicMember keyPath from Binding
/// A value and a means to mutate it. @available(iOS 13.0, OSX10.15, tvOS 13.0, watchOS 6.0, *) @propertyWrapper @dynamicMemberLookup publicstructBinding<Value> {
/// The transaction used for any changes to the binding's value. publicvar transaction: Transaction
/// Initializes from functions to read and write the value. publicinit(get: @escaping () -> Value, set: @escaping (Value) -> Void)
/// Initializes from functions to read and write the value. publicinit(get: @escaping () -> Value, set: @escaping (Value, Transaction) -> Void)
/// Creates a binding with an immutable `value`. publicstaticfuncconstant(_ value: Value) -> Binding<Value>
/// The value referenced by the binding. Assignments to the value /// will be immediately visible on reading (assuming the binding /// represents a mutable location), but the view changes they cause /// may be processed asynchronously to the assignment. publicvar wrappedValue: Value { getnonmutatingset }
/// The binding value, as "unwrapped" by accessing `$foo` on a `@Binding` property. publicvar projectedValue: Binding<Value> { get }
/// Creates a new `Binding` focused on `Subject` using a key path. publicsubscript<Subject>(dynamicMember keyPath: WritableKeyPath<Value, Subject>) -> Binding<Subject> { get } }
In theory, this should be triggered every time this view appears. But in practice, it is only called when it is pushed on navigation stack, not when we return to it.
So if user goes to a bookmark in a bookmark list, unbookmark an item and go back to the bookmark list, onAppear is not called again and the list is not updated.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
import SwiftUI
structBookmarksView: View{ let service: Service @Statevar items: [AnyItem] @EnvironmentObjectvar storeContainer: StoreContainer
var body: some View { List(items) { item in makeItemRow(item: item) .padding([.top, .bottom], 4) } .onAppear(perform: { self.items = storeContainer.bookmarks(service: service).map({ AnyItem(item: $0) }) }) } }
So instead of relying on UI state, we should rely on data state, by listening to onReceive and update our local @State
Reference that in HostingController. Note that we need to change from generic MainView to WKHostingController<AnyView> as environmentObject returns View protocol
1 2 3 4 5 6 7 8 9 10 11 12 13 14
classHostingController: WKHostingController<AnyView> { var storeContainer: StoreContainer!
In theory, the environment object will be propagated down the view hierarchy, but in practice it throws error. So a workaround now is to just pass that environment object down manually
Fatal error: No ObservableObject of type SomeType found A View.environmentObject(_:) for StoreContainer.Type may be missing as an ancestor of this view
/// The content of the scroll view. publicvar content: Content
}
1 2 3 4 5 6 7 8 9 10
extensionGroup : ViewwhereContent : View{
/// The type of view representing the body of this view. /// /// When you create a custom view, Swift infers this type from your /// implementation of the required `body` property. publictypealiasBody = Never
Use ObservableObject and onReceive to receive event. URLSession.dataTask reports in background queue, so need to .receive(on: RunLoop.main) to receive events on main queue.
For better dependency injection, need to use ImageLoader from Environment
There should be a way to propagate event from Publisher to another Publisher, for now we use sink