SwiftUI 状态管理
本文描述了使用 SwiftUI 编写图形界面的应用应该如何进行状态管理
前提条件
- 了解 SwiftUI 基础组件的用法, 如
Text
,VStack
.
@State 声明状态
基础用法
@State
是 SwiftUI 一个属性包装器(Property Wrapper), 用于在视图内部存储和管理可变状态. 当状态发生变化时, SwiftUI 会自动重新计算依赖该状态的视图, 并更新界面. 比如在下面这个例子中, 修改 detailIsShown
变量的值为 false
, 将会导致 "Detail" 文字不再显示.
swift
struct ContentView: View {
@State var detailIsShown = false
var body: some View {
VStack(spacing: 16) {
Button("Toggle") {
detailIsShown.toggle()
}
if detailIsShown {
Text("Detail")
}
}
}
}
监听状态变化
可以使用 View
组件的装饰器 .onChange
来监听状态变化. 如果需要去抖动, 可以使用 Timer.scheduledTimer
方法.
swift
struct ContentView: View {
@State private var searchInput = ""
@State private var searchResult: [CampusModel] = []
@State private var debounceTimer: Timer? = nil
private let debounceInterval: TimeInterval = 0.5
var body: some View {
VStack(spacing: 16) {
TextField("Search", text: $searchInput)
}
.onChange(of: searchInput, initial: true) { _oldValue, newValue in
debounceTimer?.invalidate()
debounceTimer = Timer.scheduledTimer(withTimeInterval: debounceInterval, repeats: false) { _ in
search(searchInput: newValue)
}
}
}
}
@Binding 传递状态到子组件
基础用法
如果需要将 @State
包装的状态传递到子组件, 则需要在子组件中声明一个使用 @Binding
装饰器包装的变量来接收.
swift
struct ParentView: View {
@State private var isOn = false
var body: some View {
ToggleView(isOn: $isOn)
}
}
struct ToggleView: View {
@Binding var isOn: Bool
var body: some View {
Toggle("Switch", isOn: $isOn)
}
}
等价写法
有时候, 子组件的 Binding
值和父组件的 State
值不完全相同, 那么我们可以使用 Binding(get: @escaping () -> Value, set: @escaping (Value) -> Void)
来进行转换
swift
struct ParentView: View {
@State private var isOn = false
var body: some View {
ToggleView(isOn: Binding<Bool>(
get: { isOn },
set: { isOn = $0 }
))
}
}
@StateObject 声明状态对象
基础用法
如果状态不是一个值而是一个对象, 那么创建这个状态的时候, 使用的属性包装器将不再是 @State
而是 @StateObject
. 使用 @StateObject
包装的变量, 其类型需要符合 ObservableObject
协议. 在该类型中声明状态变量采用 @Published
属性包装器而不是 @State
swift
import Combine
struct FruitModel: Identifiable {
let id: String = UUID().uuidString
var label: String
}
class FruitViewModel: ObservableObject {
@Published var fruits: [FruitModel] = []
init() {
self.getFruits()
}
func getFruits() {
self.fruits = [
Fruit(label: "Apple"),
Fruit(label: "Orange")
]
}
}
struct FruitsView: View {
@StateObject var vm = FruitViewModel()
var body: some View {
List {
ForEach($vm.fruits) { fruit in
Text(fruit.label)
}
}
}
}
监听状态变化
swift
import Combine
class FruitViewModel: ObservableObject {
@Published var fruits: [FruitModel] = []
@Published var searchText = ""
@Published var searchResults: [FruitModel] = []
private var cancellables = Set<AnyCancellable>()
init() {
self.getFruits()
addSubscribers()
}
func getFruits() {
self.fruits = [
Fruit(label: "Apple"),
Fruit(label: "Orange")
]
}
func search(searchText: String) {
searchResults = fruits.filter({ $0.label.contains(searchText) })
}
private func addSubscribers() {
$searchText
// Debounce 0.5s
.debounce(for: .seconds(0.5), scheduler: DispatchQueue.main)
.sink { [weak self] newVal in
guard let self = self else { return }
self.search(searchText: newVal)
}
.store(in: &cancellables)
}
}
从外部传入初始值
swift
struct HomeView: View {
@StateObject var vm: HomeViewModel
init(audioManager: AudioManager) {
_vm = StateObject(wrappedValue: HomeViewModel(audioManager: audioManager))
}
...
}
@EnvironmentObject 全局状态
- 定义数据类型
swift
// Models/CreatureModel.swift
import Foundation
struct CreatureModel: Identifiable {
var name: String
var emoji: String
var id = UUID()
}
- 定义Store
swift
// ViewModels/CreatureStore.swift
import Foundation
class CreatureStore: ObservableObject {
@Published var creatures: [CreatureModel] = []
init() {
self.getData()
}
func getData() {
self.creatures = [
Creature(name: "Gorilla", emoji: "🦍"),
Creature(name: "Peacock", emoji: "🦚"),
Creature(name: "Squid", emoji: "🦑"),
]
}
}
- 在 AppEntry 创建 Store, 然后作为 environmentObject 传入应用
swift
// AppEntry.swift
import SwiftUI
@main
struct AppEntry: App {
@StateObject var creatureStore = CreatureStore() // Step#1
var body: some Scene {
WindowGroup {
ContentView()
.environmentObject(creatureStore) // Step#2
}
}
}
- 在子组件中使用 Store
swift
struct ContentView: View {
@EnvironmentObject var creatureStore : CreatureStore
var body: some View {
List {
ForEach(creatureStore.creatures) { creature in
CreatureRow(creature: creature)
}
}
}
}
// 如需预览, 记得使用 environmentObject 装饰器传入一个实例, 不然预览会崩溃.
#Preview {
ContentView()
.environmentObject(DeveloperPreview.instance.creatureStore)
}