简体   繁体   中英

Why doesn't custom SwiftUI view animate on ViewModel state change?

I am presenting a custom view when my viewModel changes to an error state. I want to animate this presentation. I represent the viewModel state using an enum

enum ViewModelState<T> {
    case idle
    case error(Error)
    case loading
    case data(T)

    var isError: Bool {
        switch self {
        case .error:
            return true
        default:
           return false
        }
    }
}

In my viewModel I have this property...

@Published var state: ViewModelState =.idle

When it changes to an error state I want to animate in my error view. When I do the following with an if statement in my view it animates in...

private var content: some View {
    return ZStack {
        mainView // VStack
        if case let .error(error) = viewModel.state {
            ErrorView(
                errorDescription: error.localizedDescription,
                state: $viewModel.state
            )
        }
    }.animation(.default)
}

var body: some View {
    content
    .navigationBarTitle(viewModel.title)
    .navigationBarBackButtonHidden(true)
}

However I want to switch on the error and do this

private var content: some View {
    switch viewModel.state {
    case .idle:
        return mainView.eraseToAnyView()
    case let .error(error):
        return ZStack {
            mainView
            ErrorView(
               errorDescription: error.localizedDescription,
               state: $viewModel.state
            )
        }.animation(.default).eraseToAnyView()
    default:
        return EmptyView().eraseToAnyView()
    }
} 

I don't understand why this approach doesn't animate - is there a way to animate views without resorting to multiple properties in my view model to represent state/computed properties off my state enum or is this the best approach?

AnyView hides all implementation details from the compiler. That means that SiwftUI can't animate, because it tries to infer all animations during compilation.

That's why you should try to avoid AnyView whenever possible.

If you want to seperate some logic of your body into a computed property (or function), use the @ViewBuilder annotation.

@ViewBuilder
private var content: some View {
    switch viewModel.state {
    case .idle:
       mainView
    case let .error(error):
       ZStack {
            mainView
            ErrorView(
               errorDescription: error.localizedDescription,
               state: $viewModel.state
            )
        }.animation(.default)
    default:
       EmptyView()
    }
} 

Notice that I removed return in the property. The @ViewBuilder behaves much like the default body property by wrapping everything in a Group .


The second problem are the states between you animate. By using the switch you are creating an SwiftUI._ConditionalContent<MainView,SwiftUI.ZStack<SwiftUI.TupleView<(MainView, ErrorView)>>

That is a little hard to parse, but you creating a ConditionalContent that switches between just the MainView and an ZStack consisting of your MainView and the ErrorView , which is not what we want.

We should try to create a ZStack with out MainView and an optional ErrorView .

What I like to to is to extend by view models with a computed error: Error? property.

extension ViewModel {
    var error: Error? {
        guard case let .error(error) = state else { 
              return nil
        }
        return error
   }
}

Now, you can create an optional view, by using the map function of the Optional type

ZStack {
    mainView
    viewModel.error.map { unwrapped in
         ErrorView(
             errorDescription: unwrapped.localizedDescription,
             state: $viewModel.state
         )
    }
}

That should work as expected.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM