如何以动画方式删除使用 ForEach 循环创建的视图,并从 SwiftUI 中的 ObservableObject 获取数据

Mar*_*ner 6 foreach animation swift swiftui observableobject

该应用程序具有以下设置:

我的主视图使用 SwiftUI 循环创建标签云ForEach。从被调用的数组中获取ForEach数据。使用 a ,每三秒向数组添加一个新标签。通过添加标签,会再次触发并创建另一个. 一旦将三个以上的标签添加到数组中,就会从数组中删除第一个(最旧的)标签,并且 ForEach 会删除该特定的。@PublishedObservableObjectTagModelTimerObservableObjectForEachTagViewObservableObjectTagView

存在以下问题:

TagView 的创建非常完美。它还使用动画和 .onAppear 修饰符以应有的方式进行动画处理TagView。然而,当最旧的标签被删除时,它不会动画化其删除。中的代码.onDisappear会执行,但TagView会立即被删除。

我尝试了以下方法来解决该问题:

我尝试通过使用重复然后自动反转的动画来让整个出现和消失的动画TagView在内部运行。.onAppear它有点有效,但这样有两个问题。首先,如果动画时间太短,一旦动画完成并被TagView删除,将在没有应用任何修改器的情况下显示一小会儿,然后它将被删除。其次,如果我将动画持续时间设置得更长,则会TagView在动画完成之前将其删除。为了使其工作,我需要非常精确地计算动画的删除和持续时间,这将使动画TagView非常依赖Timer,这似乎不是一个好的解决方案。

我尝试的另一个解决方案是找到类似于self.presentationMode.wrappedValue.dismiss()使用@Environment(\.presentationMode)变量的东西,并以某种方式TagView.onAppear动画完成后将其自身删除。但这仅适用于在导航堆栈中创建视图并且我找不到任何其他方法来让视图自行销毁的情况。我还认为,一旦TagModel更新其数组,就会再次引起问题。

我阅读了其他几个 SO 解决方案,它们指向 ForEach 循环中的数据枚举。但我将每个 TagView 创建为自己的对象,我认为这不应该是问题,并且我不确定如果这是问题的一部分,我必须如何实现它。

这是我的应用程序的简化代码,可以在 iOS 单视图 SwiftUI 项目中运行。

import SwiftUI

struct ContentView: View {
    private let timer = Timer.publish(every: 3, on: .main, in: .common).autoconnect()
    @ObservedObject var tagModel = TagModel()
    
    var body: some View {
        ZStack {
            ForEach(tagModel.tags, id: \.self) { label in
                TagView(label: label)
            }
            .onReceive(timer) { _ in
                self.tagModel.addNextTag()
                if tagModel.tags.count > 3 {
                    self.tagModel.removeOldestTag()
                }
            }
        }
    }
}

class TagModel: ObservableObject {
    @Published var tags = [String]()
    
    func addNextTag() {
        tags.append(String( Date().timeIntervalSince1970 ))
    }
    func removeOldestTag() {
        tags.remove(at: 0)
    }
}

struct TagView: View {
    @State private var show: Bool = false
    @State private var position: CGPoint = CGPoint(x: Int.random(in: 50..<250), y: Int.random(in: 10..<25))
    @State private var offsetY: CGFloat = .zero
    
    let label: String
    
    var body: some View {
        let text = Text(label)
            .opacity(show ? 1.0 : 0.0)
            .scaleEffect(show ? 1.0 : 0.0)
            .animation(Animation.easeInOut(duration: 6))
            .position(position)
            .offset(y: offsetY)
            .animation(Animation.easeInOut(duration: 6))
            .onAppear() {
                show = true
                offsetY = 100
            }
            .onDisappear() {
                show = false
                offsetY = 0
            }
        return text
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}
Run Code Online (Sandbox Code Playgroud)

Asp*_*eri 8

目前尚不清楚您要尝试实现哪种效果,但在删除时,您应该动画化而不是查看内部结构,而是查看本身,即。在父级中,因为视图将其作为一个整体删除。

类似的东西(只是实验的方向):

var body: some View {
    ZStack {
        ForEach(tagModel.tags, id: \.self) { label in
            TagView(label: label)
                .transition(.move(edge: .leading))    // << here !! (maybe asymmetric needed)
        }
        .onReceive(timer) { _ in
            self.tagModel.addNextTag()
            if tagModel.tags.count > 3 {
                self.tagModel.removeOldestTag()
            }
        }
    }
    .animation(Animation.easeInOut(duration: 1))    // << here !! (parent animates subview removing)
Run Code Online (Sandbox Code Playgroud)

  • 您的代码确实帮助我得到了最终结果。我将 `.transition` 修饰符移到了 TagView 内部,因为这没有什么区别。将 `.animation()` 添加到 `ZStack` 正是我所需要的!谢谢你!:) 我仍在尝试理解一些 SwiftUI 概念,以确定修饰符到底属于哪里。 (2认同)