是否可以将一个枚举映射到SwiftUI中的Binding <Bool>?

时间:2019-10-28 08:38:53

标签: ios swift swiftui

在SwifUI中使用MVVM。我的目标是在enum中拥有一个ViewModel状态属性,以便View可以根据状态属性自行进行调整。状态可以是:idlebusydoneerror。在done上,我想使用NavigationLink导航到另一个屏幕,但是问题是它正在期待Binding<Bool>,因此我想不出一种将枚举状态映射为bool的方法。

这是简化的代码:

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() }
        ...
    }
}

希望我确实缺少一些基本知识,并且可以轻松地以优雅的方式实现。

编辑:

似乎可以使用下一个方法来实现:
NavigationLink(destination: HomeFactory().make(), tag: .done, selection: self.$viewModel.viewState, label: { EmptyView() })

但是我收到一个错误,但我无法弄清楚出什么问题了:Cannot convert value of type 'Binding<ViewState>' to expected argument type 'Binding<_?>'

代码如下:

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() })
    ..
  }

更新:

我非常亲密。 vm中的ViewState应该是可选的:
@Published var viewState: ViewState? = .idle

3 个答案:

答案 0 :(得分:1)

没有一种优雅的方法可以在视图中进行映射。
但是,在您的LoginViewModel中,您可以拥有一个@Published变量,该变量在状态更新时会被设置。

这里是一个例子:

class LoginViewModel: ObservableObject {
    @Published var shouldNavigate = false  

    var state: State = .idle {
        didSet {
            self.shouldNavigate = state == .done
        }
    }
}

然后将您的NavigationLink更改为:

NavigationLink(destination: HomeFactory().make(), isActive: self.$viewModel.shouldNavigate){ EmptyView() }

编辑:
您可以使用导航链接根据状态或其他枚举进行导航,如下所示:

NavigationLink(destination: HomeFactory().make(), tag: State.done, selection: self.$state){ EmptyView() }

并将您的虚拟机状态定义更新为:
@Published var state: State = .idle

答案 1 :(得分:0)

有同样的问题。这是我所做的:

    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() })
        ..
      }

答案 2 :(得分:0)

转换的问题在于它们中的大多数是单向的,并且绑定要求其内容既可读又可写。因此,即使您像

一样扩展您的枚举
enum State {
  case idle, busy, done

  var isDone: Bool { self == .done }
}

您仍然无法绑定到 $viewModel.state.isDone,因为该属性是计算出来的。通过添加 set 使其可写是不可行的,因为您不想通过 isDone 更改枚举值。

然而,这并不意味着它不能完成。您可以在 map 上定义 Binding,并欺骗系统您有双向通信:

extension Binding {
    func map<NewValue>(_ transform: @escaping (Value) -> NewValue) -> Binding<NewValue> {
        Binding<NewValue>(get: { transform(wrappedValue) }, set: { _ in })
    }
}

,您可以在导航链接中使用它,或者在任何需要枚举中的布尔绑定的地方使用它。

NavigationLink(destination: HomeFactory().make(), isActive: $viewModel.state.isDone)

不过,需要注意的是,映射的 Binding 具有欺骗性的 API,它说它也可以更新值,而实际上它不能。然而,对于大多数使用布尔值来配置可用性的情况,这应该没问题。小心,但不要在上下文之外传播该 Binding,因为该实例的其他使用者可能不知道其限制。