如何在SwiftUI中实现数组元素数量的链式动画?
SwiftUI实现TextEditor链式动画解决方案
需求描述
我需要在SwiftUI中实现数组元素数量变化时的链式动画:
- 添加元素:第一个TextEditor先入场,后续组件依次延迟从左侧滑入
- 移除元素:最后一个TextEditor先从右侧滑出,其余组件依次延迟执行
我编写了初始测试代码和更新后的版本,但均未达到预期效果,附上相关代码及目标动画示例,寻求可行实现方案。
初始测试代码
import SwiftUI struct ContentView: View { @State private var arrayOfCustomType: [MyCustomType] = [MyCustomType]() var body: some View { VStack { if (!arrayOfCustomType.isEmpty) { ScrollView { AnimatableForEach(arrayOfCustomType: $arrayOfCustomType) } .scrollIndicators(.visible) } else { Text("Please run the function!") } } .frame(height: 500.0) .padding() .animation(.easeInOut(duration: 1), value: CGFloat(arrayOfCustomType.count)) Button("run") { if (!arrayOfCustomType.isEmpty) { arrayOfCustomType.removeAll() } else { arrayOfCustomType = myFunc(string: myString, separatedBy: " ") } } .padding() } } func myFunc(string: String, separatedBy: String) -> [MyCustomType] { let stringComponents: [String] = string.components(separatedBy: separatedBy) var arrayOfCustomType: [MyCustomType] = [MyCustomType]() for index in stringComponents.indices { arrayOfCustomType.append(MyCustomType(intValue: index, stringValue: stringComponents[index])) } return arrayOfCustomType } let myString: String = """ Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque porro quisquam est, qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit. """ struct AnimatableForEach: View, Animatable { @Binding var arrayOfCustomType: [MyCustomType] var animatableData: CGFloat { get { return CGFloat(arrayOfCustomType.count) } set { } } var body: some View { ForEach(arrayOfCustomType) { element in TextEditor(text: Binding( get: { arrayOfCustomType[element.intValue].stringValue }, set: { newValue in arrayOfCustomType[element.intValue].stringValue = newValue } )) .font(.title) .fontDesign(.monospaced) .cornerRadius(5.0) .padding(.trailing) .transition(.asymmetric(insertion: .move(edge: .leading), removal: .move(edge: .trailing))) } } } struct MyCustomType: Identifiable, Animatable { let id: UUID = UUID() var intValue: Int var stringValue: String var animatableData: CGFloat { get { CGFloat(intValue) } set { intValue = Int(newValue) } } } extension Array { var animatableData: CGFloat { get { return CGFloat(self.count) } set { } } }
更新后的代码
import SwiftUI struct ContentView: View { @State private var arrayOfCustomType: [MyCustomType] = [MyCustomType]() var body: some View { VStack { if (!arrayOfCustomType.isEmpty) { ScrollView { AnimatableForEach(arrayOfCustomType: $arrayOfCustomType) } .scrollIndicators(.visible) } else { Text("Please run the function!") } } .frame(height: 500.0) .padding() .animation(.easeInOut(duration: 1), value: CGFloat(arrayOfCustomType.count)) Button("run") { if (!arrayOfCustomType.isEmpty) { animationFunction(from: CGFloat(arrayOfCustomType.count), to: .zero, animation: .easeInOut(duration: 1), animatableData: { value in if ((arrayOfCustomType.count - Int(value)) >= 0) { arrayOfCustomType.removeLast() } }) } else { let myArray = myFunc(string: myString, separatedBy: " ") animationFunction(from: CGFloat(arrayOfCustomType.count), to: .zero, animation: .easeInOut(duration: 1), animatableData: { value in if ((arrayOfCustomType.count - Int(value)) >= 0) { arrayOfCustomType.append(myArray[Int(value)]) } }) } } .padding() } } func myFunc(string: String, separatedBy: String) -> [MyCustomType] { let stringComponents: [String] = string.components(separatedBy: separatedBy) var arrayOfCustomType: [MyCustomType] = [MyCustomType]() for index in stringComponents.indices { arrayOfCustomType.append(MyCustomType(intValue: index, stringValue: stringComponents[index])) } return arrayOfCustomType } let myString: String = """ Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque porro quisquam est, qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit. """ struct AnimatableForEach: View { @Binding var arrayOfCustomType: [MyCustomType] var body: some View { ForEach(arrayOfCustomType) { element in TextEditor(text: Binding( get: { arrayOfCustomType[element.intValue].stringValue }, set: { newValue in arrayOfCustomType[element.intValue].stringValue = newValue } )) .font(.title) .fontDesign(.monospaced) .cornerRadius(5.0) .padding(.trailing) .transition(.asymmetric(insertion: .move(edge: .leading).combined(with: .opacity), removal: .move(edge: .trailing).combined(with: .opacity))) } } } struct MyCustomType: Identifiable { let id: UUID = UUID() var intValue: Int var stringValue: String } func animationFunction(from: CGFloat, to: CGFloat, animation: Animation, animatableData: (CGFloat) -> Void ) { // sending animatableData from to to view ... animatableData(0.25) }
目标动画效果


可行实现方案
核心思路是利用元素索引计算延迟时间,结合transition和animation修饰符,让每个元素的动画按顺序触发。
完整实现代码
import SwiftUI struct ContentView: View { @State private var arrayOfCustomType: [MyCustomType] = [] @State private var animationTrigger = UUID() var body: some View { VStack { if !arrayOfCustomType.isEmpty { ScrollView { LazyVStack(spacing: 8) { ForEach($arrayOfCustomType) { $element in TextEditor(text: $element.stringValue) .font(.title) .fontDesign(.monospaced) .cornerRadius(5.0) .padding(.trailing) .frame(height: 80) .transition(.asymmetric( insertion: .move(edge: .leading).combined(with: .opacity), removal: .move(edge: .trailing).combined(with: .opacity) )) .animation(.easeInOut(duration: 0.5).delay(Double(element.intValue) * 0.1), value: animationTrigger) } } } .scrollIndicators(.visible) } else { Text("点击按钮触发动画!") .font(.title) } } .frame(height: 500.0) .padding() Button("执行") { withAnimation { if !arrayOfCustomType.isEmpty { arrayOfCustomType.removeAll() } else { arrayOfCustomType = myFunc(string: myString, separatedBy: " ") } animationTrigger = UUID() } } .padding() .buttonStyle(.borderedProminent) } } func myFunc(string: String, separatedBy: String) -> [MyCustomType] { let stringComponents = string.components(separatedBy: separatedBy) return stringComponents.enumerated().map { index, str in MyCustomType(intValue: index, stringValue: str) } } let myString: String = """ Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque porro quisquam est, qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit. """ struct MyCustomType: Identifiable { let id: UUID = UUID() var intValue: Int var stringValue: String } #Preview { ContentView() }
关键说明
- 链式动画实现:通过元素的
intValue计算延迟时间,每个元素延迟0.1秒入场,形成依次滑入的连贯效果 - 移除动画优化:使用
withAnimation包裹数组移除操作,结合移除转场动画,最后一个元素先滑出,其余元素自动依次跟进 - 绑定修复:直接使用
ForEach($arrayOfCustomType)获取元素绑定,避免手动索引导致的错位问题 - 动画触发保障:通过
animationTrigger的UUID值变化,确保每次数组更新时动画都能重新计算并触发
内容的提问来源于stack exchange,提问作者swiftPunk
相关产品推荐
相关产品推荐

