You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在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()
}

关键说明

  1. 链式动画实现:通过元素的intValue计算延迟时间,每个元素延迟0.1秒入场,形成依次滑入的连贯效果
  2. 移除动画优化:使用withAnimation包裹数组移除操作,结合移除转场动画,最后一个元素先滑出,其余元素自动依次跟进
  3. 绑定修复:直接使用ForEach($arrayOfCustomType)获取元素绑定,避免手动索引导致的错位问题
  4. 动画触发保障:通过animationTrigger的UUID值变化,确保每次数组更新时动画都能重新计算并触发

内容的提问来源于stack exchange,提问作者swiftPunk

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.16 18:33:15