From 83993cec9632ac69c3ca4d004b485f1c0fad3df6 Mon Sep 17 00:00:00 2001 From: Kyle Date: Mon, 18 Dec 2023 02:14:52 +0800 Subject: [PATCH] Update model doc --- .../ModelData/Binding/Binding.swift | 157 ++++++ .../DynamicProperty/DynamicProperty.swift | 2 +- .../ModelData/State/ObservedObject.swift | 157 +++++- .../ModelData/State/State.swift | 163 ++++++ .../ModelData/State/StateObject.swift | 233 ++++++++ .../DataAndStorage/ModelData/Binding.md | 38 ++ .../ModelData/DynamicProperty.md | 7 + .../Managing-model-data-in-your-app.md | 496 ++++++++++++++++++ .../DataAndStorage/ModelData/Model-Data.md | 86 +++ .../ModelData/ObservedObject.md | 17 + .../DataAndStorage/ModelData/State.md | 17 + .../DataAndStorage/ModelData/StateObject.md | 13 + .../OpenSwiftUI.docc/OpenSwiftUI.md | 2 + 13 files changed, 1382 insertions(+), 6 deletions(-) create mode 100644 Sources/OpenSwiftUI/OpenSwiftUI.docc/DataAndStorage/ModelData/Binding.md create mode 100644 Sources/OpenSwiftUI/OpenSwiftUI.docc/DataAndStorage/ModelData/DynamicProperty.md create mode 100644 Sources/OpenSwiftUI/OpenSwiftUI.docc/DataAndStorage/ModelData/Managing-model-data-in-your-app.md create mode 100644 Sources/OpenSwiftUI/OpenSwiftUI.docc/DataAndStorage/ModelData/Model-Data.md create mode 100644 Sources/OpenSwiftUI/OpenSwiftUI.docc/DataAndStorage/ModelData/ObservedObject.md create mode 100644 Sources/OpenSwiftUI/OpenSwiftUI.docc/DataAndStorage/ModelData/State.md create mode 100644 Sources/OpenSwiftUI/OpenSwiftUI.docc/DataAndStorage/ModelData/StateObject.md diff --git a/Sources/OpenSwiftUI/DataAndStorage/ModelData/Binding/Binding.swift b/Sources/OpenSwiftUI/DataAndStorage/ModelData/Binding/Binding.swift index aa6a0ef7..ff91a705 100644 --- a/Sources/OpenSwiftUI/DataAndStorage/ModelData/Binding/Binding.swift +++ b/Sources/OpenSwiftUI/DataAndStorage/ModelData/Binding/Binding.swift @@ -6,32 +6,134 @@ // Lastest Version: iOS 15.5 // Status: Blocked by DynamicProperty +/// A property wrapper type that can read and write a value owned by a source of +/// truth. +/// +/// Use a binding to create a two-way connection between a property that stores +/// data, and a view that displays and changes the data. A binding connects a +/// property to a source of truth stored elsewhere, instead of storing data +/// directly. For example, a button that toggles between play and pause can +/// create a binding to a property of its parent view using the `Binding` +/// property wrapper. +/// +/// struct PlayButton: View { +/// @Binding var isPlaying: Bool +/// +/// var body: some View { +/// Button(isPlaying ? "Pause" : "Play") { +/// isPlaying.toggle() +/// } +/// } +/// } +/// +/// The parent view declares a property to hold the playing state, using the +/// ``State`` property wrapper to indicate that this property is the value's +/// source of truth. +/// +/// struct PlayerView: View { +/// var episode: Episode +/// @State private var isPlaying: Bool = false +/// +/// var body: some View { +/// VStack { +/// Text(episode.title) +/// .foregroundStyle(isPlaying ? .primary : .secondary) +/// PlayButton(isPlaying: $isPlaying) // Pass a binding. +/// } +/// } +/// } +/// +/// When `PlayerView` initializes `PlayButton`, it passes a binding of its state +/// property into the button's binding property. Applying the `$` prefix to a +/// property wrapped value returns its ``State/projectedValue``, which for a +/// state property wrapper returns a binding to the value. +/// +/// Whenever the user taps the `PlayButton`, the `PlayerView` updates its +/// `isPlaying` state. +/// +/// > Note: To create bindings to properties of a type that conforms to the +/// [Observable](https://developer.apple.com/documentation/Observation/Observable) +/// protocol, use the ``Bindable`` property wrapper. For more information, +/// see . @frozen @propertyWrapper @dynamicMemberLookup public struct Binding { + /// The binding's transaction. + /// + /// The transaction captures the information needed to update the view when + /// the binding value changes. public var transaction: Transaction var location: AnyLocation private var _value: Value + /// Creates a binding with closures that read and write the binding value. + /// + /// - Parameters: + /// - get: A closure that retrieves the binding value. The closure has no + /// parameters, and returns a value. + /// - set: A closure that sets the binding value. The closure has the + /// following parameter: + /// - newValue: The new value of the binding value. public init(get: @escaping () -> Value, set: @escaping (Value) -> Void) { let location = FunctionalLocation(getValue: get) { value, _ in set(value) } let box = LocationBox(location: location) self.init(value: get(), location: box) } + /// Creates a binding with a closure that reads from the binding value, and + /// a closure that applies a transaction when writing to the binding value. + /// + /// - Parameters: + /// - get: A closure to retrieve the binding value. The closure has no + /// parameters, and returns a value. + /// - set: A closure to set the binding value. The closure has the + /// following parameters: + /// - newValue: The new value of the binding value. + /// - transaction: The transaction to apply when setting a new value. public init(get: @escaping () -> Value, set: @escaping (Value, Transaction) -> Void) { let location = FunctionalLocation(getValue: get, setValue: set) let box = LocationBox(location: location) self.init(value: get(), location: box) } + /// Creates a binding with an immutable value. + /// + /// Use this method to create a binding to a value that cannot change. + /// This can be useful when using a ``PreviewProvider`` to see how a view + /// represents different values. + /// + /// // Example of binding to an immutable value. + /// PlayButton(isPlaying: Binding.constant(true)) + /// + /// - Parameter value: An immutable value. public static func constant(_ value: Value) -> Binding { let location = ConstantLocation(value: value) let box = LocationBox(location: location) return Binding(value: value, location: box) } + /// The underlying value referenced by the binding variable. + /// + /// This property provides primary access to the value's data. However, you + /// don't access `wrappedValue` directly. Instead, you use the property + /// variable created with the ``Binding`` attribute. In the + /// following code example, the binding variable `isPlaying` returns the + /// value of `wrappedValue`: + /// + /// struct PlayButton: View { + /// @Binding var isPlaying: Bool + /// + /// var body: some View { + /// Button(isPlaying ? "Pause" : "Play") { + /// isPlaying.toggle() + /// } + /// } + /// } + /// + /// When a mutable binding value changes, the new value is immediately + /// available. However, updates to a view displaying the value happens + /// asynchronously, so the view may not show the change immediately. public var wrappedValue: Value { get { readValue() @@ -41,23 +143,58 @@ public struct Binding { } } + /// A projection of the binding value that returns a binding. + /// + /// Use the projected value to pass a binding value down a view hierarchy. + /// To get the `projectedValue`, prefix the property variable with `$`. For + /// example, in the following code example `PlayerView` projects a binding + /// of the state property `isPlaying` to the `PlayButton` view using + /// `$isPlaying`. + /// + /// struct PlayerView: View { + /// var episode: Episode + /// @State private var isPlaying: Bool = false + /// + /// var body: some View { + /// VStack { + /// Text(episode.title) + /// .foregroundStyle(isPlaying ? .primary : .secondary) + /// PlayButton(isPlaying: $isPlaying) + /// } + /// } + /// } + /// public var projectedValue: Binding { self } + /// Creates a binding from the value of another binding. @_alwaysEmitIntoClient public init(projectedValue: Binding) { self = projectedValue } + /// Returns a binding to the resulting value of a given key path. + /// + /// - Parameter keyPath: A key path to a specific resulting value. + /// + /// - Returns: A new binding. public subscript(dynamicMember keyPath: WritableKeyPath) -> Binding { projecting(keyPath) } } extension Binding { + /// Creates a binding by projecting the base value to an optional value. + /// + /// - Parameter base: A value to project to an optional value. public init(_ base: Binding) where Value == V? { self = base.projecting(BindingOperations.ToOptional()) } + /// Creates a binding by projecting the base value to an unwrapped value. + /// + /// - Parameter base: A value to project to an unwrapped value. + /// + /// - Returns: A new binding or `nil` when `base` is `nil`. public init?(_ base: Binding) { guard let _ = base.wrappedValue else { return nil @@ -65,13 +202,22 @@ extension Binding { self = base.projecting(BindingOperations.ForceUnwrapping()) } + /// Creates a binding by projecting the base value to a hashable value. + /// + /// - Parameters: + /// - base: A `Hashable` value to project to an `AnyHashable` value. public init(_ base: Binding) where Value == AnyHashable { self = base.projecting(BindingOperations.ToAnyHashable()) } } extension Binding: Identifiable where Value: Identifiable { + /// The stable identity of the entity associated with this instance, + /// corresponding to the `id` of the binding's wrapped value. public var id: Value.ID { wrappedValue.id } + + /// A type representing the stable identity of the entity associated with + /// an instance. public typealias ID = Value.ID } @@ -126,12 +272,23 @@ extension Binding: BidirectionalCollection where Value: BidirectionalCollection, extension Binding: RandomAccessCollection where Value: MutableCollection, Value: RandomAccessCollection {} extension Binding { + /// Specifies a transaction for the binding. + /// + /// - Parameter transaction : An instance of a ``Transaction``. + /// + /// - Returns: A new binding. public func transaction(_ transaction: Transaction) -> Binding { var binding = self binding.transaction = transaction return binding } + /// Specifies an animation to perform when the binding value changes. + /// + /// - Parameter animation: An animation sequence performed when the binding + /// value changes. + /// + /// - Returns: A new binding. public func animation(_ animation: Animation? = .default) -> Binding { var binding = self binding.transaction.animation = animation diff --git a/Sources/OpenSwiftUI/DataAndStorage/ModelData/DynamicProperty/DynamicProperty.swift b/Sources/OpenSwiftUI/DataAndStorage/ModelData/DynamicProperty/DynamicProperty.swift index 043f971e..e2435c92 100644 --- a/Sources/OpenSwiftUI/DataAndStorage/ModelData/DynamicProperty/DynamicProperty.swift +++ b/Sources/OpenSwiftUI/DataAndStorage/ModelData/DynamicProperty/DynamicProperty.swift @@ -16,7 +16,7 @@ public protocol DynamicProperty { static var _propertyBehaviors: UInt32 { get } /// Updates the underlying value of the stored value. /// - /// SwiftUI calls this function before rendering a view's + /// OpenSwiftUI calls this function before rendering a view's /// ``View/body-swift.property`` to ensure the view has the most recent /// value. mutating func update() diff --git a/Sources/OpenSwiftUI/DataAndStorage/ModelData/State/ObservedObject.swift b/Sources/OpenSwiftUI/DataAndStorage/ModelData/State/ObservedObject.swift index 2dbacf55..e7ed293c 100644 --- a/Sources/OpenSwiftUI/DataAndStorage/ModelData/State/ObservedObject.swift +++ b/Sources/OpenSwiftUI/DataAndStorage/ModelData/State/ObservedObject.swift @@ -12,32 +12,179 @@ import Combine import OpenCombine #endif +/// A property wrapper type that subscribes to an observable object and +/// invalidates a view whenever the observable object changes. +/// +/// Add the `@ObservedObject` attribute to a parameter of a OpenSwiftUI ``View`` +/// when the input is an +/// [ObservableObject](https://developer.apple.com/documentation/combine/observableobject) +/// and you want the view to update when the object's published properties +/// change. You typically do this to pass a ``StateObject`` into a subview. +/// +/// The following example defines a data model as an observable object, +/// instantiates the model in a view as a state object, and then passes +/// the instance to a subview as an observed object: +/// +/// class DataModel: ObservableObject { +/// @Published var name = "Some Name" +/// @Published var isEnabled = false +/// } +/// +/// struct MyView: View { +/// @StateObject private var model = DataModel() +/// +/// var body: some View { +/// Text(model.name) +/// MySubView(model: model) +/// } +/// } +/// +/// struct MySubView: View { +/// @ObservedObject var model: DataModel +/// +/// var body: some View { +/// Toggle("Enabled", isOn: $model.isEnabled) +/// } +/// } +/// +/// When any published property of the observable object changes, OpenSwiftUI +/// updates any view that depends on the object. Subviews can +/// also make updates to the model properties, like the ``Toggle`` in the +/// above example, that propagate to other observers throughout the view +/// hierarchy. +/// +/// Don't specify a default or initial value for the observed object. Use the +/// attribute only for a property that acts as an input for a view, as in the +/// above example. +/// +/// > Note: Don't wrap objects conforming to the +/// [Observable](https://developer.apple.com/documentation/Observation/Observable) +/// protocol with `@ObservedObject`. OpenSwiftUI automatically tracks dependencies +/// to `Observable` objects used within body and updates dependent views when +/// their data changes. Attempting to wrap an `Observable` object with +/// `@ObservedObject` may cause a compiler error, because it requires that its +/// wrapped object to conform to the +/// [ObservableObject](https://developer.apple.com/documentation/combine/observableobject) +/// protocol. +/// > +/// > If the view needs a binding to a property of an `Observable` object in +/// its body, wrap the object with the ``Bindable`` property wrapper instead; +/// for example, `@Bindable var model: DataModel`. For more information, see +/// . @propertyWrapper @frozen public struct ObservedObject where ObjectType: ObservableObject { + /// A wrapper of the underlying observable object that can create bindings + /// to its properties. @dynamicMemberLookup @frozen public struct Wrapper { let root: ObjectType + + /// Gets a binding to the value of a specified key path. + /// + /// - Parameter keyPath: A key path to a specific value. + /// + /// - Returns: A new binding. public subscript(dynamicMember keyPath: ReferenceWritableKeyPath) -> Binding { Binding(root, keyPath: keyPath) } } - @usableFromInline - var _seed = 0 - - public var wrappedValue: ObjectType - + /// Creates an observed object with an initial value. + /// + /// This initializer has the same behavior as the ``init(wrappedValue:)`` + /// initializer. See that initializer for more information. + /// + /// - Parameter initialValue: An initial value. @_alwaysEmitIntoClient public init(initialValue: ObjectType) { self.init(wrappedValue: initialValue) } + /// Creates an observed object with an initial wrapped value. + /// + /// Don't call this initializer directly. Instead, declare + /// an input to a view with the `@ObservedObject` attribute, and pass a + /// value to this input when you instantiate the view. Unlike a + /// ``StateObject`` which manages data storage, you use an observed + /// object to refer to storage that you manage elsewhere, as in the + /// following example: + /// + /// class DataModel: ObservableObject { + /// @Published var name = "Some Name" + /// @Published var isEnabled = false + /// } + /// + /// struct MyView: View { + /// @StateObject private var model = DataModel() + /// + /// var body: some View { + /// Text(model.name) + /// MySubView(model: model) + /// } + /// } + /// + /// struct MySubView: View { + /// @ObservedObject var model: DataModel + /// + /// var body: some View { + /// Toggle("Enabled", isOn: $model.isEnabled) + /// } + /// } + /// + /// Explicitly calling the observed object initializer in `MySubView` would + /// behave correctly, but would needlessly recreate the same observed object + /// instance every time OpenSwiftUI calls the view's initializer to redraw the + /// view. + /// + /// - Parameter wrappedValue: An initial value for the observable object. public init(wrappedValue: ObjectType) { self.wrappedValue = wrappedValue } + @usableFromInline + var _seed = 0 + + /// The underlying value that the observed object references. + /// + /// The wrapped value property provides primary access to the observed + /// object's data. However, you don't typically access it by name. Instead, + /// OpenSwiftUI accesses this property for you when you refer to the variable + /// that you create with the `@ObservedObject` attribute. + /// + /// struct MySubView: View { + /// @ObservedObject var model: DataModel + /// + /// var body: some View { + /// Text(model.name) // Reads name from model's wrapped value. + /// } + /// } + /// + /// When you change a wrapped value, you can access the new value + /// immediately. However, OpenSwiftUI updates views that display the value + /// asynchronously, so the interface might not update immediately. + @MainActor + public var wrappedValue: ObjectType + + /// A projection of the observed object that creates bindings to its + /// properties. + /// + /// Use the projected value to get a ``Binding`` to a property of an + /// observed object. To access the projected value, prefix the property + /// variable with a dollar sign (`$`). For example, you can get a binding + /// to a model's `isEnabled` Boolean so that a ``Toggle`` can control its + /// value: + /// + /// struct MySubView: View { + /// @ObservedObject var model: DataModel + /// + /// var body: some View { + /// Toggle("Enabled", isOn: $model.isEnabled) + /// } + /// } + /// + @MainActor public var projectedValue: ObservedObject.Wrapper { .init(root: wrappedValue) } diff --git a/Sources/OpenSwiftUI/DataAndStorage/ModelData/State/State.swift b/Sources/OpenSwiftUI/DataAndStorage/ModelData/State/State.swift index 74a181fe..06fbc2bc 100644 --- a/Sources/OpenSwiftUI/DataAndStorage/ModelData/State/State.swift +++ b/Sources/OpenSwiftUI/DataAndStorage/ModelData/State/State.swift @@ -7,6 +7,84 @@ // Status: Blocked by DynamicProperty // ID: 08168374F4710A99DCB15B5E8768D632 +/// A property wrapper type that can read and write a value managed by OpenSwiftUI. +/// +/// Use state as the single source of truth for a given value type that you +/// store in a view hierarchy. Create a state value in an ``App``, ``Scene``, +/// or ``View`` by applying the `@State` attribute to a property declaration +/// and providing an initial value. Declare state as private to prevent setting +/// it in a memberwise initializer, which can conflict with the storage +/// management that OpenSwiftUI provides: +/// +/// struct PlayButton: View { +/// @State private var isPlaying: Bool = false // Create the state. +/// +/// var body: some View { +/// Button(isPlaying ? "Pause" : "Play") { // Read the state. +/// isPlaying.toggle() // Write the state. +/// } +/// } +/// } +/// +/// OpenSwiftUI manages the property's storage. When the value changes, OpenSwiftUI +/// updates the parts of the view hierarchy that depend on the value. +/// To access a state's underlying value, you use its ``wrappedValue`` property. +/// However, as a shortcut Swift enables you to access the wrapped value by +/// referring directly to the state instance. The above example reads and +/// writes the `isPlaying` state property's wrapped value by referring to the +/// property directly. +/// +/// Declare state as private in the highest view in the view hierarchy that +/// needs access to the value. Then share the state with any subviews that also +/// need access, either directly for read-only access, or as a binding for +/// read-write access. You can safely mutate state properties from any thread. +/// +/// > Note: If you need to store a reference type, like an instance of a class, +/// use a ``StateObject`` instead. +/// +/// ### Share state with subviews +/// +/// If you pass a state property to a subview, OpenSwiftUI updates the subview +/// any time the value changes in the container view, but the subview can't +/// modify the value. To enable the subview to modify the state's stored value, +/// pass a ``Binding`` instead. You can get a binding to a state value by +/// accessing the state's ``projectedValue``, which you get by prefixing the +/// property name with a dollar sign (`$`). +/// +/// For example, you can remove the `isPlaying` state from the play button in +/// the above example, and instead make the button take a binding: +/// +/// struct PlayButton: View { +/// @Binding var isPlaying: Bool // Play button now receives a binding. +/// +/// var body: some View { +/// Button(isPlaying ? "Pause" : "Play") { +/// isPlaying.toggle() +/// } +/// } +/// } +/// +/// Then you can define a player view that declares the state and creates a +/// binding to the state using the dollar sign prefix: +/// +/// struct PlayerView: View { +/// @State private var isPlaying: Bool = false // Create the state here now. +/// +/// var body: some View { +/// VStack { +/// PlayButton(isPlaying: $isPlaying) // Pass a binding. +/// +/// // ... +/// } +/// } +/// } +/// +/// Like you do for a ``StateObject``, declare ``State`` as private to prevent +/// setting it in a memberwise initializer, which can conflict with the storage +/// management that OpenSwiftUI provides. Unlike a state object, always +/// initialize state by providing a default value in the state's +/// declaration, as in the above examples. Use state only for storage that's +/// local to a view and its subviews. @frozen @propertyWrapper public struct State { @@ -16,16 +94,73 @@ public struct State { @usableFromInline var _location: AnyLocation? + /// Creates a state property that stores an initial wrapped value. + /// + /// You don't call this initializer directly. Instead, OpenSwiftUI + /// calls it for you when you declare a property with the `@State` + /// attribute and provide an initial value: + /// + /// struct MyView: View { + /// @State private var isPlaying: Bool = false + /// + /// // ... + /// } + /// + /// OpenSwiftUI initializes the state's storage only once for each + /// container instance that you declare. In the above code, OpenSwiftUI + /// creates `isPlaying` only the first time it initializes a particular + /// instance of `MyView`. On the other hand, each instance of `MyView` + /// creates a distinct instance of the state. For example, each of + /// the views in the following ``VStack`` has its own `isPlaying` value: + /// + /// var body: some View { + /// VStack { + /// MyView() + /// MyView() + /// } + /// } + /// + /// - Parameter value: An initial value to store in the state + /// property. public init(wrappedValue value: Value) { _value = value _location = nil } + /// Creates a state property that stores an initial value. + /// + /// This initializer has the same behavior as the ``init(wrappedValue:)`` + /// initializer. See that initializer for more information. + /// + /// - Parameter value: An initial value to store in the state + /// property. @_alwaysEmitIntoClient public init(initialValue value: Value) { _value = value } + /// The underlying value referenced by the state variable. + /// + /// This property provides primary access to the value's data. However, you + /// don't typically access `wrappedValue` explicitly. Instead, you gain + /// access to the wrapped value by referring to the property variable that + /// you create with the `@State` attribute. + /// + /// In the following example, the button's label depends on the value of + /// `isPlaying` and the button's action toggles the value of `isPlaying`. + /// Both of these accesses implicitly access the state property's wrapped + /// value: + /// + /// struct PlayButton: View { + /// @State private var isPlaying: Bool = false + /// + /// var body: some View { + /// Button(isPlaying ? "Pause" : "Play") { + /// isPlaying.toggle() + /// } + /// } + /// } + /// public var wrappedValue: Value { get { getValue(forReading: true) @@ -38,6 +173,30 @@ public struct State { } } + /// A binding to the state value. + /// + /// Use the projected value to get a ``Binding`` to the stored value. The + /// binding provides a two-way connection to the stored value. To access + /// the `projectedValue`, prefix the property variable with a dollar + /// sign (`$`). + /// + /// In the following example, `PlayerView` projects a binding of the state + /// property `isPlaying` to the `PlayButton` view using `$isPlaying`. That + /// enables the play button to both read and write the value: + /// + /// struct PlayerView: View { + /// var episode: Episode + /// @State private var isPlaying: Bool = false + /// + /// var body: some View { + /// VStack { + /// Text(episode.title) + /// .foregroundStyle(isPlaying ? .primary : .secondary) + /// PlayButton(isPlaying: $isPlaying) + /// } + /// } + /// } + /// public var projectedValue: Binding { let value = getValue(forReading: false) guard let _location else { @@ -54,6 +213,10 @@ extension State: DynamicProperty { } extension State where Value: ExpressibleByNilLiteral { + /// Creates a state property without an initial value. + /// + /// This initializer behaves like the ``init(wrappedValue:)`` initializer + /// with an input of `nil`. See that initializer for more information. @inlinable public init() { self.init(wrappedValue: nil) diff --git a/Sources/OpenSwiftUI/DataAndStorage/ModelData/State/StateObject.swift b/Sources/OpenSwiftUI/DataAndStorage/ModelData/State/StateObject.swift index 86f14a36..de122430 100644 --- a/Sources/OpenSwiftUI/DataAndStorage/ModelData/State/StateObject.swift +++ b/Sources/OpenSwiftUI/DataAndStorage/ModelData/State/StateObject.swift @@ -12,6 +12,168 @@ import Combine import OpenCombine #endif +/// A property wrapper type that instantiates an observable object. +/// +/// Use a state object as the single source of truth for a reference type that +/// you store in a view hierarchy. Create a state object in an ``App``, +/// ``Scene``, or ``View`` by applying the `@StateObject` attribute to a +/// property declaration and providing an initial value that conforms to the +/// [ObservableObject](https://developer.apple.com/documentation/combine/observableobject) +/// protocol. Declare state objects as private to prevent setting them from a +/// memberwise initializer, which can conflict with the storage management that +/// OpenSwiftUI provides: +/// +/// class DataModel: ObservableObject { +/// @Published var name = "Some Name" +/// @Published var isEnabled = false +/// } +/// +/// struct MyView: View { +/// @StateObject private var model = DataModel() // Create the state object. +/// +/// var body: some View { +/// Text(model.name) // Updates when the data model changes. +/// MySubView() +/// .environmentObject(model) +/// } +/// } +/// +/// OpenSwiftUI creates a new instance of the model object only once during the +/// lifetime of the container that declares the state object. For example, +/// OpenSwiftUI doesn't create a new instance if a view's inputs change, but does +/// create a new instance if the identity of a view changes. When published +/// properties of the observable object change, OpenSwiftUI updates any view that +/// depends on those properties, like the ``Text`` view in the above example. +/// +/// > Note: If you need to store a value type, like a structure, string, or +/// integer, use the ``State`` property wrapper instead. Also use ``State`` +/// if you need to store a reference type that conforms to the +/// [Observable](https://developer.apple.com/documentation/observation/observable()) +/// protocol. To learn more about Observation in OpenSwiftUI, see +/// . +/// +/// ### Share state objects with subviews +/// +/// You can pass a state object into a subview through a property that has the +/// ``ObservedObject`` attribute. Alternatively, add the object to the +/// environment of a view hierarchy by applying the +/// ``View/environmentObject(_:)`` modifier to a view, like `MySubView` in the +/// above code. You can then read the object inside `MySubView` or any of its +/// descendants using the ``EnvironmentObject`` attribute: +/// +/// struct MySubView: View { +/// @EnvironmentObject var model: DataModel +/// +/// var body: some View { +/// Toggle("Enabled", isOn: $model.isEnabled) +/// } +/// } +/// +/// Get a ``Binding`` to the state object's properties using the dollar sign +/// (`$`) operator. Use a binding when you want to create a two-way connection. +/// In the above code, the ``Toggle`` controls the model's `isEnabled` value +/// through a binding. +/// +/// ### Initialize state objects using external data +/// +/// When a state object's initial state depends on data that comes from +/// outside its container, you can call the object's initializer +/// explicitly from within its container's initializer. For example, +/// suppose the data model from the previous example takes a `name` +/// input during initialization and you want to use a value for that +/// name that comes from outside the view. You can do this with +/// a call to the state object's initializer inside an explicit initializer +/// that you create for the view: +/// +/// struct MyInitializableView: View { +/// @StateObject private var model: DataModel +/// +/// init(name: String) { +/// // OpenSwiftUI ensures that the following initialization uses the +/// // closure only once during the lifetime of the view, so +/// // later changes to the view's name input have no effect. +/// _model = StateObject(wrappedValue: DataModel(name: name)) +/// } +/// +/// var body: some View { +/// VStack { +/// Text("Name: \(model.name)") +/// } +/// } +/// } +/// +/// Use caution when doing this. OpenSwiftUI only initializes a state object +/// the first time you call its initializer in a given view. This +/// ensures that the object provides stable storage even as the view's +/// inputs change. However, it might result in unexpected behavior or +/// unwanted side effects if you explicitly initialize the state object. +/// +/// In the above example, if the `name` input to `MyInitializableView` +/// changes, OpenSwiftUI reruns the view's initializer with the new value. However, +/// OpenSwiftUI runs the autoclosure that you provide to the state object's +/// initializer only the first time you call the state object's initializer, so +/// the model's stored `name` value doesn't change. +/// +/// Explicit state object initialization works well when the external data +/// that the object depends on doesn't change for a given instance of the +/// object's container. For example, you can create two views with different +/// constant names: +/// +/// var body: some View { +/// VStack { +/// MyInitializableView(name: "Ravi") +/// MyInitializableView(name: "Maria") +/// } +/// } +/// +/// > Important: Even for a configurable state object, you still declare it +/// as private. This ensures that you can't accidentally set the parameter +/// through a memberwise initializer of the view, because doing so can +/// conflict with the framework's storage management and produce unexpected +/// results. +/// +/// ### Force reinitialization by changing view identity +/// +/// If you want OpenSwiftUI to reinitialize a state object when a view input +/// changes, make sure that the view's identity changes at the same time. +/// One way to do this is to bind the view's identity to the value that changes +/// using the ``View/id(_:)`` modifier. For example, you can ensure that +/// the identity of an instance of `MyInitializableView` changes when its +/// `name` input changes: +/// +/// MyInitializableView(name: name) +/// .id(name) // Binds the identity of the view to the name property. +/// +/// > NOTE: If your view appears inside a ``ForEach``, it implicitly receives an +/// ``View/id(_:)`` modifier that uses the identifier of the corresponding +/// data element. +/// +/// If you need the view to reinitialize state based on changes in more than +/// one value, you can combine the values into a single identifier using a +/// [Hasher](https://developer.apple.com/documentation/swift/hasher). For example, +/// if you want to update the data model in `MyInitializableView` when the +/// values of either `name` or `isEnabled` change, you can combine both +/// variables into a single hash: +/// +/// var hash: Int { +/// var hasher = Hasher() +/// hasher.combine(name) +/// hasher.combine(isEnabled) +/// return hasher.finalize() +/// } +/// +/// Then apply the combined hash to the view as an identifier: +/// +/// MyInitializableView(name: name, isEnabled: isEnabled) +/// .id(hash) +/// +/// Be mindful of the performance cost of reinitializing the state object every +/// time the input changes. Also, changing view identity can have side +/// effects. For example, OpenSwiftUI doesn't automatically animate +/// changes inside the view if the view's identity changes at the same time. +/// Also, changing the identity resets _all_ state held by the view, including +/// values that you manage as ``State``, ``FocusState``, ``GestureState``, +/// and so on. @frozen @propertyWrapper public struct StateObject where ObjectType: ObservableObject { @@ -25,15 +187,86 @@ public struct StateObject where ObjectType: ObservableObject { @usableFromInline var storage: StateObject.Storage + /// Creates a new state object with an initial wrapped value. + /// + /// You typically don’t call this initializer directly. Instead, OpenSwiftUI + /// calls it for you when you declare a property with the `@StateObject` + /// attribute in an ``App``, ``Scene``, or ``View`` and provide an initial + /// value: + /// + /// struct MyView: View { + /// @StateObject private var model = DataModel() + /// + /// // ... + /// } + /// + /// OpenSwiftUI creates only one instance of the state object for each + /// container instance that you declare. In the above code, OpenSwiftUI + /// creates `model` only the first time it initializes a particular + /// instance of `MyView`. On the other hand, each instance of `MyView` + /// creates a distinct instance of the data model. For example, each of + /// the views in the following ``VStack`` has its own model storage: + /// + /// var body: some View { + /// VStack { + /// MyView() + /// MyView() + /// } + /// } + /// + /// ### Initialize using external data + /// + /// If the initial state of a state object depends on external data, you can + /// call this initializer directly. However, use caution when doing this, + /// because OpenSwiftUI only initializes the object once during the lifetime of + /// the view --- even if you call the state object initializer more than + /// once --- which might result in unexpected behavior. For more information + /// and an example, see ``StateObject``. + /// + /// - Parameter thunk: An initial value for the state object. @inlinable public init(wrappedValue thunk: @autoclosure @escaping () -> ObjectType) { storage = .initially(thunk) } + /// The underlying value referenced by the state object. + /// + /// The wrapped value property provides primary access to the value's data. + /// However, you don't typically access it directly. Instead, + /// OpenSwiftUI accesses this property for you when you refer to the variable + /// that you create with the `@StateObject` attribute: + /// + /// @StateObject private var contact = Contact() + /// + /// var body: some View { + /// Text(contact.name) // Reads name from contact's wrapped value. + /// } + /// + /// When you change a wrapped value, you can access the new + /// value immediately. However, OpenSwiftUI updates views that display the value + /// asynchronously, so the interface might not update immediately. + @MainActor public var wrappedValue: ObjectType { objectValue.wrappedValue } + /// A projection of the state object that creates bindings to its + /// properties. + /// + /// Use the projected value to get a ``Binding`` to a property of a state + /// object. To access the projected value, prefix the property name + /// with a dollar sign (`$`). For example, you can get a binding to a + /// model's `isEnabled` Boolean so that a ``Toggle`` can control the value: + /// + /// struct MyView: View { + /// @StateObject private var model = DataModel() + /// + /// var body: some View { + /// Toggle("Enabled", isOn: $model.isEnabled) + /// } + /// } + /// + @MainActor public var projectedValue: ObservedObject.Wrapper { objectValue.projectedValue } diff --git a/Sources/OpenSwiftUI/OpenSwiftUI.docc/DataAndStorage/ModelData/Binding.md b/Sources/OpenSwiftUI/OpenSwiftUI.docc/DataAndStorage/ModelData/Binding.md new file mode 100644 index 00000000..ebe54087 --- /dev/null +++ b/Sources/OpenSwiftUI/OpenSwiftUI.docc/DataAndStorage/ModelData/Binding.md @@ -0,0 +1,38 @@ +# ``Binding`` + +## Topics + +### Creating a binding + +- ``init(_:)-qgp1`` + +- ``init(_:)-jtxz`` + +- ``init(_:)-57cwg`` + +- ``init(projectedValue:)`` + +- ``init(get:set:)-x2sw`` + +- ``init(get:set:)-4fn32`` + +- ``constant(_:)`` + + +### Getting the value + +- ``wrappedValue`` + +- ``projectedValue`` + +- ``subscript(dynamicMember:)`` + +### Managing changes + +- ``id`` + +- ``animation(_:)`` + +- ``transaction(_:)`` + +- ``transaction`` diff --git a/Sources/OpenSwiftUI/OpenSwiftUI.docc/DataAndStorage/ModelData/DynamicProperty.md b/Sources/OpenSwiftUI/OpenSwiftUI.docc/DataAndStorage/ModelData/DynamicProperty.md new file mode 100644 index 00000000..6c3d19c3 --- /dev/null +++ b/Sources/OpenSwiftUI/OpenSwiftUI.docc/DataAndStorage/ModelData/DynamicProperty.md @@ -0,0 +1,7 @@ +# ``DynamicProperty`` + +## Topics + +### Updating the value + +- ``update()`` diff --git a/Sources/OpenSwiftUI/OpenSwiftUI.docc/DataAndStorage/ModelData/Managing-model-data-in-your-app.md b/Sources/OpenSwiftUI/OpenSwiftUI.docc/DataAndStorage/ModelData/Managing-model-data-in-your-app.md new file mode 100644 index 00000000..c395563f --- /dev/null +++ b/Sources/OpenSwiftUI/OpenSwiftUI.docc/DataAndStorage/ModelData/Managing-model-data-in-your-app.md @@ -0,0 +1,496 @@ +# Managing model data in your app + +Create connections between your app’s data model and views. + +## Overview + +A OpenSwiftUI app can display data that people can change using the app’s user +interface (UI). To manage that data, an app creates a data model, which is a +custom type that represents the data. A data model provides separation between +the data and the views that interact with the data. This separation promotes +modularity, improves testability, and helps make it easier to reason about how +the app works. + +Keeping the model data (that is, an instance of a data model) in sync with what +appears on the screen can be challenging, especially when the data appears in +multiple views of the UI at the same time. + +OpenSwiftUI helps keep your app’s UI up to date with changes made to the data +thanks to Observation. With Observation, a view in OpenSwiftUI can form +dependencies on observable data models and update the UI when data changes. + +> Note: +> [Observation](https://developer.apple.com/documentation/observation) support +> in OpenSwiftUI is available starting with iOS 17, iPadOS 17, macOS 14, +> tvOS 17, and watchOS 10. For information about adopting Observation in +> existing apps, see +> [Migrating from the Observable Object protocol to the Observable macro](https://developer.apple.com/documentation/swiftui/migrating-from-the-observable-object-protocol-to-the-observable-macro). + +### Make model data observable + +To make data changes visible to OpenSwiftUI, apply the +[Observable()](https://developer.apple.com/documentation/observation/observable()) +macro to your data model. This macro generates code that adds observation +support to your data model at compile time, keeping your data model code focused +on the properties that store data. For example, the following code defines a +data model for books: + +```swift +@Observable class Book: Identifiable { + var title = "Sample Book Title" + var author = Author() + var isAvailable = true +} +``` + +Observation also supports reference and value types. To help you decide which +type to use for your data model, see +[Choosing Between Structures and Classes](https://developer.apple.com/documentation/swift/choosing-between-structures-and-classes). + +> Important: +> The [Observable()](https://developer.apple.com/documentation/observation/observable()) +> macro, in addition to adding observation functionality, also conforms your +> data model type to the +> [Observable](https://developer.apple.com/documentation/observation/observable) +> protocol, which serves as a signal to other APIs that your type supports +> observation. Don’t apply the `Observable` protocol by itself to your data +> model type, since that alone doesn’t add any observation functionality. +> Instead, always use the `Observable` macro when adding observation support to +> your type. + +### Observe model data in a view + +In OpenSwiftUI, a view forms a dependency on an observable data model object, +such as an instance of Book, when the view’s ``View/body-swift.property`` +property reads a property of the object. If body doesn’t read any properties of +an observable data model object, the view doesn’t track any dependencies. + +When a tracked property changes, OpenSwiftUI updates the view. If other +properties change that ``View/body-swift.property`` doesn’t read, the view is +unaffected and avoids unnecessary updates. For example, the view in the +following code updates only when a book’s `title` changes but not when `author` +or `isAvailable` changes: + +```swift +struct BookView: View { + var book: Book + + var body: some View { + Text(book.title) + } +} +``` + +OpenSwiftUI establishes this dependency tracking even if the view doesn’t store +the observable type, such as when using a global property or singleton: + +```swift +var globalBook: Book = Book() + +struct BookView: View { + var body: some View { + Text(globalBook.title) + } +} +``` + +Observation also supports tracking of computed properties when the computed +property makes use of an observable property. For instance, the view in the +following code updates when the number of available books changes: + +```swift +@Observable class Library { + var books: [Book] = [Book(), Book(), Book()] + + var availableBooksCount: Int { + books.filter(\.isAvailable).count + } +} + +struct LibraryView: View { + @Environment(Library.self) private var library + + var body: some View { + NavigationStack { + List(library.books) { book in + // ... + } + .navigationTitle("Books available: \(library.availableBooksCount)") + } + } +} +``` + +When a view forms a dependency on a collection of objects, of any collection +type, the view tracks changes made to the collection itself. For instance, the +view in the following code forms a dependency on books because body reads it. +As changes occur to books, such as inserting, deleting, moving, or replacing +items in the collection, OpenSwiftUI updates the view. + +```swift +struct LibraryView: View { + @State private var books = [Book(), Book(), Book()] + + var body: some View { + List(books) { book in + Text(book.title) + } + } +} +``` + +However, `LibraryView` doesn’t form a dependency on the property `title` because +the view’s ``View/body-swift.property`` doesn’t read it directly. The view +stores the ``List`` content closure as an `@escaping` closure that OpenSwiftUI +calls when lazily creating list items before they appear on the screen. This +means that instead of `LibraryView` depending on a book’s `title`, each ``Text`` +item of the list depends on `title`. Any changes to a `title` updates only the +individual ``Text`` representing the book and not the others. + +> Note: +> Observation tracks changes to any observable property that appears in the +> execution scope of a view’s ``View/body-swift.property`` property. + +You can also share an observable model data object with another view. The +receiving view forms a dependency if it reads any properties of the object in +the its ``View/body-swift.property``. For example, in the following code +`LibraryView` shares an instance of `Book` with `BookView`, and `BookView` +displays the book’s `title`. If the book’s `title` changes, OpenSwiftUI updates +only `BookView`, and not `LibraryView`, because only `BookView` reads the title +property. + +```swift +struct LibraryView: View { + @State private var books = [Book(), Book(), Book()] + + var body: some View { + List(books) { book in + BookView(book: book) + } + } +} + +struct BookView: View { + var book: Book + + var body: some View { + Text(book.title) + } +} +``` + +If a view doesn’t have any dependencies, OpenSwiftUI doesn’t update the view +when data changes. This approach allows an observable model data object to pass +through multiple layers of a view hierarchy without each intermediate view +forming a dependency. + +```swift +// Will not update when any property of `book` changes. +struct LibraryView: View { + @State private var books = [Book(), Book(), Book()] + + var body: some View { + LibraryItemView(book: book) + } +} + +// Will not update when any property of `book` changes. +struct LibraryItemView: View { + var book: Book + + var body: some View { + BookView(book: book) + } +} + +// Will update when `book.title` changes. +struct BookView: View { + var book: Book + + var body: some View { + Text(book.title) + } +} +``` + +However, a view that stores a reference to the observable object updates if the +reference changes. This happens because the stored reference is part of the +view’s value and not because the object is observable. For example, if the +reference to book in the follow code changes, OpenSwiftUI updates the view: + +```swift +struct BookView: View { + var book: Book + + var body: some View { + // ... + } +} +``` + +A view can also form a dependency on an observable data model object accessed +through another object. For example, the view in the following code updates when +the author’s `name` changes: + +```swift +struct LibraryItemView: View { + var book: Book + + var body: some View { + VStack(alignment: .leading) { + Text(book.title) + Text("Written by: \(book.author.name)") + .font(.caption) + } + } +} +``` + +### Create the source of truth for model data + +To create and store the source of truth for model data, declare a private +variable and initialize it with a instance of an observable data model type. +Then wrap it with a ``State`` property wrapper. For example, the following code +stores an instance of the data model type Book in the state variable `book`: + +```swift +struct BookView: View { + @State private var book = Book() + + var body: some View { + Text(book.title) + } +} +``` + +By wrapping the book with ``State``, you’re telling OpenSwiftUI to manage the +storage of the instance. Each time OpenSwiftUI re-creates `BookView`, it +connects the `book` variable to the managed instance, providing the view a +single source of truth for the model data. + +You can also create a state object in your top-level ``App`` instance or in one +of your app’s ``Scene`` instances. For example, the following code creates an +instance of `Library` in the app’s top-level structure: + +```swift +@main +struct BookReaderApp: App { + @State private var library = Library() + + var body: some Scene { + WindowGroup { + LibraryView() + .environment(library) + } + } +} +``` + +### Share model data throughout a view hierarchy + +If you have a data model object, like `Library`, that you want to share +throughout your app, you can either: + +- pass the data model object to each view in the view hierarchy; or + +- add the data model object to the view’s environment + +Passing model data to each view is convenient when you have a shallow view +hierarchy; for example, when a view doesn’t share the object with its subviews. +However, you usually don’t know if a view needs to pass the object to subviews, +and you may not know if a subview deep inside the layers of the hierarchy needs +the model data. + +To share model data throughout a view hierarchy without needing to pass it to +each view, add the model data to the view’s environment. You can add the data to +the environment using either ``environment(_:_:)`` or the ``environment(_:)`` +modifier, passing in the model data. + +Before you can use the ``environment(_:_:)`` modifier, you need to create a +custom ``EnvironmentKey``. Then extend ``EnvironmentValues`` to include a custom +environment property that gets and sets the value for the custom key. For +instance, the following code creates an environment key and property for +`library`: + +```swift +extension EnvironmentValues { + var library: Library { + get { self[LibraryKey.self] } + set { self[LibraryKey.self] = newValue } + } +} + +private struct LibraryKey: EnvironmentKey { + static var defaultValue: Library = Library() +} +``` + +With the custom environment key and property in place, a view can add model data +to its environment. For example, `LibraryView` adds the source of truth for a +`Library` instance to its environment using the ``environment(_:_:)`` modifier: + +```swift +@main +struct BookReaderApp: App { + @State private var library = Library() + + var body: some Scene { + WindowGroup { + LibraryView() + .environment(\.library, library) + } + } +} +``` + +To retrieve the `Library` instance from the environment, a view defines a local +variable that stores a reference to the instance, and then wraps the variable +with the ``Environment`` property wrapper, passing in the key path to the custom +environment value. + +```swift +struct LibraryView: View { + @Environment(\.library) private var library + + var body: some View { + // ... + } +} +``` + +You can also store model data directly in the environment without defining a +custom environment value by using the ``environment(_:)`` modifier. For +instance, the following code adds a `Library` instance to the environment using +this modifier: + +```swift +@main +struct BookReaderApp: App { + @State private var library = Library() + + var body: some Scene { + WindowGroup { + LibraryView() + .environment(library) + } + } +} +``` + +To retrieve the instance from the environment, another view defines a local +variable to store the instance and wraps it with the ``Environment`` property +wrapper. But instead of providing a key path to the environment value, you can +provide the model data type, as shown in the following code: + +```swift +struct LibraryView: View { + @Environment(Library.self) private var library + + var body: some View { + // ... + } +} +``` + +By default, reading an object from the environment returns a non-optional object +when using the object type as the key. This default behavior assumes that a view +in the current hierarchy previously stored a non-optional instance of the type +using the ``environment(_:)`` modifier. If a view attempts to retrieve an object +using its type and that object isn’t in the environment, OpenSwiftUI throws +exception. + +In cases where there is no guarantee that an object is in the environment, +retrieve an optional version of the object as shown in the following code. If +the object isn’t available the environment, OpenSwiftUI returns nil instead of +throwing an exception. + +```swift +@Environment(Library.self) private var library: Library? +``` + +### Change model data in a view + +In most apps, people can change data that the app presents. When data changes, +any views that display the data should update to reflect the changed data. With +Observation in OpenSwiftUI, a view can support data changes without using +property wrappers or bindings. For example, the following toggles the +`isAvailable` property of a book in the action closure of a button: + +```swift +struct BookView: View { + var book: Book + + var body: some View { + List { + Text(book.title) + HStack { + Text(book.isAvailable ? "Available for checkout" : "Waiting for return") + Spacer() + Button(book.isAvailable ? "Check out" : "Return") { + book.isAvailable.toggle() + } + } + } + } +} +``` + +However, there may be times when a view expects a binding before it can change +the value of a mutable property. To provide a binding, wrap the model data with +the ``Bindable`` property wrapper. For example, the following code wraps the +book variable with `@Bindable`. Then it uses a ``TextField`` to change the +`title` property of a book, and a ``Toggle`` to change the `isAvailable` +property, using the `$` syntax to pass a binding to each property. + +```swift +struct BookEditView: View { + @Bindable var book: Book + @Environment(\.dismiss) private var dismiss + + var body: some View { + VStack() { + HStack { + Text("Title") + TextField("Title", text: $book.title) + .textFieldStyle(.roundedBorder) + .onSubmit { + dismiss() + } + } + + Toggle(isOn: $book.isAvailable) { + Text("Book is available") + } + + Button("Close") { + dismiss() + } + .buttonStyle(.borderedProminent) + } + .padding() + } +} +``` + +You can use the ``Bindable`` property wrapper on properties and variables to an +[Observable](https://developer.apple.com/documentation/observation/observable) +object. This includes global variables, properties that exists outside of +OpenSwiftUI types, or even local variables. For example, you can create a +`@Bindable` variable within a view’s ``View/body-swift.property``: + +```swift +struct LibraryView: View { + @State private var books = [Book(), Book(), Book()] + + var body: some View { + List(books) { book in + @Bindable var book = book + TextField("Title", text: $book.title) + } + } +} +``` + +The `@Bindable` variable `book` provides a binding that connects ``TextField`` +to the `title` property of a book so that a person can make changes directly to +the model data. diff --git a/Sources/OpenSwiftUI/OpenSwiftUI.docc/DataAndStorage/ModelData/Model-Data.md b/Sources/OpenSwiftUI/OpenSwiftUI.docc/DataAndStorage/ModelData/Model-Data.md new file mode 100644 index 00000000..fbe7a004 --- /dev/null +++ b/Sources/OpenSwiftUI/OpenSwiftUI.docc/DataAndStorage/ModelData/Model-Data.md @@ -0,0 +1,86 @@ +# Model data + +Manage the data that your app uses to drive its interface. + +## Overview + +OpenSwiftUI offers a declarative approach to user interface design. As you +compose a hierarchy of views, you also indicate data dependencies for the views. +When the data changes, either due to an external event or because of an action +that the user performs, OpenSwiftUI automatically updates the affected parts of +the interface. As a result, the framework automatically performs most of the +work that view controllers traditionally do. + +The framework provides tools, like state variables and bindings, for connecting +your app’s data to the user interface. These tools help you maintain a single +source of truth for every piece of data in your app, in part by reducing the +amount of glue logic you write. Select the tool that best suits the task you +need to perform: + +- Manage transient UI state locally within a view by wrapping value types as +``State`` properties. + +- Share a reference to a source of truth, like local state, using the +``Binding`` property wrapper. + +- Connect to and observe reference model data in views by applying the +``Observable()`` macro to the model data type. Instantiate an observable model +data type directly in a view using a ``State`` property. Share the observable +model data with other views in the hierarchy without passing a reference using +the ``Environment`` property wrapper. + +## Leveraging property wrappers + +OpenSwiftUI implements many data management types, like ``State`` and +``Binding``, as Swift property wrappers. Apply a property wrapper by adding an +attribute with the wrapper’s name to a property’s declaration. + +```swift +@State private var isVisible = true // Declares isVisible as a state variable. +``` + +The property gains the behavior that the wrapper specifies. The state and data +flow property wrappers in OpenSwiftUI watch for changes in your data, and +automatically update affected views as necessary. When you refer directly to the +property in your code, you access the wrapped value, which for the `isVisible` +state property in the example above is the stored Boolean. + +```swift +if isVisible == true { + Text("Hello") // Only renders when isVisible is true. +} +``` + +Alternatively, you can access a property wrapper’s projected value by prefixing +the property name with the dollar sign ($). OpenSwiftUI state and data flow +property wrappers project a ``Binding``, which is a two-way connection to the +wrapped value, allowing another view to access and mutate a single source of +truth. + +```swift +Toggle("Visible", isOn: $isVisible) // The toggle can update the stored value. +``` + +For more information about property wrappers, see +[Property Wrappers](https://docs.swift.org/swift-book/documentation/the-swift-programming-language/properties/#Property-Wrappers) +in [The Swift Programming Language](https://www.swift.org/documentation/#the-swift-programming-language). + +## Topics + +### Creating and sharing view state + +- ``State`` + +- ``Binding`` + +### Creating model data + +- + +- ``StateObject`` + +- ``ObservedObject`` + +### Managing dynamic data + +- ``DynamicProperty`` diff --git a/Sources/OpenSwiftUI/OpenSwiftUI.docc/DataAndStorage/ModelData/ObservedObject.md b/Sources/OpenSwiftUI/OpenSwiftUI.docc/DataAndStorage/ModelData/ObservedObject.md new file mode 100644 index 00000000..460c2a26 --- /dev/null +++ b/Sources/OpenSwiftUI/OpenSwiftUI.docc/DataAndStorage/ModelData/ObservedObject.md @@ -0,0 +1,17 @@ +# ``ObservedObject`` + +## Topics + +### Creating an observed object + +- ``init(wrappedValue:)`` + +- ``init(initialValue:)`` + +### Getting the value + +- ``wrappedValue`` + +- ``projectedValue`` + +- ``Wrapper`` diff --git a/Sources/OpenSwiftUI/OpenSwiftUI.docc/DataAndStorage/ModelData/State.md b/Sources/OpenSwiftUI/OpenSwiftUI.docc/DataAndStorage/ModelData/State.md new file mode 100644 index 00000000..8e894e30 --- /dev/null +++ b/Sources/OpenSwiftUI/OpenSwiftUI.docc/DataAndStorage/ModelData/State.md @@ -0,0 +1,17 @@ +# ``State`` + +## Topics + +### Creating a state + +- ``init(wrappedValue:)`` + +- ``init(initialValue:)`` + +- ``init()`` + +### Getting the value + +- ``wrappedValue`` + +- ``projectedValue`` diff --git a/Sources/OpenSwiftUI/OpenSwiftUI.docc/DataAndStorage/ModelData/StateObject.md b/Sources/OpenSwiftUI/OpenSwiftUI.docc/DataAndStorage/ModelData/StateObject.md new file mode 100644 index 00000000..45d0b094 --- /dev/null +++ b/Sources/OpenSwiftUI/OpenSwiftUI.docc/DataAndStorage/ModelData/StateObject.md @@ -0,0 +1,13 @@ +# ``StateObject`` + +## Topics + +### Creating a state object + +- ``init(wrappedValue:)`` + +### Getting the value + +- ``wrappedValue`` + +- ``projectedValue`` diff --git a/Sources/OpenSwiftUI/OpenSwiftUI.docc/OpenSwiftUI.md b/Sources/OpenSwiftUI/OpenSwiftUI.docc/OpenSwiftUI.md index e837fceb..03740cbf 100644 --- a/Sources/OpenSwiftUI/OpenSwiftUI.docc/OpenSwiftUI.md +++ b/Sources/OpenSwiftUI/OpenSwiftUI.docc/OpenSwiftUI.md @@ -22,6 +22,8 @@ You can integrate OpenSwiftUI views with objects from the [UIKit](https://develo ### Data and storage +- + - ### Views