根据保存的状态自动激活 SwiftUI 中的动画
Activating an animation in SwiftUI automatically based on saved state
我正在尝试编写一个显示 3 个按钮的视图,我无法让动画在加载时启动。
当一个按钮被点击时,我希望它动画直到:
- 第二次点击
- 点击了 3 个按钮中的另一个
我已经使用 @Environment 对象存储 运行 状态的代码。它很好地在 3 个按钮之间切换:
代码在这里:
struct ContentView : View {
@EnvironmentObject var model : ModelClockToggle
var body: some View {
VStack {
ForEach(0...2) { timerButton in
ActivityBreak(myId: timerButton)
.padding()
}
}
}
}
import SwiftUI
struct ActivityBreak : View {
var myId: Int
@EnvironmentObject var model : ModelClockToggle
let anim1 = Animation.basic(duration: 1.0, curve: .easeInOut).repeatCount(Int.max)
let noAni = Animation.basic(duration: 0.2, curve: .easeInOut).repeatCount(0)
var body: some View {
return Circle()
.foregroundColor(.red)
.scaleEffect(self.model.amIRunning(clock: self.myId) ? 1.0 : 0.6)
.animation( self.model.amIRunning(clock: self.myId) ? anim1 : noAni )
.tapAction {
self.model.toggle(clock: self.myId)
}
}
}
为了完整性,模型是:
import Foundation
import SwiftUI
import Combine
class ModelClockToggle: BindableObject {
let didChange = PassthroughSubject<ModelClockToggle, Never>()
private var clocksOn: [Bool] = [false,false,false]
init() {
clocksOn = []
clocksOn.append(UserDefaults.standard.bool(forKey: "toggle1"))
clocksOn.append(UserDefaults.standard.bool(forKey: "toggle2"))
clocksOn.append(UserDefaults.standard.bool(forKey: "toggle3"))
debugPrint(clocksOn)
}
func toggle(clock: Int) {
debugPrint(#function)
if clocksOn[clock] {
clocksOn[clock].toggle()
} else {
clocksOn = [false,false,false]
clocksOn[clock].toggle()
}
saveState()
didChange.send(self)
}
func amIRunning(clock: Int) -> Bool {
debugPrint(clocksOn)
return clocksOn[clock]
}
private func saveState() {
UserDefaults.standard.set(clocksOn[0], forKey: "toggle1")
UserDefaults.standard.set(clocksOn[1], forKey: "toggle2")
UserDefaults.standard.set(clocksOn[2], forKey: "toggle3")
}
}
如何根据传递到视图中的@Environment 对象使重复动画在加载时开始?现在 SwiftUI 似乎只在加载视图后才考虑状态更改。
我尝试添加一个 .onAppear 修饰符,但这意味着我必须使用不同的动画师 - 这会产生非常奇怪的效果。
感谢收到帮助。
在您的示例中,您使用的是隐式动画。这些动画会寻找任何可动画参数的变化,例如大小、位置、不透明度、颜色等。当 SwiftUI 检测到任何变化时,它会对其进行动画处理。
在您的特定情况下,圆在不活动时通常缩放为 0.6,在活动时缩放为 1.0。在非活动状态和活动状态之间变化,使您的 Circle 改变比例,并且这种变化是循环动画的。
但是,您的问题是最初以 1.0 比例加载的 Circle(因为模型说它处于活动状态)不会检测到变化:它从 1.0 开始并保持在 1.0。所以没有什么动画。
在您的评论中,您提到了一个解决方案,即让模型推迟加载 Circle 状态的状态。这样,您的视图首先被创建,然后您要求模型加载状态,然后您的视图中有一个可以动画的变化。这行得通,但是有一个问题。
您正在使模型的行为依赖于视图。当它真的应该相反时。假设屏幕上有两个视图实例。视时间而定,一个会启动正常,但另一个不会。
解决它的方法是确保整个逻辑都由视图本身处理。您想要完成的是,您的 Circle always 以 0.6 的比例创建。然后,您检查模型以查看 Circel 是否应该处于活动状态。如果是这样,您立即将其更改为 1.0。这样可以保证视图的动画效果。
这是一个可能的解决方案,它使用一个名为 booted
的 @State
变量来跟踪它。您的圈子将始终 以 0.6 的比例创建,但是一旦调用 onAppear() 方法,视图将缩放到 1.0(如果处于活动状态),并产生相应的动画。
struct ActivityBreak : View {
var myId: Int
@EnvironmentObject var model : ModelClockToggle
@State private var booted: Bool = false
// Beta 4
let anim1 = Animation.easeInOut(duration: 1.0).repeatCount(Int.max)
let noAni = Animation.easeInOut(duration: 0.2).repeatCount(0)
// Beta 3
// let anim1 = Animation.basic(duration: 1.0, curve: .easeInOut).repeatCount(Int.max)
// let noAni = Animation.basic(duration: 0.2, curve: .easeInOut).repeatCount(0)
var body: some View {
return Circle()
.foregroundColor(.red)
.scaleEffect(!booted ? 0.6 : self.model.amIRunning(clock: self.myId) ? 1.0 : 0.6)
.animation( self.model.amIRunning(clock: self.myId) ? anim1 : noAni )
.tapAction {
self.model.toggle(clock: self.myId)
}
.onAppear {
self.booted = true
}
}
}
我正在尝试编写一个显示 3 个按钮的视图,我无法让动画在加载时启动。
当一个按钮被点击时,我希望它动画直到:
- 第二次点击
- 点击了 3 个按钮中的另一个
我已经使用 @Environment 对象存储 运行 状态的代码。它很好地在 3 个按钮之间切换:
代码在这里:
struct ContentView : View {
@EnvironmentObject var model : ModelClockToggle
var body: some View {
VStack {
ForEach(0...2) { timerButton in
ActivityBreak(myId: timerButton)
.padding()
}
}
}
}
import SwiftUI
struct ActivityBreak : View {
var myId: Int
@EnvironmentObject var model : ModelClockToggle
let anim1 = Animation.basic(duration: 1.0, curve: .easeInOut).repeatCount(Int.max)
let noAni = Animation.basic(duration: 0.2, curve: .easeInOut).repeatCount(0)
var body: some View {
return Circle()
.foregroundColor(.red)
.scaleEffect(self.model.amIRunning(clock: self.myId) ? 1.0 : 0.6)
.animation( self.model.amIRunning(clock: self.myId) ? anim1 : noAni )
.tapAction {
self.model.toggle(clock: self.myId)
}
}
}
为了完整性,模型是:
import Foundation
import SwiftUI
import Combine
class ModelClockToggle: BindableObject {
let didChange = PassthroughSubject<ModelClockToggle, Never>()
private var clocksOn: [Bool] = [false,false,false]
init() {
clocksOn = []
clocksOn.append(UserDefaults.standard.bool(forKey: "toggle1"))
clocksOn.append(UserDefaults.standard.bool(forKey: "toggle2"))
clocksOn.append(UserDefaults.standard.bool(forKey: "toggle3"))
debugPrint(clocksOn)
}
func toggle(clock: Int) {
debugPrint(#function)
if clocksOn[clock] {
clocksOn[clock].toggle()
} else {
clocksOn = [false,false,false]
clocksOn[clock].toggle()
}
saveState()
didChange.send(self)
}
func amIRunning(clock: Int) -> Bool {
debugPrint(clocksOn)
return clocksOn[clock]
}
private func saveState() {
UserDefaults.standard.set(clocksOn[0], forKey: "toggle1")
UserDefaults.standard.set(clocksOn[1], forKey: "toggle2")
UserDefaults.standard.set(clocksOn[2], forKey: "toggle3")
}
}
如何根据传递到视图中的@Environment 对象使重复动画在加载时开始?现在 SwiftUI 似乎只在加载视图后才考虑状态更改。
我尝试添加一个 .onAppear 修饰符,但这意味着我必须使用不同的动画师 - 这会产生非常奇怪的效果。
感谢收到帮助。
在您的示例中,您使用的是隐式动画。这些动画会寻找任何可动画参数的变化,例如大小、位置、不透明度、颜色等。当 SwiftUI 检测到任何变化时,它会对其进行动画处理。
在您的特定情况下,圆在不活动时通常缩放为 0.6,在活动时缩放为 1.0。在非活动状态和活动状态之间变化,使您的 Circle 改变比例,并且这种变化是循环动画的。
但是,您的问题是最初以 1.0 比例加载的 Circle(因为模型说它处于活动状态)不会检测到变化:它从 1.0 开始并保持在 1.0。所以没有什么动画。
在您的评论中,您提到了一个解决方案,即让模型推迟加载 Circle 状态的状态。这样,您的视图首先被创建,然后您要求模型加载状态,然后您的视图中有一个可以动画的变化。这行得通,但是有一个问题。
您正在使模型的行为依赖于视图。当它真的应该相反时。假设屏幕上有两个视图实例。视时间而定,一个会启动正常,但另一个不会。
解决它的方法是确保整个逻辑都由视图本身处理。您想要完成的是,您的 Circle always 以 0.6 的比例创建。然后,您检查模型以查看 Circel 是否应该处于活动状态。如果是这样,您立即将其更改为 1.0。这样可以保证视图的动画效果。
这是一个可能的解决方案,它使用一个名为 booted
的 @State
变量来跟踪它。您的圈子将始终 以 0.6 的比例创建,但是一旦调用 onAppear() 方法,视图将缩放到 1.0(如果处于活动状态),并产生相应的动画。
struct ActivityBreak : View {
var myId: Int
@EnvironmentObject var model : ModelClockToggle
@State private var booted: Bool = false
// Beta 4
let anim1 = Animation.easeInOut(duration: 1.0).repeatCount(Int.max)
let noAni = Animation.easeInOut(duration: 0.2).repeatCount(0)
// Beta 3
// let anim1 = Animation.basic(duration: 1.0, curve: .easeInOut).repeatCount(Int.max)
// let noAni = Animation.basic(duration: 0.2, curve: .easeInOut).repeatCount(0)
var body: some View {
return Circle()
.foregroundColor(.red)
.scaleEffect(!booted ? 0.6 : self.model.amIRunning(clock: self.myId) ? 1.0 : 0.6)
.animation( self.model.amIRunning(clock: self.myId) ? anim1 : noAni )
.tapAction {
self.model.toggle(clock: self.myId)
}
.onAppear {
self.booted = true
}
}
}