一个基于 UIKit + Swift Package Manager 的 iOS 模块化架构示例项目。
项目通过统一路由层 AppRouting 和门面层 AppModuleFacade 组织各业务模块,支持 Tab 场景、模块内路由分发以及 DeepLink 映射。
iOSArchitecture
├── iOSArchitecture.xcodeproj # Xcode 工程
├── iOSArchitecture/ # App 壳工程(UIApplication/Scene)
│ ├── AppDelegate.swift
│ ├── SceneDelegate.swift # 组装模块、注入依赖、启动导航
│ └── AppDeepLinkMiddleware.swift # DeepLink 白名单 + 路由表映射
├── Packages/ # 业务与基础能力模块(SwiftPM)
│ ├── AppRouting/ # 路由协议、导航样式、Coordinator/Router 抽象
│ ├── AppModuleFacade/ # 模块门面层(统一暴露路由与模块协议)
│ ├── HomeModule/ # Home 业务模块(Tab + Detail)
│ ├── ProfileModule/ # Profile 业务模块
│ ├── SettingModule/ # Setting 业务模块
│ ├── LoginModule/ # Login 业务模块
│ └── NotFoundModule/ # 兜底模块(未匹配路由)
└── readme.md
AppRouting:定义路由、导航行为和路由器核心能力,是最底层抽象。AppModuleFacade:对上层 App 提供统一模块接口,减少业务模块与 App 壳直接耦合。*Module(Home/Login/Profile/Setting/NotFound):各自实现模块入口,向路由注册对应页面构建逻辑。SceneDelegate:集中完成依赖注入、Tab 模块装配、非 Tab 模块注册与应用启动。AppDeepLinkMiddleware:进行 URL 白名单校验后,将 URL 映射为内部路由对象。
- Xcode 16+
- Swift 6.2(各
Package.swift已声明) - iOS 15.0+
- 用 Xcode 打开
iOSArchitecture.xcodeproj。 - 等待 Swift Package 依赖解析完成。
- 选择
iOSArchitectureScheme。 - 选择任意 iOS Simulator 或真机运行。
AppDeepLinkMiddleware 当前允许:
- Scheme:
myapp - Host:
app
支持的 path 映射示例:
myapp://app/home->HomeRoute.homemyapp://app/settings->SettingsRoute.settingsmyapp://app/detail?id=1001->HomeRoute.detail(id:)myapp://app/profile?userId=u001->ProfileRoute.profile(userId:)
未命中映射时,交由 NotFoundModule 兜底处理。
- 在
Packages/下创建新 Swift Package(例如OrderModule)。 - 在模块内实现
AppModuleProviding或TabModuleProviding,并注册路由处理器。 - 若需要对外定义路由,放在
AppModuleFacade对应业务目录中统一暴露。 - 在
SceneDelegate中将模块加入:- Tab 页面:加入
tabs - 非 Tab 页面:加入
appModules
- Tab 页面:加入
- 如需支持 DeepLink,在
AppDeepLinkMiddleware的路由表中新增 path 映射。
每个模块都包含独立测试 Target(Tests/)。可按模块分别执行单测,或在 Xcode 中统一运行测试。
- 依赖方向单向:业务模块依赖
AppRouting(及必要公共能力),不反向依赖 App 壳;App 壳负责最终装配。 - 路由定义集中:跨模块可见的路由协议与导航语义由
AppRouting/AppModuleFacade统一管理,避免在业务模块中散落定义。 - 模块边界清晰:每个
*Module只处理本模块路由到页面的映射,不直接感知其他模块内部实现。 - 启动装配集中:
SceneDelegate作为 Composition Root,统一注入依赖、注册模块和启动导航流程。 - DeepLink 先校验后映射:先做 scheme/host 白名单校验,再做 path/query 到内部路由的转换,默认保守放行。
- 兜底可观测:未匹配路由统一进入
NotFoundModule,保证异常路径可见、可追踪,而不是静默失败。 - 新增模块最小改动:新增业务能力时,优先通过“新增模块 + 注册路由 + 场景装配”扩展,避免修改既有模块核心逻辑。
- 跨模块轻量通知走事件总线:适合“不关心具体接收方”的广播(如会话状态);需要落页、入栈、切 Tab 仍以路由为主,避免用事件替代导航导致流程难追踪。
flowchart TB
A[iOS App Shell<br/>AppDelegate / SceneDelegate] --> B[AppDeepLinkMiddleware]
A --> C[AppNavigationAdaptor]
B --> D[Route]
C --> E[AppModuleFacade]
E --> F[AppRouting]
E --> G[AppDependencies]
E --> EB[AppEventBus]
subgraph Tabs[Tab Modules]
H[HomeModule]
I[ProfileModule]
J[SettingModule]
end
subgraph Biz[Non-Tab Modules]
K[LoginModule]
L[NotFoundModule]
end
C --> H
C --> I
C --> J
C --> K
C --> L
H --> F
I --> F
J --> F
K --> F
L --> F
D --> F
K -.->|post AppBusEvent| EB
EB -.->|subscribe via Coordinator| C
说明:
- App 壳层在
SceneDelegate完成模块装配与依赖注入。 AppDeepLinkMiddleware负责 URL 到内部Route的转换。AppNavigationAdaptor统一协调 Tab 模块与非 Tab 模块导航。- 各业务模块通过
AppRouting对齐统一路由与导航协议。 AppEventBus由AppModuleFacade提供类型与实现;LoginModule经ILoginService.loginEventBus投递事件,AppNavigationAdaptor内的AppCoordinator订阅并更新会话 / 鉴权后续导航(虚线为事件流,实线仍为依赖与路由)。
除 路由导航(AppRoutable / Routable)外,项目用 AppModuleFacade 中的 AppEventBus 做 发布—订阅式 的跨模块通知:派发与订阅都在 @MainActor 上执行;事件类型需遵循 AppBusEvent(Sendable),post 时只通知订阅了 同一事件类型 的监听者。
| 场景 | 推荐方式 |
|---|---|
| 打开页面、切换 Tab、DeepLink 落地 | 路由(route(_:from:) 等) |
| 广播状态变化、触发与具体 UI 解耦的副作用 | AppEventBus(如登录态同步) |
- 总线从哪来:
ILoginService协议要求实现方提供loginEventBus: AppEventBus。LoginModule的LoginService内部持有一个AppEventBus实例并对外暴露,由AppService在壳层注册后全局解析。 - 谁发事件:
LoginService.logout()会post(LoginAuthStateEvent(isLoggedIn: false));LoginViewController在模拟登录成功时post(LoginAuthStateEvent(isLoggedIn: true))与post(RefreshEvent())。事件类型LoginAuthStateEvent、RefreshEvent定义在AppModuleFacade(LoginBiz),业务模块只依赖门面即可发事件。 - 谁收事件:
AppCoordinator.addLoginObserve()将on返回的AppEventSubscription存为属性(loginAuthStateSubscription/loginRefreshSubscription),在 Coordinator 生命周期内持续监听并更新AuthenticationSessionManaging;在route鉴权拦截 时用pendingAuthLoginSubscription挂 一次性 监听,登录成功后继续原先路由,并在回调内cancel()后置空,避免重复触发(cancel()同步摘钩;句柄释放时也会在deinit里异步补一次移除,二者幂等)。
- 事件类型放在门面层:与
LoginAuthStateEvent一样放在AppModuleFacade,便于各模块依赖一致类型而不互相引用实现。 - 总线实例由组合根装配:通过某
AppServiceProtocol暴露AppEventBus,而不是让模块 A 直接持有模块 B 的类型。 - 订阅生命周期:
AppEventSubscription为类,在 最后一个强引用释放 时会自动从总线移除(deinit里调度到 MainActor);需要长期收事件时务必把句柄存成属性。若要在回调内 立刻 摘钩(避免同线程后续post再次命中),可调用cancel(),仍可与置空属性配合使用。bus.remove(...)仍保留,用于不持有句柄时的显式移除。
- 按业务域分组:路由按模块拆分,例如
HomeRoute、ProfileRoute、SettingsRoute,不要创建“全局大而全路由枚举”。 - 语义优先:路由 case 使用业务语义命名(如
.detail(id:)、.profile(userId:)),避免使用页面类名(如pushXXXVC)。 - 参数显式化:需要的上下文通过关联值声明,避免通过全局状态或
UserDefaults临时传参。 - 导航行为内聚:每个路由自行声明
navigationStyle和requiresAuthentication,调用方不再重复判断。 - 跨模块路由最小暴露:只暴露必要对外入口,模块内部页面路由保持 internal,降低耦合面。
- DeepLink 一一映射:每条可公开 URL 都应映射到明确路由,并在 README 持续维护示例。
下面以 OrderModule 为例,给出推荐最小骨架。
Packages/OrderModule
├── Package.swift
│ └──OrderBiz
│ └── OrderRoute.swift
├── Sources/OrderModule
│ ├── OrderModule.swift
│ └── OrderViewController.swift
└── Tests/OrderModuleTests
└── OrderModuleTests.swift
@MainActor
public enum OrderRoute: Routable {
case list
case detail(orderId: String)
public var navigationStyle: NavigationStyle {
switch self {
case .list:
return .push(animated: true)
case .detail:
return .push(animated: true)
}
}
public var requiresAuthentication: Bool { true }
public var associatedModule: String { AppModuleName.order.rawValue }
}import AppModuleFacade
@MainActor
public final class OrderModule: AppModuleProviding {
public init() {}
public var moduleName: String { AppModuleName.order.rawValue }
public func registerRouteHandlers(name: String, into resolver: AppModuleBootstrap) {
resolver.append(moduleName: moduleName) { route, routing in
guard let route = route as? OrderRoute else { return nil }
switch route {
case .list:
return OrderViewController(routing: routing)
case .detail(let orderId):
return OrderViewController(routing: routing, orderId: orderId)
}
}
}
}- 在
SceneDelegate的appModules(或tabs)里注册OrderModule()。 - 在
AppModuleFacade增加order模块名(如AppModuleName.order)。 - 若需 DeepLink,补充
AppDeepLinkMiddleware路由表:myapp://app/order->OrderRoute.listmyapp://app/order/detail?orderId=xxx->OrderRoute.detail(orderId:)
- 为
OrderModule添加基础单测,至少覆盖:- route -> viewController 解析成功
- 非本模块 route 不应误匹配