我有一个独立的
View
,因为它可以处理自己的状态。
View
维护着粒子模型的列表。 View
管理每个模型的生命周期和渲染。
我想触发视图以一种特殊的方式创建一个新的粒子,例如响应计时器或用户输入。
目前,我正在使用
@Observable
视图模型。
视图模型有一个标志“articleRequested”。
为了管理此标志,我在视图模型上调用一个便利函数:
func requestParticle() {
guard particleRequested == false else { return }
particleRequested = true
// Wait a brief moment, then reset the flag.
Task { @MainActor [weak self] in
guard let self = self else { return }
try await Task.sleep(for: .seconds(0.01))
self.particleRequested = false
}
}
在视图本身中,我只观察到标志的更改:
.onChange(of: viewModel.particleRequested, { _, newValue in
if newValue {
addParticle() // Modifies View's state.
}
})
这感觉……很卡顿。整个方法都有气味。有没有更好、更干净的方法?
您的总体想法是正确的 - 使用
onChange
检测视图更新中的更改并执行所需的操作。您当前的代码可以简化很多:
true
上的操作。每当值改变时触发动作struct Particles<Trigger: Equatable>: View {
@State private var particles = [String]()
let newParticleTrigger: Trigger
var body: some View {
List(particles, id: \.self) {
Text($0)
}
.onChange(of: newParticleTrigger) {
particles.append(UUID().uuidString)
}
}
}
// Example usage:
struct ContentView: View {
@State private var newParticleTrigger = false
var body: some View {
Button("Foo") {
newParticleTrigger.toggle()
}
Particles(newParticleTrigger: newParticleTrigger)
}
}
许多 SwiftUI 内置视图和修饰符中都使用相同的模式,例如
PhaseAnimator
、sensoryFeedback
、task(id:)
以及任何演示修饰符,如 sheet
、confirmationDialog
等。
如果您想要触发多个不同的事物,只需将所有触发器放入
struct
,
struct Particles: View {
struct Triggers: Hashable {
fileprivate var trigger1 = false
fileprivate var trigger2 = false
fileprivate var trigger3 = false
mutating func triggerThing1() {
trigger1.toggle()
}
mutating func triggerThing2() {
trigger2.toggle()
}
mutating func triggerThing3() {
trigger3.toggle()
}
}
let triggers: Triggers
var body: some View {
// ...
.onChange(of: triggers.trigger1) {
// ...
}
.onChange(of: triggers.trigger2) {
// ...
}
.onChange(of: triggers.trigger3) {
// ...
}
}
}
另一种设计是利用环境:
extension EnvironmentValues {
@Entry var trigger1: Bool = false
@Entry var trigger2: Bool = false
@Entry var trigger3: Bool = false
}
struct OnEnvironmentChange<Trigger: Equatable>: ViewModifier {
let environmentValueKeyPath: WritableKeyPath<EnvironmentValues, Bool>
let trigger: Trigger
@State private var currentState = false
func body(content: Content) -> some View {
content
.environment(environmentValueKeyPath, currentState)
.onChange(of: trigger) {
currentState.toggle()
}
}
}
extension View {
func thing1<T: Equatable>(trigger: T) -> some View {
modifier(OnEnvironmentChange(environmentValueKeyPath: \.trigger1, trigger: trigger))
}
func thing2<T: Equatable>(trigger: T) -> some View {
modifier(OnEnvironmentChange(environmentValueKeyPath: \.trigger2, trigger: trigger))
}
func thing3<T: Equatable>(trigger: T) -> some View {
modifier(OnEnvironmentChange(environmentValueKeyPath: \.trigger3, trigger: trigger))
}
}
struct Particles: View {
@Environment(\.trigger1) var trigger1
@Environment(\.trigger2) var trigger2
@Environment(\.trigger3) var trigger3
var body: some View {
// ...
.onChange(of: trigger1) {
// ...
}
.onChange(of: trigger2) {
// ...
}
.onChange(of: trigger3) {
// ...
}
}
}
这样,当使用
Particles
时,你不需要知道你不想触发的触发器。仅应用与您要触发的内容相对应的视图修饰符。
// suppose this view only wants to trigger thing1
struct ContentView: View {
@State private var thing1 = false
var body: some View {
Button("Foo") {
thing1.toggle()
}
Particles()
// then only apply this modifier
.thing1(trigger: thing1)
// don't need to care about thing2 or thing3
}
}