|
| 1 | +# Managing user interface state |
| 2 | + |
| 3 | +Encapsulate view-specific data within your app’s view hierarchy to make your |
| 4 | +views reusable. |
| 5 | + |
| 6 | +## Overview |
| 7 | + |
| 8 | +Store data as state in the least common ancestor of the views that need the data |
| 9 | +to establish a single source of truth that’s shared across views. Provide the |
| 10 | +data as read-only through a Swift property, or create a two-way connection to |
| 11 | +the state with a binding. OpenSwiftUI watches for changes in the data, and |
| 12 | +updates any affected views as needed. |
| 13 | + |
| 14 | + |
| 15 | + |
| 16 | +Don’t use state properties for persistent storage because the life cycle of |
| 17 | +state variables mirrors the view life cycle. Instead, use them to manage |
| 18 | +transient state that only affects the user interface, like the highlight state |
| 19 | +of a button, filter settings, or the currently selected list item. You might |
| 20 | +also find this kind of storage convenient while you prototype, before you’re |
| 21 | +ready to make changes to your app’s data model. |
| 22 | + |
| 23 | +### Manage mutable values as state |
| 24 | + |
| 25 | +If a view needs to store data that it can modify, declare a variable with the |
| 26 | +``State`` property wrapper. For example, you can create an isPlaying Boolean inside |
| 27 | +a podcast player view to keep track of when a podcast is running: |
| 28 | + |
| 29 | +``` |
| 30 | +struct PlayerView: View { |
| 31 | + @State private var isPlaying: Bool = false |
| 32 | + |
| 33 | + var body: some View { |
| 34 | + // ... |
| 35 | + } |
| 36 | +} |
| 37 | +``` |
| 38 | + |
| 39 | +Marking the property as state tells the framework to manage the underlying |
| 40 | +storage. Your view reads and writes the data, found in the state’s |
| 41 | +``wrappedValue`` property, by using the property name. When you change the |
| 42 | +value, OpenSwiftUI updates the affected parts of the view. For example, you can |
| 43 | +add a button to the PlayerView that toggles the stored value when tapped, and |
| 44 | +that displays a different image depending on the stored value: |
| 45 | + |
| 46 | +``` |
| 47 | +Button(action: { |
| 48 | + self.isPlaying.toggle() |
| 49 | +}) { |
| 50 | + Image(systemName: isPlaying ? "pause.circle" : "play.circle") |
| 51 | +} |
| 52 | +``` |
| 53 | + |
| 54 | +Limit the scope of state variables by declaring them as private. This ensures |
| 55 | +that the variables remain encapsulated in the view hierarchy that declares them. |
| 56 | + |
| 57 | +### Declare Swift properties to store immutable values |
| 58 | + |
| 59 | +To provide a view with data that the view doesn’t modify, declare a standard |
| 60 | +Swift property. For example, you can extend the podcast player to have an input |
| 61 | +structure that contains strings for the episode title and the show name: |
| 62 | + |
| 63 | +``` |
| 64 | +struct PlayerView: View { |
| 65 | + let episode: Episode // The queued episode. |
| 66 | + @State private var isPlaying: Bool = false |
| 67 | + |
| 68 | + var body: some View { |
| 69 | + VStack { |
| 70 | + // Display information about the episode. |
| 71 | + Text(episode.title) |
| 72 | + Text(episode.showTitle) |
| 73 | +
|
| 74 | +
|
| 75 | + Button(action: { |
| 76 | + self.isPlaying.toggle() |
| 77 | + }) { |
| 78 | + Image(systemName: isPlaying ? "pause.circle" : "play.circle") |
| 79 | + } |
| 80 | + } |
| 81 | + } |
| 82 | +} |
| 83 | +``` |
| 84 | + |
| 85 | +While the value of the episode property is a constant for PlayerView, it doesn’t |
| 86 | +need to be constant in this view’s parent view. When the user selects a |
| 87 | +different episode in the parent, OpenSwiftUI detects the state change and |
| 88 | +recreates the PlayerView with a new input. |
| 89 | + |
| 90 | +### Share access to state with bindings |
| 91 | + |
| 92 | +If a view needs to share control of state with a child view, declare a property |
| 93 | +in the child with the ``Binding`` property wrapper. A binding represents a |
| 94 | +reference to existing storage, preserving a single source of truth for the |
| 95 | +underlying data. For example, if you refactor the podcast player view’s button |
| 96 | +into a child view called PlayButton, you can give it a binding to the isPlaying |
| 97 | +property: |
| 98 | + |
| 99 | +``` |
| 100 | +struct PlayButton: View { |
| 101 | + @Binding var isPlaying: Bool |
| 102 | + |
| 103 | + var body: some View { |
| 104 | + Button(action: { |
| 105 | + self.isPlaying.toggle() |
| 106 | + }) { |
| 107 | + Image(systemName: isPlaying ? "pause.circle" : "play.circle") |
| 108 | + } |
| 109 | + } |
| 110 | +} |
| 111 | +``` |
| 112 | + |
| 113 | +As shown above, you read and write the binding’s wrapped value by referring |
| 114 | +directly to the property, just like state. But unlike a state property, the |
| 115 | +binding doesn’t have its own storage. Instead, it references a state property |
| 116 | +stored somewhere else, and provides a two-way connection to that storage. |
| 117 | + |
| 118 | +When you instantiate PlayButton, provide a binding to the corresponding state |
| 119 | +variable declared in the parent view by prefixing it with the dollar sign ($): |
| 120 | + |
| 121 | +``` |
| 122 | +struct PlayerView: View { |
| 123 | + var episode: Episode |
| 124 | + @State private var isPlaying: Bool = false |
| 125 | + |
| 126 | + var body: some View { |
| 127 | + VStack { |
| 128 | + Text(episode.title) |
| 129 | + Text(episode.showTitle) |
| 130 | + PlayButton(isPlaying: $isPlaying) // Pass a binding. |
| 131 | + } |
| 132 | + } |
| 133 | +} |
| 134 | +``` |
| 135 | + |
| 136 | +The $ prefix asks a wrapped property for its projectedValue, which for state is |
| 137 | +a binding to the underlying storage. Similarly, you can get a binding from a |
| 138 | +binding using the $ prefix, allowing you to pass a binding through an arbitrary |
| 139 | +number of levels of view hierarchy. |
| 140 | + |
| 141 | +You can also get a binding to a scoped value within a state variable. For |
| 142 | +example, if you declare episode as a state variable in the player’s parent view, |
| 143 | +and the episode structure also contains an isFavorite Boolean that you want to |
| 144 | +control with a toggle, then you can refer to $episode.isFavorite to get a |
| 145 | +binding to the episode’s favorite status: |
| 146 | + |
| 147 | +``` |
| 148 | +struct Podcaster: View { |
| 149 | + @State private var episode = Episode(title: "Some Episode", |
| 150 | + showTitle: "Great Show", |
| 151 | + isFavorite: false) |
| 152 | + var body: some View { |
| 153 | + VStack { |
| 154 | + Toggle("Favorite", isOn: $episode.isFavorite) // Bind to the Boolean. |
| 155 | + PlayerView(episode: episode) |
| 156 | + } |
| 157 | + } |
| 158 | +} |
| 159 | +``` |
| 160 | + |
| 161 | +### Animate state transitions |
| 162 | + |
| 163 | +When the view state changes, OpenSwiftUI updates affected views right away. If |
| 164 | +you want to smooth visual transitions, you can tell SwiftUI to animate them by |
| 165 | +wrapping the state change that triggers them in a call to the |
| 166 | +``withAnimation(_:_:)`` function. For example, you can animate changes |
| 167 | +controlled by the isPlaying Boolean: |
| 168 | + |
| 169 | +``` |
| 170 | +withAnimation(.easeInOut(duration: 1)) { |
| 171 | + self.isPlaying.toggle() |
| 172 | +} |
| 173 | +``` |
| 174 | + |
| 175 | +By changing isPlaying inside the animation function’s trailing closure, you tell |
| 176 | +OpenSwiftUI to animate anything that depends on the wrapped value, like a scale |
| 177 | +effect on the button’s image: |
| 178 | + |
| 179 | +``` |
| 180 | +Image(systemName: isPlaying ? "pause.circle" : "play.circle") |
| 181 | + .scaleEffect(isPlaying ? 1 : 1.5) |
| 182 | +``` |
| 183 | + |
| 184 | +OpenSwiftUI transitions the scale effect input over time between the given |
| 185 | +values of 1 and 1.5, using the curve and duration that you specify, or |
| 186 | +reasonable default values if you provide none. On the other hand, the image |
| 187 | +content isn’t affected by the animation, even though the same Boolean dictates |
| 188 | +which system image to display. That’s because OpenSwiftUI can’t incrementally |
| 189 | +transition in a meaningful way between the two strings `pause.circle` and |
| 190 | +`play.circle`. |
| 191 | + |
| 192 | +You can add animation to a state property, or as in the above example, to a |
| 193 | +binding. Either way, OpenSwiftUI animates any view changes that happen when the |
| 194 | +underlying stored value changes. For example, if you add a background color to |
| 195 | +the PlayerView — at a level of view hierarchy above the location of the |
| 196 | +animation block — OpenSwiftUI animates that as well: |
| 197 | + |
| 198 | +``` |
| 199 | +VStack { |
| 200 | + Text(episode.title) |
| 201 | + Text(episode.showTitle) |
| 202 | + PlayButton(isPlaying: $isPlaying) |
| 203 | +} |
| 204 | +.background(isPlaying ? Color.green : Color.red) // Transitions with animation. |
| 205 | +``` |
| 206 | + |
| 207 | +When you want to apply animations to specific views, rather than across all |
| 208 | +views triggered by a change in state, use the ``View/animation(_:value:)`` view |
| 209 | +modifier instead. |
0 commit comments