Administrator
发布于 2026-02-24 / 2 阅读
0
0

VIPER架构概览

你这个点非常关键:“推荐 struct”指的是 Service/依赖,而不是整个架构都要 struct

VIPER + DI + Router 在 SwiftUI 下,最稳的组合其实是:

Service 用 struct(依赖/能力)

State/Presenter 用 class(@Observable / @MainActor)

Interactor 可用 struct(纯业务)或 actor(需要并发隔离)

Router 多数用 class(持有导航状态/回调),也可以用 struct + Binding 注入

下面我给你一套AIChats 适配的 SwiftUI + 临时VIPER + DI模板(你可以直接照这个搭)。


1) 分层职责(推荐)

Entity(模型)

  • struct(Codable/Sendable)

Service(依赖:网络/Firebase/存储)

  • struct(无状态、易 Sendable、易 mock)

Interactor(用例/业务编排)

两种:

  • 简单纯业务:struct

  • 需要缓存/并发安全:actor(推荐,比 class 更干净)

Presenter(UI 状态源)

  • @MainActor @Observable final class

  • 这里就是你说的“需要 class + @Observable”的地方 ✅

Router(导航/路由状态)

  • SwiftUI 最终导航一定落在 NavigationStack(path:) 或 sheet/item

  • Router 通常是:

    • @MainActor @Observable final class(管理 path、sheet、alert)

    • 或 struct Router + Binding(更函数式)


2) 依赖容器(DI)怎么写:用 struct 最舒服

2.1 AppDependencies(全局依赖集合)

struct AppDependencies: Sendable {
  var authService: FirebaseAuthService
  var apiClient: APIClient
  // var logger: Logger

  static let live = AppDependencies(
    authService: .init(),
    apiClient: .init()
  )

  static let mock = AppDependencies(
    authService: .mock,
    apiClient: .mock
  )
}

这里全部用 struct:轻量、可替换、可 Sendable。


3) Router:让它成为“路由状态源”(class)

3.1 定义路由枚举

enum AppRoute: Hashable {
  case chat(avatarId: String)
  case category(CharacterOptions, imageName: String)
}

3.2 Router(@Observable class)

@MainActor
@Observable
final class AppRouter {
  var path: [AppRoute] = []
  var sheet: AppSheet?
  var alert: AnyAppAlert?

  func push(_ route: AppRoute) { path.append(route) }
  func pop() { _ = path.popLast() }
  func popToRoot() { path.removeAll() }
}

enum AppSheet: Identifiable {
  case createAccount

  var id: String { "\(self)" }
}


4) Presenter:class + @Observable(你要的地方)

以 “Explore” 为例(你已经写了 NavigationPathOption,可以直接替换成 Router)

@MainActor
@Observable
final class ExplorePresenter {
  private let interactor: ExploreInteractor
  private let router: AppRouter

  var featureAvatars: [AvatarModel] = []
  var categories: [CharacterOptions] = CharacterOptions.allCases
  var popularAvatars: [AvatarModel] = []

  init(interactor: ExploreInteractor, router: AppRouter) {
    self.interactor = interactor
    self.router = router
  }

  func onAppear() async {
    // 这里做数据加载
    featureAvatars = interactor.loadFeatured()
    popularAvatars = interactor.loadPopular()
  }

  func avatarTapped(_ avatar: AvatarModel) {
    router.push(.chat(avatarId: avatar.avatarId))
  }

  func categoryTapped(_ category: CharacterOptions, imageName: String) {
    router.push(.category(category, imageName: imageName))
  }
}


5) Interactor:推荐 struct 或 actor

struct ExploreInteractor: Sendable {
  let deps: AppDependencies

  func loadFeatured() -> [AvatarModel] { AvatarModel.mocks }
  func loadPopular() -> [AvatarModel] { AvatarModel.mocks }
}

如果未来要做缓存/并发安全,就把 Interactor 升级成 actor:

actor ExploreInteractor {
  let deps: AppDependencies
  init(deps: AppDependencies) { self.deps = deps }
}


6) View:只负责展示 + 触发 Presenter(不写业务)

struct ExploreView: View {
  @Environment(AppRouter.self) private var router
  @State private var presenter: ExplorePresenter

  init(deps: AppDependencies) {
    // Router 从 Environment 注入更好,但为了示例先从外部传一个
    let router = AppRouter()
    _presenter = State(initialValue: ExplorePresenter(
      interactor: ExploreInteractor(deps: deps),
      router: router
    ))
  }

  var body: some View {
    NavigationStack(path: Binding(
      get: { router.path },
      set: { router.path = $0 }
    )) {
      List {
        // 用 presenter.featureAvatars 等渲染
      }
      .navigationTitle("Explore")
      .navigationDestination(for: AppRoute.self) { route in
        switch route {
        case .chat(let avatarId):
          ChatView(avatarId: avatarId)
        case .category(let category, let imageName):
          CategoryListView(category: category, imageName: imageName)
        }
      }
      .task { await presenter.onAppear() }
    }
  }
}

你会发现:

View 不做业务,只触发 Presenter。

Presenter 管数据和路由。

Interactor 调 Service。

Service struct 纯能力。


7) ModuleFactory(DI 装配):把一切组起来

这是你 VIPER 最舒服的入口:

enum ExploreModuleFactory {
  @MainActor
  static func make(deps: AppDependencies, router: AppRouter) -> ExploreView {
    let interactor = ExploreInteractor(deps: deps)
    let presenter = ExplorePresenter(interactor: interactor, router: router)
    return ExploreView(presenter: presenter) // 你需要给 ExploreView 写一个 init(presenter:)
  }
}


8) 你真正该怎么理解 “推荐 struct”?

你现在纠结的点是:

框架需要 class + @Observable,那我是不是就不能“推荐 struct”?

不是。

“推荐 struct”说的是:

  • 依赖(Service/Client/Config)尽量 struct

  • 状态源(Presenter/Store/Router)必须 class/actor

VIPER/Clean 的现代 Swift 版本就是这样落地的。


我建议你在 AIChats 里立一条硬规则

✅ Rule 1:任何 “状态源” 只能是:

  • @Observable final class

  • 或 actor

✅ Rule 2:任何 “纯依赖/能力” 都是:

  • struct(最好 Sendable)

✅ Rule 3:View 不写业务,只发事件:

  • presenter.didTapX()

  • await presenter.refresh()


如果你愿意,我可以按你现在的“临时VIPER架构”目录,把 AIChats 的一个模块(比如 Explore 或 Settings/Auth)完整拆出来:

  • Entity

  • Interactor (with deps)

  • Presenter (@Observable)

  • Router (AppRouter)

  • ModuleFactory (DI)

并且适配你现有的 .anyButton / .showCustomAlert / .appModal 这些基础设施。


评论