5

I would like to create a view in SwiftUI that add a subview dynamically and with animation.

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"))
            }
        }
    }
}

The above code works fine with the animation . however when i move the view selection part into a function, the animation is not working anymore (since i want to add different views dynamically, therefore, I put the logic in a function.)


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"))
        }
    }
}

it looks totally the same to me, however, i don't understand why they have different result. Could somebody explain me why? and any better solutions? thanks alot!

justicepenny
  • 2,004
  • 2
  • 24
  • 48

3 Answers3

9

Here's your code, modified so that it works:

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")
            }
        }
    }
}

Note two things:

  1. Your two examples above are different, which is why you get different results. The first applies a transition and animation to a DetailView, then type-erases it with AnyView. The second type-erases a DetailView with AnyView, then applies a transition and animation.
  2. Rather that using AnyView and type-erasure, I prefer to encapsulate the conditional logic inside of a Group view. Then the type you return is Group, which will animate properly.
  3. If you wanted different animations on the two possibilities for your subview, you can now apply them directly to DetailView() or Text("test").

Update

The Group method will only work with if, elseif, and else statements. If you want to use a switch, you will have to wrap each branch in AnyView(). However, this breaks transitions/animations. Using switch and setting custom animations is currently not possible.

John M.
  • 8,892
  • 4
  • 31
  • 42
  • Thank you so much for your help. You help me a lot. I get another problem. I could not set a switch inside the group. I will get the error "Closure containing control flow statement cannot be used with function builder 'ViewBuilder'" func subView() -> some View { Group { switch status { case .start: Text("start") break case .main: Text("main") break case .unknown: Text("unknown") break } } } – justicepenny Sep 30 '19 at 19:33
  • 1
    @justicepenny See my update. Basically, you can't, if you want to keep your animations. Use `elseif` statements, and [submit feedback to Apple](http://feedbackassistant.apple.com) if you would like to use `switch` here. – John M. Sep 30 '19 at 20:34
  • yea, I have to use if else statement. and the enum has to be with raw value. anyway Thanks a lot for your help – justicepenny Oct 01 '19 at 08:43
  • Why does wrapping in AnyView break animations/transitions? Is it because there's no way for it to establish if the view has changed? – horseshoe7 Nov 03 '21 at 08:07
  • Thank-you! I was getting very frustrated trying to use switch and I didn't have my Views in a Group, so I could not get transitions to work until I found this answer! – Bruce Webster May 06 '22 at 14:34
3

I was able to get it to work with a switch statement by wrapping the function that returns an AnyView in a VStack. I also had to give the AnyView an .id so SwiftUI can know when it changes. This is on Xcode 11.3 and 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))
        }
    }

}

Also for my example in the ViewModel I need to wrap the viewState changes in a withAnimation block

withAnimation {
    self.viewState = .loading
}
  • 2020 Xcode 11, this solution works. Bravo. Id trick is the best. Thank you, @MinnesotaGus. – Ilya Aug 30 '20 at 15:00
1

In iOS 14 they added the possibility to use if let and switch statements in function builders. Maybe it helps for your issues:

https://www.hackingwithswift.com/articles/221/whats-new-in-swiftui-for-ios-14 (at the article's bottom)

blackjacx
  • 9,011
  • 7
  • 45
  • 56
  • I was wondering why the OP was using a function that returns a View instead of another View struct – malhal Dec 13 '20 at 17:29