Reputation: 12179
Working with MVVM
in SwifUI. My aim is to have an enum
state property in the ViewModel
so the View
could adjust it self according to the state property. States could be: idle
, busy
, done
and error
. On done
I want to navigate to another screen using NavigationLink
, however the problem is that it is expecting a Binding<Bool>
and I could not figure out a way to map my enum state to bool.
Here is the simplified code:
struct LoginView: View {
@ObservedObject private var viewModel: LoginViewModel
@ViewBuilder
var body: some View {
...
// success state
NavigationLink(destination: HomeFactory().make(), isActive: self.$viewModel.state /* <---- some sort of mapping should come here */){ EmptyView() }
...
}
}
Hope that I am missing something really basic and it could be easily achieved in an elegant way.
EDIT:
Seems like it should be possible with the next method:
NavigationLink(destination: HomeFactory().make(), tag: .done, selection: self.$viewModel.viewState, label: { EmptyView() })
However I get an error and I can't figure out what is wrong: Cannot convert value of type 'Binding<ViewState>' to expected argument type 'Binding<_?>'
Here is the code:
final class LoginViewModel: ObservableObject {
@Published var viewState: ViewState = .idle
func begin() {
..
self.viewState = .done
..
}
}
struct LoginView: View {
@ObservedObject private var viewModel: LoginViewModel
@ViewBuilder
var body: some View {
..
NavigationLink(destination: HomeFactory().make(), tag: .done, selection: self.$viewModel.viewState, label: { EmptyView() })
..
}
UPDATE:
I was very close. The ViewState
in the vm should be optional:
@Published var viewState: ViewState? = .idle
Upvotes: 13
Views: 8996
Reputation: 32806
The problem with transformations is that most of them are uni-directional, and a binding requires that its content is both readable and writable. Thus even if you would extend your enum like
enum State {
case idle, busy, done
var isDone: Bool { self == .done }
}
you'd still be unable to bind to $viewModel.state.isDone
because the property is computed. And making it writable by adding a set
is not feasible as you'd not want to change the enum value via isDone
.
However, this doesn't mean that it can't be done. You can define map
over Binding
, and fool the system that you have a bidirectional communication:
extension Binding {
func map<NewValue>(_ transform: @escaping (Value) -> NewValue) -> Binding<NewValue> {
Binding<NewValue>(get: { transform(wrappedValue) }, set: { _ in })
}
}
, which you can use it in your navigation link, or wherever you need a boolean binding from the enum one.
NavigationLink(destination: HomeFactory().make(), isActive: $viewModel.state.isDone)
One caveat, though, is the fact that the mapped Binding
has a deceptive API, it says that it can also update values, when in fact it doesn't. However for most of the cases where booleans are used to configure availability, this should be fine. Careful, though to not circulate that Binding
outside the context, as other consumers of that instance might not be aware of its limitation.
Upvotes: 5
Reputation: 33
had the same problem. Here's what I did:
final class LoginViewModel: ObservableObject {
@Published var viewState: ViewState = .idle
func begin() {
..
self.viewState = .done
..
}
}
struct LoginView: View {
@ObservedObject private var viewModel: LoginViewModel
@ViewBuilder
var body: some View {
..
NavigationLink(destination: HomeFactory().make(), tag: .done, selection:
Binding<Bool>(get: { viewModel.viewState.isPresentable }, set: { _ in viewModel.viewState = .idle })), label: { EmptyView() })
..
}
Upvotes: 1
Reputation: 1041
There isn't an elegant way to map it in the view.
However, in your LoginViewModel
you can have an @Published variable that gets set when the state is updated.
Here is an example:
class LoginViewModel: ObservableObject {
@Published var shouldNavigate = false
var state: State = .idle {
didSet {
self.shouldNavigate = state == .done
}
}
}
Then change your NavigationLink
to:
NavigationLink(destination: HomeFactory().make(), isActive: self.$viewModel.shouldNavigate){ EmptyView() }
EDIT:
You can navigate based on state, or some other enum using a NavigationLink like this:
NavigationLink(destination: HomeFactory().make(), tag: State.done, selection: self.$state){ EmptyView() }
And update your vm state definition to:
@Published var state: State = .idle
Upvotes: 6