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.