你这个点非常关键:“推荐 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 这些基础设施。