如何创建一个 NavigationManager 来更改视图模型中的选项卡?

How to create a NavigationManager that would change tab within view models?

我有一个 NavigationManager 来处理更改 SwiftUI 选项卡栏选择。

如果在我的 SwiftUI 视图中将其设置为 @EnvironmentObject,它会起作用,但在我的视图模型中将 NavigationManager 作为服务调用时则不起作用。问题是我想使用一个更简单的解决方案,而不是四处传递 @EnvironmentObject var navigationManager 并将它们传递到视图模型初始化程序中,因为我有很多,我正在寻找一种更简洁的方法。

如何使用我的 NavigationManager 从我的视图模型中更改选项卡而不在 init() 中传递它?

import SwiftUI

struct ContentView: View {

  @StateObject var navigationManager = NavigationManager()

  var body: some View {
    TabView(selection: $navigationManager.selection) {
      AccountView()
        .tabItem {
          Text("Account")
          Image(systemName: "person.crop.circle") }
        .tag(NavigationItem.account)

      SettingsView()
          .tabItem {
            Text("Settings")
            Image(systemName: "gear") }
          .tag(NavigationItem.settings)
          .environmentObject(navigationManager)
    }
  }
}

我想在视图模型中使用的导航管理器。

class NavigationManager: ObservableObject {

  @Published var selection: NavigationItem = .account
}

enum NavigationItem {
  case account
  case settings
}

我的 AccountViewModel 和设置视图模型:

class AccountViewModel: ObservableObject {

  let navigationManager = NavigationManager()
}

struct AccountView: View {

  @StateObject var viewModel = AccountViewModel()

  var body: some View {
    VStack(spacing: 16) {
    Text("AccountView")
        .font(.title3)

      Button(action: {
        viewModel.navigationManager.selection = .settings
      }) {
        Text("Go to Settings tab")
          .font(.headline)
      }
      .buttonStyle(.borderedProminent)
    }
  }
}


class SettingsViewModel: ObservableObject {

  let navigationManager = NavigationManager()
}

struct SettingsView: View {

  @EnvironmentObject var navigationManager: NavigationManager

  @StateObject var viewModel = SettingsViewModel()

  var body: some View {
    VStack(spacing: 16) {
    Text("SettingsView")
        .font(.title3)

      Button(action: {
        navigationManager.selection = .account
      }) {
        Text("Go to Account tab")
          .font(.headline)
      }
      .buttonStyle(.borderedProminent)
    }
  }
}

我成功地使用 属性 包装器将我的 navigationManager 作为共享依赖项注入,并使用 Combine 更改其 selection 变量。

所以我创建了一个协议,将 NavigationManager 包装到 属性 包装器 @Injection 并使其值成为 CurrentValueSubject

import Combine

final class NavigationManager: NavigationManagerProtocol, ObservableObject {

  var selection = CurrentValueSubject<NavigationItem, Never>(.settings)
}

protocol NavigationManagerProtocol {
  var selection: CurrentValueSubject<NavigationItem, Never> { get set }
}

这是 @Injection 属性 包装器,用于在 .swift 个文件之间传递我的 NavigationManager 实例。

@propertyWrapper
struct Injection<T> {

  private let keyPath: WritableKeyPath<InjectedDependency, T>

  var wrappedValue: T {
    get { InjectedDependency[keyPath] }
    set { InjectedDependency[keyPath] = newValue }
  }

  init(_ keyPath: WritableKeyPath<InjectedDependency, T>) {
    self.keyPath = keyPath
  }
}

protocol InjectedKeyProtocol {
  associatedtype Value
  static var currentValue: Self.Value { get set }
}

struct InjectedDependency {

  private static var current = InjectedDependency()

  static subscript<K>(key: K.Type) -> K.Value where K: InjectedKeyProtocol {
    get { key.currentValue }
    set { key.currentValue = newValue }
  }

  static subscript<T>(_ keyPath: WritableKeyPath<InjectedDependency, T>) -> T {
    get { current[keyPath: keyPath] }
    set { current[keyPath: keyPath] = newValue }
  }
}

extension InjectedDependency {

  var navigationManager: NavigationManagerProtocol {
    get { Self[NavigationManagerKey.self] }
    set { Self[NavigationManagerKey.self] = newValue }
  }
}

private struct NavigationManagerKey: InjectedKeyProtocol {
  static var currentValue: NavigationManagerProtocol = NavigationManager()
}

有了这个,我可以在我的视图模型之间传递我的 NavigationManager 并在点击按钮时使用 Combine 发送新值:

class AccountViewModel: ObservableObject {

  @Injection(\.navigationManager) var navigationManager
}

struct AccountView: View {

  var viewModel = AccountViewModel()

  var body: some View {
    VStack(spacing: 16) {
    Text("AccountView")
        .font(.title3)

      Button(action: {
        viewModel.navigationManager.selection.send(.settings)
      }) {
        Text("Go to Settings tab")
          .font(.headline)
      }
      .buttonStyle(.borderedProminent)
    }
  }
}


class SettingsViewModel: ObservableObject {

  @Injection(\.navigationManager) var navigationManager
}

struct SettingsView: View {

  @StateObject var viewModel = SettingsViewModel()

  var body: some View {
    VStack(spacing: 16) {
    Text("SettingsView")
        .font(.title3)

      Button(action: {
        viewModel.navigationManager.selection.send(.account)
      }) {
        Text("Go to Account tab")
          .font(.headline)
      }
      .buttonStyle(.borderedProminent)
    }
  }
} 

最后,我在我的 ContentView 中注入 NavigationManager 并使用 .onReceive(_:action:) 修饰符从代码中的任何位置跟踪新选择的选项卡。

struct ContentView: View {

  @Injection(\.navigationManager) var navigationManager

  @State var selection: NavigationItem = .account

  var body: some View {
    TabView(selection: $selection) {
      AccountView()
        .tabItem {
          Text("Account")
          Image(systemName: "person.crop.circle") }
        .tag(NavigationItem.account)

      SettingsView()
          .tabItem {
            Text("Settings")
            Image(systemName: "gear") }
          .tag(NavigationItem.settings)
    }
    .onReceive(navigationManager.selection) { newValue in
      selection = newValue
    }
  }
}