文章

管理用户界面状态

将视图专用数据封装到 App 的视图层次结构中,使视图可以重复使用。

最新英文文章

Managing user interface state


概览

如果视图需要数据来确定各视图间共享的单一数据源,则可以在视图最不常见的上级结构中将数据存储为状态。既可通过一个 Swift 属性以只读方式提供这个数据,也可使用绑定创建与状态的双向连接。SwiftUI 会观察数据的变化,并根据需要更新任何受影响的视图。

示意图显示了状态存储在一个视图中,并与另一个视图共享

请勿将状态属性用于持久存储,因为状态变量的生命周期与视图生命周期是一样的。应将它们用于管理仅影响用户界面的瞬间状态,例如按钮的高亮显示状态、筛选器设置或当前选定的列表项目。你可能还会发现,在你准备对 App 数据模型进行更改之前制作原型时,这种存储很方便。

将可变值作为状态来管理

如果视图需要储存它可以修改的数据,应通过 State (英文) 属性包装器声明一个变量。例如,你可以在播客播放器视图中创建一个 isPlaying Boolean,以跟踪播客何时运行:


struct PlayerView: View {
    @State private var isPlaying: Bool = false
    
    var body: some View {
        // ...
    }
}

将属性标记为状态会指示框架管理底层存储。你的视图使用属性名称,读取和写入在状态的 wrappedValue (英文) 属性中找到的数据。在你更改值时,SwiftUI 会更新视图的受影响部分。例如,你可以向 PlayerView 添加一个按钮,在轻点该按钮后切换存储的值并根据存储的值显示不同的图像:


Button(action: {
    self.isPlaying.toggle()
}) {
    Image(systemName: isPlaying ? "pause.circle" : "play.circle")
}

通过将状态变量声明为私有变量来限制它们的范围。这确保变量在声明它们的视图层次结构中保持封装状态。

声明 Swift 属性以存储不可变值

若要为视图提供其不修改的数据,请声明一个标准 Swift 属性。例如,你可以扩展播客播放器,以增加一个输入结构,用于包含代表单集标题和节目名称的字符串:


struct PlayerView: View {
    let episode: Episode // The queued episode.
    @State private var isPlaying: Bool = false
    
    var body: some View {
        VStack {
            // Display information about the episode.
            Text(episode.title)
            Text(episode.showTitle)


            Button(action: {
                self.isPlaying.toggle()
            }) {
                Image(systemName: isPlaying ? "pause.circle" : "play.circle")
            }
        }
    }
}

尽管单集属性的值对于 PlayerView 是一个常量,但在这个视图的父视图中,它不一定要是常量。当用户在父项中选择另一个单集时,SwiftUI 会检测到状态变化并使用一个新输入来重新创建 PlayerView

通过绑定共享状态访问

如果视图需要与一个子视图共享状态的控件,应在带有 Binding (英文) 属性包装器的子项中声明一个属性。绑定表示对现有存储的引用,从而保留底层数据的单一数据源。例如,如果你将播客播放器视图的按钮重构成一个名为 PlayButton 的子视图,你可以给它提供一个与 isPlaying 属性的绑定:


struct PlayButton: View {
    @Binding var isPlaying: Bool
    
    var body: some View {
        Button(action: {
            self.isPlaying.toggle()
        }) {
            Image(systemName: isPlaying ? "pause.circle" : "play.circle")
        }
    }
}

如上所示,你可以通过直接引用属性来读取和写入绑定包装的值,这一点与状态属性一样。但是与状态属性不同的是,绑定没有自己的存储,而是引用一个存储在其他地方的状态属性,并提供与该存储的双向连接。

当你实例化 PlayButton 时,可通过添加一个美元符号 ($) 前缀,提供与父视图中声明的相应状态变量的绑定:


struct PlayerView: View {
    var episode: Episode
    @State private var isPlaying: Bool = false
    
    var body: some View {
        VStack {
            Text(episode.title)
            Text(episode.showTitle)
            PlayButton(isPlaying: $isPlaying) // Pass a binding.
        }
    }
}

$ 前缀要求为它的 projectedValue (英文) 提供一个包装的属性,这对状态而言,就是与底层存储的绑定。同样,你可以通过使用 $ 前缀的绑定获得绑定,从而让你可以在视图层次结构的任意数量层级间传递绑定。

你还可以获得与状态变量中限定范围的值的绑定。例如,如果你在播放器的父视图中将 episode 声明为状态变量,并且单集结构还包含一个你想要通过切换控制的 isFavorite Boolean,那么,你可以引用 $episode.isFavorite 来获得与单集的个人收藏状态的绑定:


struct Podcaster: View {
    @State private var episode = Episode(title: "Some Episode",
                                         showTitle: "Great Show",
                                         isFavorite: false)
    var body: some View {
        VStack {
            Toggle("Favorite", isOn: $episode.isFavorite) // Bind to the Boolean.
            PlayerView(episode: episode)
        }
    }
}

为状态过渡添加动画效果

当视图状态发生改变时,SwiftUI 会立即更新受影响的视图。如果你需要实现顺畅的视觉过渡,可以将触发过渡的状态更改包装在对 withAnimation(_:_:) (英文) 函数的调用中,以指示 SwiftUI 为它们添加动画效果。例如,你可以为由 isPlaying Boolean 控制的更改添加动画效果:


withAnimation(.easeInOut(duration: 1)) {
    self.isPlaying.toggle()
}

通过更改动画函数结尾闭包中的 isPlaying,可指示 SwiftUI 为依赖于包装值的一切内容添加动画效果,例如,按钮图像上的缩放特效:


Image(systemName: isPlaying ? "pause.circle" : "play.circle")
    .scaleEffect(isPlaying ? 1 : 1.5)

SwiftUI 会使用你指定的曲线和持续时间,如果你未提供则使用合理的默认值,在一段时间内在给定的 11.5 值之间过渡缩放特效输入。另一方面,图像内容不会受到动画的影响,即使同一个 Boolean 规定要显示哪个系统图像也是如此。那是因为 SwiftUI 无法以有意义的方式在两个字符串 pause.circleplay.circle 之间逐步过渡。

你可以向状态属性添加动画,或与上述示例一样,向绑定添加动画。无论是哪一种方式,在底层存储的值发生变化时,SwiftUI 都会为发生的任何视图变化添加动画效果。例如,如果你在动画块位置上方的某个视图层次结构层级向 PlayerView 添加背景色,SwiftUI 同样会为此添加动画效果:


VStack {
    Text(episode.title)
    Text(episode.showTitle)
    PlayButton(isPlaying: $isPlaying)
}
.background(isPlaying ? Color.green : Color.red) // Transitions with animation.

如果你想将动画应用于特定的视图,而不是状态变化触发的所有视图,请改用 animation(_:) (英文) 视图修饰符。

另请参阅

视图状态