防止 SwiftUI 动画覆盖嵌套的 withAnimation 块
Prevent SwiftUI animation from overriding nested withAnimation block
我有一个带有可拖动组件(“弹性”文本)的屏幕,当释放时 spring 返回并带有非常突出的 spring 动画。
有一些文本是异步输入的(在这个例子中,使用 Timer
),我希望它能很好地淡入淡出,同时 Springy 标签平滑地向上移动以为它腾出空间。
我向 VStack 添加了一个动画,当异步加载的文本淡入时,它会按我想要的方式转换。但是,这会破坏“弹性”文本上的 spring 动画。
这里有一个动画被注释掉了,如果将 1 秒动画切换到那个位置,异步加载的文本会淡入,但就在动画开始时,“弹性”文本会跳起来而不是滑动向上。
如何让两个动画都按我希望的方式工作?
import SwiftUI
import Combine
class Store: ObservableObject {
@Published var showSection: Bool = false
private var cancellables: [AnyCancellable] = []
init() {
Timer
.publish(every: 2, on: RunLoop.main, in: .common)
.autoconnect()
.map { _ in true}
.assign(to: \.showSection,
on: self)
.store(in: &cancellables)
}
}
struct ContentView: View {
@ObservedObject var store = Store()
@State var draggedState = CGSize.zero
var body: some View {
VStack {
Spacer()
VStack(alignment: .leading) {
VStack(spacing: 4) {
HStack {
Text("Springy")
.font(.title)
Image(systemName: "hand.point.up.left.fill")
.imageScale(.large)
}
.offset(x: draggedState.width, y: draggedState.height)
.foregroundColor(.secondary)
.gesture(
DragGesture().onChanged { value in
draggedState = value.translation
}
.onEnded { value in
// How can this animation be fast without the other animation slowing it down?
withAnimation(Animation
.interactiveSpring(response: 0.3,
dampingFraction: 0.1,
blendDuration: 0)) {
draggedState = .zero
}
}
)
VStack {
if store.showSection {
Text("Something that animates in after loading asynchrously.")
.font(.body)
.padding()
.transition(.opacity)
}
}
// When the animation is here, it doesn't cancel out the spring animation, but the Springy text jumps up at the beginning of the animation instead of animating.
// .animation(.easeInOut(duration: 1))
}
// Animation here has desired effect for loading, but overrides the Springy release animation.
.animation(.easeInOut(duration: 1))
}
Spacer()
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
.preferredColorScheme(.dark)
}
}
扩展问题,基于 Asperi 的回答。
现在,假设我在主 VStack
中有多个元素,我想在它们进入时设置动画。
- 为我要设置动画的每个部分添加一个带有
value
的单独 .animation
修饰符是否是一个好的解决方案?
在下面的扩展示例中,这是带有以下内容的部分:
.animation(.easeInOut(duration: 1),
value: store.showSection)
.animation(.easeInOut(duration: 1),
value: store.somePossibleText)
.animation(.easeInOut(duration: 1),
value: store.moreInformation)
- 我在这里使用了 3 种方法来确定是否显示一个部分。为简单起见,我在初始示例中使用了
Bool
,但在我的真实场景中,我认为使用 String?
方法而不是设置单独的 Bool
或检查空字符串。在动画方面使用它有什么缺点吗?鉴于 Asperi 的解决方案,它似乎在这个示例中运行良好。
这里是经过新修改的完整示例:
import SwiftUI
import Combine
class Store: ObservableObject {
@Published var showSection: Bool = false
@Published var somePossibleText: String = ""
@Published var moreInformation: String?
private var cancellables: [AnyCancellable] = []
init() {
Timer
.publish(every: 2, on: RunLoop.main, in: .common)
.autoconnect()
.map { _ in true }
.assign(to: \.showSection,
on: self)
.store(in: &cancellables)
Timer
.publish(every: 3, on: RunLoop.main, in: .common)
.autoconnect()
.map { _ in "Something else loaded later" }
.assign(to: \.somePossibleText,
on: self)
.store(in: &cancellables)
Timer
.publish(every: 4, on: RunLoop.main, in: .common)
.autoconnect()
.map { _ in "More stuff loaded later" }
.assign(to: \.moreInformation,
on: self)
.store(in: &cancellables)
}
}
struct ContentView: View {
@ObservedObject var store = Store()
@State var draggedState = CGSize.zero
var body: some View {
VStack {
Spacer()
VStack(alignment: .leading) {
VStack(spacing: 4) {
HStack {
Text("Springy")
.font(.title)
Image(systemName: "hand.point.up.left.fill")
.imageScale(.large)
}
.offset(x: draggedState.width,
y: draggedState.height)
.foregroundColor(.secondary)
.gesture(
DragGesture().onChanged { value in
draggedState = value.translation
}
.onEnded { value in
// TODO: how can this animation be fast without the other one slowing it down?
withAnimation(Animation
.interactiveSpring(response: 0.3,
dampingFraction: 0.1,
blendDuration: 0)) {
draggedState = .zero
}
}
)
if store.showSection {
Text("Something that animates in after loading asynchrously.")
.font(.body)
.padding()
.transition(.opacity)
}
if store.somePossibleText != "" {
Text(store.somePossibleText)
.font(.footnote)
.padding()
.transition(.opacity)
}
if let moreInformation = store.moreInformation {
Text(moreInformation)
.font(.footnote)
.padding()
.transition(.opacity)
}
}
.animation(.easeInOut(duration: 1),
value: store.showSection)
.animation(.easeInOut(duration: 1),
value: store.somePossibleText)
.animation(.easeInOut(duration: 1),
value: store.moreInformation)
}
Spacer()
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
.preferredColorScheme(.dark)
}
}
如果我正确理解了您的期望,将最后一个动画绑定到 showSection
值就足够了,如下所示
}
// Animation here has desired effect for loading, but overrides the Springy release animation.
.animation(.easeInOut(duration: 1), value: store.showSection)
测试 Xcode 12 / iOS 14.
我有一个带有可拖动组件(“弹性”文本)的屏幕,当释放时 spring 返回并带有非常突出的 spring 动画。
有一些文本是异步输入的(在这个例子中,使用 Timer
),我希望它能很好地淡入淡出,同时 Springy 标签平滑地向上移动以为它腾出空间。
我向 VStack 添加了一个动画,当异步加载的文本淡入时,它会按我想要的方式转换。但是,这会破坏“弹性”文本上的 spring 动画。
这里有一个动画被注释掉了,如果将 1 秒动画切换到那个位置,异步加载的文本会淡入,但就在动画开始时,“弹性”文本会跳起来而不是滑动向上。
如何让两个动画都按我希望的方式工作?
import SwiftUI
import Combine
class Store: ObservableObject {
@Published var showSection: Bool = false
private var cancellables: [AnyCancellable] = []
init() {
Timer
.publish(every: 2, on: RunLoop.main, in: .common)
.autoconnect()
.map { _ in true}
.assign(to: \.showSection,
on: self)
.store(in: &cancellables)
}
}
struct ContentView: View {
@ObservedObject var store = Store()
@State var draggedState = CGSize.zero
var body: some View {
VStack {
Spacer()
VStack(alignment: .leading) {
VStack(spacing: 4) {
HStack {
Text("Springy")
.font(.title)
Image(systemName: "hand.point.up.left.fill")
.imageScale(.large)
}
.offset(x: draggedState.width, y: draggedState.height)
.foregroundColor(.secondary)
.gesture(
DragGesture().onChanged { value in
draggedState = value.translation
}
.onEnded { value in
// How can this animation be fast without the other animation slowing it down?
withAnimation(Animation
.interactiveSpring(response: 0.3,
dampingFraction: 0.1,
blendDuration: 0)) {
draggedState = .zero
}
}
)
VStack {
if store.showSection {
Text("Something that animates in after loading asynchrously.")
.font(.body)
.padding()
.transition(.opacity)
}
}
// When the animation is here, it doesn't cancel out the spring animation, but the Springy text jumps up at the beginning of the animation instead of animating.
// .animation(.easeInOut(duration: 1))
}
// Animation here has desired effect for loading, but overrides the Springy release animation.
.animation(.easeInOut(duration: 1))
}
Spacer()
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
.preferredColorScheme(.dark)
}
}
扩展问题,基于 Asperi 的回答。
现在,假设我在主 VStack
中有多个元素,我想在它们进入时设置动画。
- 为我要设置动画的每个部分添加一个带有
value
的单独.animation
修饰符是否是一个好的解决方案?
在下面的扩展示例中,这是带有以下内容的部分:
.animation(.easeInOut(duration: 1),
value: store.showSection)
.animation(.easeInOut(duration: 1),
value: store.somePossibleText)
.animation(.easeInOut(duration: 1),
value: store.moreInformation)
- 我在这里使用了 3 种方法来确定是否显示一个部分。为简单起见,我在初始示例中使用了
Bool
,但在我的真实场景中,我认为使用String?
方法而不是设置单独的Bool
或检查空字符串。在动画方面使用它有什么缺点吗?鉴于 Asperi 的解决方案,它似乎在这个示例中运行良好。
这里是经过新修改的完整示例:
import SwiftUI
import Combine
class Store: ObservableObject {
@Published var showSection: Bool = false
@Published var somePossibleText: String = ""
@Published var moreInformation: String?
private var cancellables: [AnyCancellable] = []
init() {
Timer
.publish(every: 2, on: RunLoop.main, in: .common)
.autoconnect()
.map { _ in true }
.assign(to: \.showSection,
on: self)
.store(in: &cancellables)
Timer
.publish(every: 3, on: RunLoop.main, in: .common)
.autoconnect()
.map { _ in "Something else loaded later" }
.assign(to: \.somePossibleText,
on: self)
.store(in: &cancellables)
Timer
.publish(every: 4, on: RunLoop.main, in: .common)
.autoconnect()
.map { _ in "More stuff loaded later" }
.assign(to: \.moreInformation,
on: self)
.store(in: &cancellables)
}
}
struct ContentView: View {
@ObservedObject var store = Store()
@State var draggedState = CGSize.zero
var body: some View {
VStack {
Spacer()
VStack(alignment: .leading) {
VStack(spacing: 4) {
HStack {
Text("Springy")
.font(.title)
Image(systemName: "hand.point.up.left.fill")
.imageScale(.large)
}
.offset(x: draggedState.width,
y: draggedState.height)
.foregroundColor(.secondary)
.gesture(
DragGesture().onChanged { value in
draggedState = value.translation
}
.onEnded { value in
// TODO: how can this animation be fast without the other one slowing it down?
withAnimation(Animation
.interactiveSpring(response: 0.3,
dampingFraction: 0.1,
blendDuration: 0)) {
draggedState = .zero
}
}
)
if store.showSection {
Text("Something that animates in after loading asynchrously.")
.font(.body)
.padding()
.transition(.opacity)
}
if store.somePossibleText != "" {
Text(store.somePossibleText)
.font(.footnote)
.padding()
.transition(.opacity)
}
if let moreInformation = store.moreInformation {
Text(moreInformation)
.font(.footnote)
.padding()
.transition(.opacity)
}
}
.animation(.easeInOut(duration: 1),
value: store.showSection)
.animation(.easeInOut(duration: 1),
value: store.somePossibleText)
.animation(.easeInOut(duration: 1),
value: store.moreInformation)
}
Spacer()
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
.preferredColorScheme(.dark)
}
}
如果我正确理解了您的期望,将最后一个动画绑定到 showSection
值就足够了,如下所示
}
// Animation here has desired effect for loading, but overrides the Springy release animation.
.animation(.easeInOut(duration: 1), value: store.showSection)
测试 Xcode 12 / iOS 14.