+ 我要发布
我发布的 我的标签 发现
浏览器扩展
斑点象@Edge

Swift如何管理用户界面状态

将视图专用数据封装到 App 的视图层次结构中,使视图可以重复使用。 概览 如果视图需要数据来确定各视图间共享的单一数据源,则可以在视图最不常见的上级结构中将数据存储为状态。既可通过一个 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 会使用你指定的曲线和持续时间,如果你未提供则使用合理的默认值,在一段时间内在给定的 1 和 1.5 值之间过渡缩放特效输入。另一方面,图像内容不会受到动画的影响,即使同一个 Boolean 规定要显示哪个系统图像也是如此。那是因为 SwiftUI 无法以有意义的方式在两个字符串 pause.circle 和 play.circle 之间逐步过渡。 你可以向状态属性添加动画,或与上述示例一样,向绑定添加动画。无论是哪一种方式,在底层存储的值发生变化时,SwiftUI 都会为发生的任何视图变化添加动画效果。例如,如果你在动画块位置上方的某个视图层次结构层级向 PlayerView 添加背景色,SwiftUI 同样会为此添加动画效果: VStack { Text(episode.title) Text(episode.showTitle) PlayButton(isPlaying: $isPlaying) } .background(isPlaying ? Color.green : Color.red) // Transitions with animation. 如果你想将动画应用于特定的视图,而不是状态变化触发的所有视图,请改用 animation(_:) (英文) 视图修饰符。