SwiftUI 动态添加子视图,但动画不起作用。

5
我希望创建一个在SwiftUI中的视图,它可以动态地添加子视图并带有动画效果。
struct ContentView : View {
    @State private var isButtonVisible = false

    var body: some View {
        VStack {
            Toggle(isOn: $isButtonVisible.animation()) {
                Text("add view button")
            }

           if isButtonVisible {
                 AnyView(DetailView())
                      .transition(.move(edge: .trailing))
                      .animation(Animation.linear(duration: 2))
             }else{
                    AnyView(Text("test"))
            }
        }
    }
}

上述代码在动画方面运行良好。然而,当我将视图选择部分移入函数时,动画不再工作(因为我希望动态添加不同的视图,因此我将逻辑放在一个函数中)。

struct ContentView : View {
    @State private var isButtonVisible = false
    var body: some View {
        VStack {
            Toggle(isOn: $isButtonVisible.animation()) {
                Text("add view button")
            }

            subView().transition(.move(edge: .trailing))
                     .animation(Animation.linear(duration: 2))   
    }

     func subView() -> some View {
         if isButtonVisible {
             return AnyView(DetailView())
         }else{
            return AnyView(Text("test"))
        }
    }
}

在我看来它们完全相同,但是我不明白为什么它们会有不同的结果。能否有人解释一下为什么?是否有更好的方案?非常感谢!


如果您要在ZStack中添加视图,则必须将.zIndex(1)添加到可动画的视图。 - koliush
3个回答

9

这是您的代码,经过修改后可以正常工作:

struct ContentView : View {
    @State private var isButtonVisible = false

    var body: some View {
        VStack {
            Toggle(isOn: $isButtonVisible.animation()) {
                Text("add view button")
            }

            subView()
                .transition(.move(edge: .trailing))
                .animation(Animation.linear(duration: 2))
        }
    }

    func subView() -> some View {
        Group {
            if isButtonVisible {
                DetailView()
            } else {
                Text("test")
            }
        }
    }
}

请注意以下两点:

  1. 你上面给出的两个例子不同,这就是为什么会得到不同的结果。第一个将过渡和动画应用于DetailView,然后使用AnyView对其进行类型抹消。第二个则使用AnyView将DetailView进行类型抹消,然后应用过渡和动画。
  2. 我更喜欢在Group视图中封装条件逻辑,而不是使用AnyView和类型抹消。然后返回的类型是Group,它将正常地执行动画。
  3. 如果您想要针对子视图的两种可能性进行不同的动画,现在可以直接将它们应用于DetailView()或Text("test")。

更新

Group方法只适用于if、elseif和else语句。如果您想使用switch语句,则必须在每个分支中包装AnyView()。但是,这会破坏过渡/动画效果。当前无法同时使用switch和设置自定义动画。


非常感谢您的帮助。您帮了我很多忙。我遇到了另一个问题。我无法在组内设置开关。我会收到错误消息“Closure containing control flow statement cannot be used with function builder 'ViewBuilder'”。func subView() -> some View { Group { switch status { case .start: Text("开始") break case .main: Text("主要") break case .unknown: Text("未知") break } } } - justicepenny
1
@justicepenny 请看我的更新。基本上,如果你想保留动画效果,就不能使用switch语句。请使用elseif语句,并向苹果提交反馈意见,如果你想在这里使用switch语句的话。 - John M.
是的,我必须使用if else语句。而且枚举必须带有原始值。无论如何,非常感谢您的帮助。 - justicepenny
为什么使用 AnyView 包装会破坏动画/过渡效果?是因为它无法确定视图是否已更改吗? - horseshoe7
谢谢!我一直在尝试使用switch,但是我的视图没有分组,所以在找到这个答案之前,我无法使转换效果起作用,非常沮丧! - Bruce Webster

3

我通过将返回一个 AnyView 的函数封装在一个 VStack 中,在使用 switch 语句时成功使其工作。我还必须为 AnyView 分配一个 .id,这样 SwiftUI 才能知道它何时更改。这是在 Xcode 11.3 和 iOS 13.3 上进行的。

struct EnumView: View {

    @ObservedObject var viewModel: ViewModel

    var body: some View {
        VStack {
            view(for: viewModel.viewState)
                .id(viewModel.viewState)
                .transition(.opacity)
        }
    }

    func view(for viewState: ViewModel.ViewState) -> AnyView {
        switch viewState {
        case .loading:
            return AnyView(LoadingStateView(viewModel: self.viewModel))
        case .dataLoaded:
            return AnyView(LoadedStateView(viewModel: self.viewModel))
        case let .error(error):
            return AnyView(ErrorView(error: error, viewModel: viewModel))
        }
    }

}

对于我的示例,我需要在ViewModel中使用withAnimation块封装viewState更改的内容。

withAnimation {
    self.viewState = .loading
}

2020年Xcode 11,这个解决方案有效。太好了,Id trick是最好的。感谢@MinnesotaGus。 - Ilya

1

我在想为什么 OP 使用返回 View 的函数而不是另一个 View 结构。 - malhal

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接