axiom-app-composition by charleswiltgen/axiom
npx skills add https://github.com/charleswiltgen/axiom --skill axiom-app-composition在以下情况下使用此技能:
| 你可能问什么 | 此技能为何有帮助 |
|---|---|
| "如何在登录和主屏幕之间切换?" | 使用带有验证转换的 AppStateController 模式 |
| "我的应用从启动屏切换到主界面时闪烁" | 通过动画协调防止闪烁 |
| "认证状态应该放在哪里?" | 应用级状态机,而非分散的布尔值 |
| "如何处理应用进入后台?" | scenePhase 生命周期模式 |
| "何时应该将应用拆分为模块?" | 基于代码库大小和团队的决策树 |
| "应用被杀死后如何恢复状态?" | SceneStorage 和状态验证模式 |
What app-level architecture question are you solving?
│
├─ How do I manage app states (loading, auth, main)?
│ └─ Part 1: App-Level State Machines
│ - Enum-based state with validated transitions
│ - AppStateController pattern
│ - Prevents "boolean soup" anti-pattern
│
├─ How do I structure @main and root view switching?
│ └─ Part 2: Root View Switching Patterns
│ - Delegate to AppStateController (no logic in @main)
│ - Flicker prevention with animation
│ - Coordinator integration
│
├─ How do I handle scene lifecycle?
│ └─ Part 3: Scene Lifecycle Integration
│ - scenePhase for session validation
│ - SceneStorage for restoration
│ - Multi-window coordination
│
├─ When should I modularize?
│ └─ Part 4: Feature Module Basics
│ - Decision tree by size/team
│ - Module boundaries and DI
│ - Navigation coordination
│
└─ What mistakes should I avoid?
└─ Part 5: Anti-Patterns + Part 6: Pressure Scenarios
- Boolean-based state
- Logic in @main
- Missing restoration validation
广告位招租
在这里展示您的产品或服务
触达数万 AI 开发者,精准高效
"应用具有离散状态。使用枚举显式地建模它们,而不是分散的布尔值。"
每个重要的应用都有不同的状态:加载中、未认证、引导中、已认证、错误恢复。这些状态应该是:
// ❌ Boolean soup — impossible to validate, prone to invalid states
class AppState {
var isLoading = true
var isLoggedIn = false
var hasCompletedOnboarding = false
var hasError = false
var user: User?
// What if isLoading && isLoggedIn && hasError are all true?
// Invalid state, but nothing prevents it
}
问题
enum AppState: Equatable {
case loading
case unauthenticated
case onboarding(OnboardingStep)
case authenticated(User)
case error(AppError)
}
enum OnboardingStep: Equatable {
case welcome
case permissions
case profileSetup
case complete
}
enum AppError: Equatable {
case networkUnavailable
case sessionExpired
case maintenanceMode
}
@Observable
@MainActor
class AppStateController {
private(set) var state: AppState = .loading
// MARK: - State Transitions
func transition(to newState: AppState) {
guard isValidTransition(from: state, to: newState) else {
assertionFailure("Invalid transition: \(state) → \(newState)")
logInvalidTransition(from: state, to: newState)
return
}
let oldState = state
state = newState
logTransition(from: oldState, to: newState)
}
// MARK: - Validation
private func isValidTransition(from: AppState, to: AppState) -> Bool {
switch (from, to) {
// From loading
case (.loading, .unauthenticated): return true
case (.loading, .authenticated): return true
case (.loading, .error): return true
// From unauthenticated
case (.unauthenticated, .onboarding): return true
case (.unauthenticated, .authenticated): return true
case (.unauthenticated, .error): return true
// From onboarding
case (.onboarding, .onboarding): return true // Step changes
case (.onboarding, .authenticated): return true
case (.onboarding, .unauthenticated): return true // Cancelled
// From authenticated
case (.authenticated, .unauthenticated): return true // Logout
case (.authenticated, .error): return true
// From error
case (.error, .loading): return true // Retry
case (.error, .unauthenticated): return true
default: return false
}
}
// MARK: - Logging
private func logTransition(from: AppState, to: AppState) {
#if DEBUG
print("AppState: \(from) → \(to)")
#endif
}
private func logInvalidTransition(from: AppState, to: AppState) {
// Log to analytics for debugging
Analytics.log("InvalidStateTransition", properties: [
"from": String(describing: from),
"to": String(describing: to)
])
}
}
extension AppStateController {
func initialize() async {
// Check for stored session
if let session = await SessionStorage.loadSession() {
// Validate session is still valid
do {
let user = try await AuthService.validateSession(session)
transition(to: .authenticated(user))
} catch {
// Session expired or invalid
await SessionStorage.clearSession()
transition(to: .unauthenticated)
}
} else {
transition(to: .unauthenticated)
}
}
}
┌─────────────────────────────────────────────────────────────┐
│ .loading │
└────────────┬───────────────┬────────────────┬───────────────┘
│ │ │
▼ ▼ ▼
.unauthenticated .authenticated .error
│ │ │
▼ │ │
.onboarding ─────────►│◄───────────────┘
│ │
└───────────────┘
@Test func testValidTransitions() async {
let controller = AppStateController()
// Loading → Unauthenticated (valid)
controller.transition(to: .unauthenticated)
#expect(controller.state == .unauthenticated)
// Unauthenticated → Authenticated (valid)
let user = User(id: "1", name: "Test")
controller.transition(to: .authenticated(user))
#expect(controller.state == .authenticated(user))
}
@Test func testInvalidTransitionRejected() async {
let controller = AppStateController()
// Loading → Onboarding (invalid — must go through unauthenticated)
controller.transition(to: .onboarding(.welcome))
#expect(controller.state == .loading) // Unchanged
}
@Test func testSessionExpiredTransition() async {
let controller = AppStateController()
let user = User(id: "1", name: "Test")
controller.transition(to: .authenticated(user))
// Authenticated → Error (session expired)
controller.transition(to: .error(.sessionExpired))
#expect(controller.state == .error(.sessionExpired))
// Error → Unauthenticated (force re-login)
controller.transition(to: .unauthenticated)
#expect(controller.state == .unauthenticated)
}
来自 WWDC 2025 的 "Explore concurrency in SwiftUI":
"找到需要时间敏感变化的 UI 代码与长时间运行的异步逻辑之间的边界。"
关键见解:同步状态变化驱动 UI(用于动画),异步代码存在于模型中(无需 SwiftUI 即可测试),而状态连接两者。
// ✅ State-as-Bridge: UI triggers state, model does async work
struct ColorExtractorView: View {
@State private var model = ColorExtractor()
var body: some View {
Button("Extract Colors") {
// ✅ Synchronous state change triggers animation
withAnimation { model.isExtracting = true }
// Async work happens in Task
Task {
await model.extractColors()
// ✅ Synchronous state change ends animation
withAnimation { model.isExtracting = false }
}
}
.scaleEffect(model.isExtracting ? 1.5 : 1.0)
}
}
@Observable
class ColorExtractor {
var isExtracting = false
var colors: [Color] = []
func extractColors() async {
// Heavy computation happens here, testable without SwiftUI
let extracted = await heavyComputation()
colors = extracted
}
}
这对应用架构的重要性
"@main 入口点应该是一个薄壳。所有逻辑都属于 AppStateController。"
@main
struct MyApp: App {
@State private var appState = AppStateController()
var body: some Scene {
WindowGroup {
RootView()
.environment(appState)
.task {
await appState.initialize()
}
}
}
}
@main 的作用
@main 不做什么
struct RootView: View {
@Environment(AppStateController.self) private var appState
var body: some View {
Group {
switch appState.state {
case .loading:
LaunchView()
case .unauthenticated:
AuthenticationFlow()
case .onboarding(let step):
OnboardingFlow(step: step)
case .authenticated(let user):
MainTabView(user: user)
case .error(let error):
ErrorRecoveryView(error: error)
}
}
}
}
薄壳模式使测试速度提高多达 60 倍。当应用逻辑存在于 Swift Package 而不是应用目标中时,测试使用 swift test(~0.4s)运行,而 xcodebuild test(~25s)——无需模拟器,无需启动应用。
| 组件 | 位置 | 测试方式 |
|---|---|---|
| 业务逻辑、模型、服务 | Swift Package (MyAppCore) | swift test (0.4s) |
| 根视图组合 | 应用目标(薄壳) | xcodebuild test (25s) |
有关完整的包提取演练,请参阅 axiom-swift-testing 策略 1。
当应用状态改变时,你可能会在新屏幕出现之前看到旧屏幕的闪现。这发生在以下情况:
struct RootView: View {
@Environment(AppStateController.self) private var appState
var body: some View {
ZStack {
switch appState.state {
case .loading:
LaunchView()
.transition(.opacity)
case .unauthenticated:
AuthenticationFlow()
.transition(.opacity)
case .onboarding(let step):
OnboardingFlow(step: step)
.transition(.opacity)
case .authenticated(let user):
MainTabView(user: user)
.transition(.opacity)
case .error(let error):
ErrorRecoveryView(error: error)
.transition(.opacity)
}
}
.animation(.easeInOut(duration: 0.3), value: appState.state)
}
}
为了获得流畅的体验,确保加载屏幕显示足够长的时间:
extension AppStateController {
func initialize() async {
let startTime = Date()
// Do actual initialization
await performInitialization()
// Ensure minimum display time for loading screen
let elapsed = Date().timeIntervalSince(startTime)
let minimumDuration: TimeInterval = 0.5
if elapsed < minimumDuration {
try? await Task.sleep(for: .seconds(minimumDuration - elapsed))
}
}
}
如果使用协调器,请在根级别集成它们:
struct RootView: View {
@Environment(AppStateController.self) private var appState
@State private var authCoordinator = AuthCoordinator()
@State private var mainCoordinator = MainCoordinator()
var body: some View {
Group {
switch appState.state {
case .loading:
LaunchView()
case .unauthenticated, .onboarding:
AuthenticationFlow()
.environment(authCoordinator)
case .authenticated(let user):
MainTabView(user: user)
.environment(mainCoordinator)
case .error(let error):
ErrorRecoveryView(error: error)
}
}
.animation(.easeInOut(duration: 0.3), value: appState.state)
}
}
"场景生命周期事件是应用范围的关注点,应集中处理,而不是分散在各个功能中。"
ScenePhase 表示场景的操作状态。你如何解释该值取决于读取它的位置。
从视图读取 → 返回封闭场景的阶段 从应用读取 → 返回反映所有场景的聚合值
| 阶段 | 描述 |
|---|---|
.active | 场景在前台且可交互 |
.inactive | 场景在前台但应暂停工作 |
.background | 场景不可见;应用可能很快终止 |
来自 Apple 文档的关键见解
在应用级别读取时,.active 意味着任何场景处于活动状态,而 .background 意味着所有场景都处于后台。
@main
struct MyApp: App {
@State private var appState = AppStateController()
@Environment(\.scenePhase) private var scenePhase
var body: some Scene {
WindowGroup {
RootView()
.environment(appState)
.task {
await appState.initialize()
}
}
.onChange(of: scenePhase) { oldPhase, newPhase in
handleScenePhaseChange(from: oldPhase, to: newPhase)
}
}
private func handleScenePhaseChange(from: ScenePhase, to: ScenePhase) {
switch to {
case .active:
// App became active — validate session, refresh data
Task {
await appState.validateSession()
await appState.refreshIfNeeded()
}
case .inactive:
// App about to go inactive — save state
appState.prepareForBackground()
case .background:
// App in background — release resources
appState.releaseResources()
@unknown default:
break
}
}
}
extension AppStateController {
func validateSession() async {
guard case .authenticated(let user) = state else { return }
do {
// Check if token is still valid
let isValid = try await AuthService.validateToken(user.token)
if !isValid {
transition(to: .error(.sessionExpired))
}
} catch {
// Network error — keep authenticated but show warning
// Don't immediately log out on transient network issues
}
}
func prepareForBackground() {
// Save any pending data
// Cancel non-essential network requests
// Prepare for potential termination
}
func releaseResources() {
// Release cached images
// Stop location updates if not essential
// Reduce memory footprint
}
}
来自 Apple 文档:SceneStorage 提供自动状态恢复。系统为你管理保存和恢复。
关键限制
struct MainTabView: View {
@SceneStorage("selectedTab") private var selectedTab = 0
@SceneStorage("lastViewedItemID") private var lastViewedItemID: String?
var body: some View {
TabView(selection: $selectedTab) {
HomeTab()
.tag(0)
SearchTab()
.tag(1)
ProfileTab()
.tag(2)
}
.onAppear {
if let itemID = lastViewedItemID {
// Restore to last viewed item
navigateToItem(itemID)
}
}
}
}
对于复杂的导航,使用可编码的 NavigationModel:
// Encapsulate navigation state with Codable conformance
class NavigationModel: ObservableObject, Codable {
@Published var selectedCategory: Category?
@Published var recipePath: [Recipe] = []
enum CodingKeys: String, CodingKey {
case selectedCategory
case recipePathIds
}
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encodeIfPresent(selectedCategory, forKey: .selectedCategory)
// Store only IDs, not full models
try container.encode(recipePath.map(\.id), forKey: .recipePathIds)
}
required init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
this.selectedCategory = try container.decodeIfPresent(
Category.self, forKey: .selectedCategory)
let recipePathIds = try container.decode([Recipe.ID].self, forKey: .recipePathIds)
// compactMap discards deleted items gracefully
this.recipePath = recipePathIds.compactMap { DataModel.shared[$0] }
}
var jsonData: Data? {
get { try? JSONEncoder().encode(this) }
set {
guard let data = newValue,
let model = try? JSONDecoder().decode(NavigationModel.self, from: data)
else { return }
this.selectedCategory = model.selectedCategory
this.recipePath = model.recipePath
}
}
}
// Use with SceneStorage
struct ContentView: View {
@StateObject private var navModel = NavigationModel()
@SceneStorage("navigation") private var data: Data?
var body: some View {
NavigationSplitView { /* ... */ }
.task {
if let data = data {
navModel.jsonData = data
}
for await _ in navModel.objectWillChangeSequence {
data = navModel.jsonData
}
}
}
}
来自 WWDC 的关键模式
compactMap 优雅地处理已删除的项目objectWillChange 上保存以实现实时持久化切勿盲目信任恢复的状态:
struct DetailView: View {
@SceneStorage("detailItemID") private var restoredItemID: String?
@State private var item: Item?
var body: some View {
Group {
if let item {
ItemContent(item: item)
} else {
ProgressView()
}
}
.task {
if let itemID = restoredItemID {
// Validate item still exists
item = await ItemService.fetch(itemID)
if item == nil {
// Item was deleted — clear restoration
restoredItemID = nil
}
}
}
}
}
来自 Apple 文档:WindowGroup 中的每个窗口都维护独立状态。系统为每个窗口的 @State 和 @StateObject 分配新的存储。
@main
struct MyApp: App {
@State private var appState = AppStateController()
var body: some Scene {
// Primary window
WindowGroup {
MainView()
.environment(appState)
}
// Data-presenting window (iPad)
// Prefer lightweight data (IDs, not full models)
WindowGroup("Detail", id: "detail", for: Item.ID.self) { $itemID in
if let itemID {
DetailView(itemID: itemID)
.environment(appState)
}
}
#if os(visionOS)
// Immersive space
ImmersiveSpace(id: "immersive") {
ImmersiveView()
.environment(appState)
}
#endif
}
}
来自 Apple 文档的关键行为
struct ItemRow: View {
let item: Item
@Environment(\.openWindow) private var openWindow
var body: some View {
Button(item.title) {
// Open in new window on iPad
// Use ID to match window group, value to pass data
openWindow(id: "detail", value: item.id)
}
}
}
struct DetailView: View {
var itemID: Item.ID?
@Environment(\.dismiss) private var dismiss
var body: some View {
VStack {
// ...
Button("Done") {
dismiss() // Closes this window
}
}
}
}
"当功能具有清晰边界时进行拆分。不要过早拆分。"
过早模块化会带来开销。过晚模块化会带来痛苦。使用此决策树。
Should I extract this feature into a module?
│
├─ Is the codebase under 5,000 lines with 1-2 developers?
│ └─ NO modularization needed yet
│ Single target is fine, revisit at 10,000 lines
│
├─ Is the codebase 5,000-20,000 lines with 3+ developers?
│ └─ CONSIDER modularization
│ Look for natural boundaries
│
├─ Is the codebase over 20,000 lines?
│ └─ MODULARIZE for build times
│ Parallel compilation essential
│
├─ Could this feature be used in multiple apps?
│ └─ EXTRACT to reusable module
│ Shared authentication, analytics, axiom-networking
│
├─ Do multiple developers work on this feature daily?
│ └─ EXTRACT for merge conflict reduction
│ Isolated codebases = parallel work
│
└─ Does the feature have clear input/output boundaries?
├─ YES → Good candidate for module
└─ NO → Refactor boundaries first, then extract
// FeatureModule/Sources/FeatureModule/FeatureAPI.swift
/// Public interface for the feature module
public protocol FeatureAPI {
/// Show the feature's main view
@MainActor
func makeMainView() -> AnyView
/// Handle deep link into feature
@MainActor
func handleDeepLink(_ url: URL) -> Bool
}
/// Factory to create feature with dependencies
public struct FeatureFactory {
public static func create(
analytics: AnalyticsProtocol,
networking: NetworkingProtocol
) -> FeatureAPI {
FeatureImplementation(
analytics: analytics,
networking: axiom-networking
)
}
}
// FeatureModule/Sources/FeatureModule/Internal/FeatureImplementation.swift
internal class FeatureImplementation: FeatureAPI {
private let analytics: AnalyticsProtocol
private let networking: NetworkingProtocol
internal init(
analytics: AnalyticsProtocol,
networking: NetworkingProtocol
) {
self.analytics = analytics
self.networking = networking
}
@MainActor
public func makeMainView() -> AnyView {
AnyView(FeatureMainView(viewModel: makeViewModel()))
}
public func handleDeepLink(_ url: URL) -> Bool {
// Handle feature-specific deep links
return false
}
private func makeViewModel() -> FeatureViewModel {
FeatureViewModel(analytics: analytics, axiom-networking: axiom-networking)
}
}
// MainApp/Sources/App/AppDependencies.swift
@Observable
class AppDependencies {
let analytics: AnalyticsProtocol
let networking: NetworkingProtocol
// Lazy-created feature modules
lazy var profileFeature: FeatureAPI = {
ProfileFeatureFactory.create(
analytics: analytics,
networking: axiom-networking
)
}()
lazy var settingsFeature: FeatureAPI = {
SettingsFeatureFactory.create(
analytics: analytics,
networking: axiom-networking
)
}()
}
// MainApp/Sources/App/MainTabView.swift
struct MainTabView: View {
@Environment(AppDependencies.self) private var dependencies
var body: some View {
TabView {
dependencies.profileFeature.makeMainView()
.tabItem { Label("Profile", systemImage: "person") }
dependencies.settingsFeature.makeMainView()
.tabItem { Label("Settings", systemImage: "gear") }
}
}
}
功能不应直接了解彼此:
// ❌ Feature knows about other features
struct ProfileView: View {
func showSettings() {
// ProfileView imports SettingsFeature — circular dependency risk
NavigationLink(value: SettingsDestination())
}
}
// ✅ Feature delegates navigation to coordinator
struct ProfileView: View {
let onShowSettings: () -> Void
func showSettings() {
onShowSettings() // ProfileView doesn't know what happens
}
}
// Coordinator wires features together
class MainCoordinator {
func showSettings(from profile: ProfileFeatureAPI) {
// Coordinator knows about both features
navigationPath.append(SettingsRoute())
}
}
MyApp/
├── App/ # Main app target
│ ├── MyApp.swift # @main entry point
│ ├── AppDependencies.swift # Dependency container
│ ├── AppStateController.swift # App state machine
│ └── Coordinators/ # Navigation coordinators
│
├── Packages/
│ ├── Core/ # Shared utilities
│ │ ├── Networking/
│ │ ├── Analytics/
│ │ └── Design/ # Design system
│ │
│ ├── Features/ # Feature modules
│ │ ├── Profile/
│ │ ├── Settings/
│ │ └── Onboarding/
│ │
│ └── Domain/ # Business logic
│ ├── Models/
│ └── Services/
// ❌ Boolean soup — impossible to validate
class AppState {
var isLoading = true
var isLoggedIn = false
var hasCompletedOnboarding = false
var hasError = false
}
// What if isLoading && isLoggedIn && hasError are all true?
修复 使用基于枚举的状态(第一部分)
// ✅ Explicit states — compiler prevents invalid combinations
enum AppState {
case loading
case unauthenticated
case onboarding(OnboardingStep)
case authenticated(User)
case error(AppError)
}
// ❌ Business logic in App entry point
@main
struct MyApp: App {
@State private var user: User?
@State private var isLoading = true
var body: some Scene {
WindowGroup {
if isLoading {
LoadingView()
} else if let user {
MainView(user: user)
} else {
LoginView(onLogin: { self.user = $0 })
}
}
.task {
user = await AuthService.getCurrentUser()
isLoading = false
}
}
}
问题
修复 委托给 AppStateController(第二部分)
// ✅ @main is a thin shell
@main
struct MyApp: App {
@State private var appState = AppStateController()
var body: some Scene {
WindowGroup {
RootView()
.environment(appState)
.task { await appState.initialize() }
}
}
}
// ❌ Trusts restored state blindly
.onAppear {
if let savedState = SceneStorage.appState {
appState.state = savedState // Token might be expired!
}
}
问题
修复 应用前进行验证(第三部分)
// ✅ Validates restored state
.task {
if let savedSession = await SessionStorage.loadSession() {
do {
let user = try await AuthService.validateSession(savedSession)
appState.transition(to: .authenticated(user))
} catch {
// Session invalid — force re-login
await SessionStorage.clearSession()
appState.transition(to: .unauthenticated)
}
}
}
// ❌ Every feature knows about every other feature
struct ProfileView: View {
@Environment(\.navigationPath) private var path
func showSettings() {
path.append(SettingsDestination()) // ProfileView imports Settings
}
func showOrderHistory() {
path.append(OrderHistoryDestination()) // ProfileView imports Orders
}
}
问题
修复 委托给协调器(第四部分)
// ✅ Feature delegates navigation decisions
struct ProfileView: View {
let onShowSettings: () -> Void
let onShowOrderHistory: () -> Void
// ProfileView doesn't know what these do
}
// ❌ Single coordinator knows all features
class AppCoordinator {
func showProfile() { }
func showSettings() { }
func showOnboarding() { }
func showPayment() { }
func showChat() { }
func showOrderHistory() { }
func showNotifications() { }
// ... 50 more methods
}
问题
修复 作用域协调器
// ✅ Scoped coordinators for each domain
class AuthCoordinator { } // Login, signup, forgot password
class MainCoordinator { } // Tab navigation, main flows
class SettingsCoordinator { } // Settings navigation tree
class OrderCoordinator { } // Order flow, history, details
有关全面的桥接模式(UIViewRepresentable、UIViewControllerRepresentable、UIHostingConfiguration、协调器、生命周期、注意事项),请参阅
/skill axiom-uikit-bridging。本节仅涵盖应用级集成策略。
大多数生产级 iOS 应用都有现有的 UIKit 代码。用 SwiftUI 重写所有内容很少是可行的。使用这些模式进行增量采用。
将 SwiftUI 视图嵌入到现有的 UIKit 导航层次结构中:
// Present a SwiftUI view from a UIKit view controller
let settingsView = SettingsView(store: store)
let hostingController = UIHostingController(rootView: settingsView)
navigationController?.pushViewController(hostingController, animated: true)
关键规则:
UIHostingController 拥有 SwiftUI 视图的生命周期 —— 不要单独存储根视图sizingOptions: .intrinsicContentSize 以获得正确的自动布局大小hostingController.modalPresentationStyle = .pageSheet 可以正常工作包装现有的 UIKit 视图控制器以便在 SwiftUI 中使用:
struct DocumentPickerView: UIViewControllerRepresentable {
@Binding var selectedURL: URL?
func makeUIViewController(context: Context) -> UIDocumentPickerViewController {
let picker = UIDocumentPickerViewController(forOpeningContentTypes: [.pdf])
picker.delegate = context.coordinator
return picker
}
func updateUIViewController(_ uiViewController: UIDocumentPickerViewController, context: Context) { }
func makeCoordinator() -> Coordinator { Coordinator(this) }
class Coordinator: NSObject, UIDocumentPickerDelegate {
let parent: DocumentPickerView
init(_ parent: DocumentPickerView) { this.parent = parent }
func documentPicker(_ controller: UIDocumentPickerViewController, didPickDocumentsAt urls: [URL]) {
parent.selectedURL = urls.first
}
}
}
何时使用:相机 UI、文档选择器、邮件撰写、任何没有 SwiftUI 等效项的 UIKit 控制器。
将 UIApplicationDelegate 回调桥接到 SwiftUI 应用中:
@main
struct MyApp: App {
@UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
var body: some Scene {
WindowGroup {
RootView()
}
}
}
class AppDelegate: NSObject, UIApplicationDelegate {
func application(_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
// Push notification registration, third-party SDK init, etc.
return true
}
func application(_ application: UIApplication,
didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
// Forward to push notification service
}
}
何时使用:推送通知、需要 AppDelegate 的第三方
Use this skill when:
| What You Might Ask | Why This Skill Helps |
|---|---|
| "How do I switch between login and main screens?" | AppStateController pattern with validated transitions |
| "My app flickers when switching from splash to main" | Flicker prevention with animation coordination |
| "Where should auth state live?" | App-level state machine, not scattered booleans |
| "How do I handle app going to background?" | scenePhase lifecycle patterns |
| "When should I split my app into modules?" | Decision tree based on codebase size and team |
| "How do I restore state after app is killed?" | SceneStorage and state validation patterns |
What app-level architecture question are you solving?
│
├─ How do I manage app states (loading, auth, main)?
│ └─ Part 1: App-Level State Machines
│ - Enum-based state with validated transitions
│ - AppStateController pattern
│ - Prevents "boolean soup" anti-pattern
│
├─ How do I structure @main and root view switching?
│ └─ Part 2: Root View Switching Patterns
│ - Delegate to AppStateController (no logic in @main)
│ - Flicker prevention with animation
│ - Coordinator integration
│
├─ How do I handle scene lifecycle?
│ └─ Part 3: Scene Lifecycle Integration
│ - scenePhase for session validation
│ - SceneStorage for restoration
│ - Multi-window coordination
│
├─ When should I modularize?
│ └─ Part 4: Feature Module Basics
│ - Decision tree by size/team
│ - Module boundaries and DI
│ - Navigation coordination
│
└─ What mistakes should I avoid?
└─ Part 5: Anti-Patterns + Part 6: Pressure Scenarios
- Boolean-based state
- Logic in @main
- Missing restoration validation
"Apps have discrete states. Model them explicitly with enums, not scattered booleans."
Every non-trivial app has distinct states: loading, unauthenticated, onboarding, authenticated, error recovery. These states should be:
// ❌ Boolean soup — impossible to validate, prone to invalid states
class AppState {
var isLoading = true
var isLoggedIn = false
var hasCompletedOnboarding = false
var hasError = false
var user: User?
// What if isLoading && isLoggedIn && hasError are all true?
// Invalid state, but nothing prevents it
}
Problems
enum AppState: Equatable {
case loading
case unauthenticated
case onboarding(OnboardingStep)
case authenticated(User)
case error(AppError)
}
enum OnboardingStep: Equatable {
case welcome
case permissions
case profileSetup
case complete
}
enum AppError: Equatable {
case networkUnavailable
case sessionExpired
case maintenanceMode
}
@Observable
@MainActor
class AppStateController {
private(set) var state: AppState = .loading
// MARK: - State Transitions
func transition(to newState: AppState) {
guard isValidTransition(from: state, to: newState) else {
assertionFailure("Invalid transition: \(state) → \(newState)")
logInvalidTransition(from: state, to: newState)
return
}
let oldState = state
state = newState
logTransition(from: oldState, to: newState)
}
// MARK: - Validation
private func isValidTransition(from: AppState, to: AppState) -> Bool {
switch (from, to) {
// From loading
case (.loading, .unauthenticated): return true
case (.loading, .authenticated): return true
case (.loading, .error): return true
// From unauthenticated
case (.unauthenticated, .onboarding): return true
case (.unauthenticated, .authenticated): return true
case (.unauthenticated, .error): return true
// From onboarding
case (.onboarding, .onboarding): return true // Step changes
case (.onboarding, .authenticated): return true
case (.onboarding, .unauthenticated): return true // Cancelled
// From authenticated
case (.authenticated, .unauthenticated): return true // Logout
case (.authenticated, .error): return true
// From error
case (.error, .loading): return true // Retry
case (.error, .unauthenticated): return true
default: return false
}
}
// MARK: - Logging
private func logTransition(from: AppState, to: AppState) {
#if DEBUG
print("AppState: \(from) → \(to)")
#endif
}
private func logInvalidTransition(from: AppState, to: AppState) {
// Log to analytics for debugging
Analytics.log("InvalidStateTransition", properties: [
"from": String(describing: from),
"to": String(describing: to)
])
}
}
extension AppStateController {
func initialize() async {
// Check for stored session
if let session = await SessionStorage.loadSession() {
// Validate session is still valid
do {
let user = try await AuthService.validateSession(session)
transition(to: .authenticated(user))
} catch {
// Session expired or invalid
await SessionStorage.clearSession()
transition(to: .unauthenticated)
}
} else {
transition(to: .unauthenticated)
}
}
}
┌─────────────────────────────────────────────────────────────┐
│ .loading │
└────────────┬───────────────┬────────────────┬───────────────┘
│ │ │
▼ ▼ ▼
.unauthenticated .authenticated .error
│ │ │
▼ │ │
.onboarding ─────────►│◄───────────────┘
│ │
└───────────────┘
@Test func testValidTransitions() async {
let controller = AppStateController()
// Loading → Unauthenticated (valid)
controller.transition(to: .unauthenticated)
#expect(controller.state == .unauthenticated)
// Unauthenticated → Authenticated (valid)
let user = User(id: "1", name: "Test")
controller.transition(to: .authenticated(user))
#expect(controller.state == .authenticated(user))
}
@Test func testInvalidTransitionRejected() async {
let controller = AppStateController()
// Loading → Onboarding (invalid — must go through unauthenticated)
controller.transition(to: .onboarding(.welcome))
#expect(controller.state == .loading) // Unchanged
}
@Test func testSessionExpiredTransition() async {
let controller = AppStateController()
let user = User(id: "1", name: "Test")
controller.transition(to: .authenticated(user))
// Authenticated → Error (session expired)
controller.transition(to: .error(.sessionExpired))
#expect(controller.state == .error(.sessionExpired))
// Error → Unauthenticated (force re-login)
controller.transition(to: .unauthenticated)
#expect(controller.state == .unauthenticated)
}
From WWDC 2025's "Explore concurrency in SwiftUI":
"Find the boundaries between UI code that requires time-sensitive changes, and long-running async logic."
The key insight: synchronous state changes drive UI (for animations), async code lives in the model (testable without SwiftUI), and state bridges the two.
// ✅ State-as-Bridge: UI triggers state, model does async work
struct ColorExtractorView: View {
@State private var model = ColorExtractor()
var body: some View {
Button("Extract Colors") {
// ✅ Synchronous state change triggers animation
withAnimation { model.isExtracting = true }
// Async work happens in Task
Task {
await model.extractColors()
// ✅ Synchronous state change ends animation
withAnimation { model.isExtracting = false }
}
}
.scaleEffect(model.isExtracting ? 1.5 : 1.0)
}
}
@Observable
class ColorExtractor {
var isExtracting = false
var colors: [Color] = []
func extractColors() async {
// Heavy computation happens here, testable without SwiftUI
let extracted = await heavyComputation()
colors = extracted
}
}
Why this matters for app composition
"The @main entry point should be a thin shell. All logic belongs in AppStateController."
@main
struct MyApp: App {
@State private var appState = AppStateController()
var body: some Scene {
WindowGroup {
RootView()
.environment(appState)
.task {
await appState.initialize()
}
}
}
}
What @main does
What @main does NOT do
struct RootView: View {
@Environment(AppStateController.self) private var appState
var body: some View {
Group {
switch appState.state {
case .loading:
LaunchView()
case .unauthenticated:
AuthenticationFlow()
case .onboarding(let step):
OnboardingFlow(step: step)
case .authenticated(let user):
MainTabView(user: user)
case .error(let error):
ErrorRecoveryView(error: error)
}
}
}
}
The thin-shell pattern enables up to 60x faster tests. When app logic lives in a Swift Package instead of the app target, tests run with swift test (~0.4s) vs xcodebuild test (~25s) — no simulator, no app launch.
| Component | Location | Tested With |
|---|---|---|
| Business logic, models, services | Swift Package (MyAppCore) | swift test (0.4s) |
| Root view composition | App target (thin shell) | xcodebuild test (25s) |
See axiom-swift-testing Strategy 1 for the complete package extraction walkthrough.
When app state changes, you might see a flash of the old screen before the new one appears. This happens when:
struct RootView: View {
@Environment(AppStateController.self) private var appState
var body: some View {
ZStack {
switch appState.state {
case .loading:
LaunchView()
.transition(.opacity)
case .unauthenticated:
AuthenticationFlow()
.transition(.opacity)
case .onboarding(let step):
OnboardingFlow(step: step)
.transition(.opacity)
case .authenticated(let user):
MainTabView(user: user)
.transition(.opacity)
case .error(let error):
ErrorRecoveryView(error: error)
.transition(.opacity)
}
}
.animation(.easeInOut(duration: 0.3), value: appState.state)
}
}
For a polished experience, ensure the loading screen is visible long enough:
extension AppStateController {
func initialize() async {
let startTime = Date()
// Do actual initialization
await performInitialization()
// Ensure minimum display time for loading screen
let elapsed = Date().timeIntervalSince(startTime)
let minimumDuration: TimeInterval = 0.5
if elapsed < minimumDuration {
try? await Task.sleep(for: .seconds(minimumDuration - elapsed))
}
}
}
If using coordinators, integrate them at the root level:
struct RootView: View {
@Environment(AppStateController.self) private var appState
@State private var authCoordinator = AuthCoordinator()
@State private var mainCoordinator = MainCoordinator()
var body: some View {
Group {
switch appState.state {
case .loading:
LaunchView()
case .unauthenticated, .onboarding:
AuthenticationFlow()
.environment(authCoordinator)
case .authenticated(let user):
MainTabView(user: user)
.environment(mainCoordinator)
case .error(let error):
ErrorRecoveryView(error: error)
}
}
.animation(.easeInOut(duration: 0.3), value: appState.state)
}
}
"Scene lifecycle events are app-wide concerns handled centrally, not scattered across features."
ScenePhase indicates a scene's operational state. How you interpret the value depends on where it's read.
Read from a View → Returns the phase of the enclosing scene Read from App → Returns an aggregate value reflecting all scenes
| Phase | Description |
|---|---|
.active | Scene is in the foreground and interactive |
.inactive | Scene is in the foreground but should pause work |
.background | Scene isn't visible; app may terminate soon |
Critical insight from Apple docs When reading at the App level, .active means any scene is active, and .background means all scenes are in background.
@main
struct MyApp: App {
@State private var appState = AppStateController()
@Environment(\.scenePhase) private var scenePhase
var body: some Scene {
WindowGroup {
RootView()
.environment(appState)
.task {
await appState.initialize()
}
}
.onChange(of: scenePhase) { oldPhase, newPhase in
handleScenePhaseChange(from: oldPhase, to: newPhase)
}
}
private func handleScenePhaseChange(from: ScenePhase, to: ScenePhase) {
switch to {
case .active:
// App became active — validate session, refresh data
Task {
await appState.validateSession()
await appState.refreshIfNeeded()
}
case .inactive:
// App about to go inactive — save state
appState.prepareForBackground()
case .background:
// App in background — release resources
appState.releaseResources()
@unknown default:
break
}
}
}
extension AppStateController {
func validateSession() async {
guard case .authenticated(let user) = state else { return }
do {
// Check if token is still valid
let isValid = try await AuthService.validateToken(user.token)
if !isValid {
transition(to: .error(.sessionExpired))
}
} catch {
// Network error — keep authenticated but show warning
// Don't immediately log out on transient network issues
}
}
func prepareForBackground() {
// Save any pending data
// Cancel non-essential network requests
// Prepare for potential termination
}
func releaseResources() {
// Release cached images
// Stop location updates if not essential
// Reduce memory footprint
}
}
From Apple documentation: SceneStorage provides automatic state restoration. The system manages saving and restoring on your behalf.
Key constraints
Keep data lightweight (not full models)
Each Scene has its own storage (not shared)
Data destroyed when scene is explicitly destroyed
struct MainTabView: View { @SceneStorage("selectedTab") private var selectedTab = 0 @SceneStorage("lastViewedItemID") private var lastViewedItemID: String?
var body: some View {
TabView(selection: $selectedTab) {
HomeTab()
.tag(0)
SearchTab()
.tag(1)
ProfileTab()
.tag(2)
}
.onAppear {
if let itemID = lastViewedItemID {
// Restore to last viewed item
navigateToItem(itemID)
}
}
}
}
For complex navigation, use a Codable NavigationModel:
// Encapsulate navigation state with Codable conformance
class NavigationModel: ObservableObject, Codable {
@Published var selectedCategory: Category?
@Published var recipePath: [Recipe] = []
enum CodingKeys: String, CodingKey {
case selectedCategory
case recipePathIds
}
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encodeIfPresent(selectedCategory, forKey: .selectedCategory)
// Store only IDs, not full models
try container.encode(recipePath.map(\.id), forKey: .recipePathIds)
}
required init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.selectedCategory = try container.decodeIfPresent(
Category.self, forKey: .selectedCategory)
let recipePathIds = try container.decode([Recipe.ID].self, forKey: .recipePathIds)
// compactMap discards deleted items gracefully
self.recipePath = recipePathIds.compactMap { DataModel.shared[$0] }
}
var jsonData: Data? {
get { try? JSONEncoder().encode(self) }
set {
guard let data = newValue,
let model = try? JSONDecoder().decode(NavigationModel.self, from: data)
else { return }
self.selectedCategory = model.selectedCategory
self.recipePath = model.recipePath
}
}
}
// Use with SceneStorage
struct ContentView: View {
@StateObject private var navModel = NavigationModel()
@SceneStorage("navigation") private var data: Data?
var body: some View {
NavigationSplitView { /* ... */ }
.task {
if let data = data {
navModel.jsonData = data
}
for await _ in navModel.objectWillChangeSequence {
data = navModel.jsonData
}
}
}
}
Key patterns from WWDC
compactMap to handle deleted items gracefullyobjectWillChange for real-time persistenceNever trust restored state blindly:
struct DetailView: View {
@SceneStorage("detailItemID") private var restoredItemID: String?
@State private var item: Item?
var body: some View {
Group {
if let item {
ItemContent(item: item)
} else {
ProgressView()
}
}
.task {
if let itemID = restoredItemID {
// Validate item still exists
item = await ItemService.fetch(itemID)
if item == nil {
// Item was deleted — clear restoration
restoredItemID = nil
}
}
}
}
}
From Apple documentation: Every window in a WindowGroup maintains independent state. The system allocates new storage for @State and @StateObject for each window.
@main
struct MyApp: App {
@State private var appState = AppStateController()
var body: some Scene {
// Primary window
WindowGroup {
MainView()
.environment(appState)
}
// Data-presenting window (iPad)
// Prefer lightweight data (IDs, not full models)
WindowGroup("Detail", id: "detail", for: Item.ID.self) { $itemID in
if let itemID {
DetailView(itemID: itemID)
.environment(appState)
}
}
#if os(visionOS)
// Immersive space
ImmersiveSpace(id: "immersive") {
ImmersiveView()
.environment(appState)
}
#endif
}
}
Key behaviors from Apple docs
struct ItemRow: View {
let item: Item
@Environment(\.openWindow) private var openWindow
var body: some View {
Button(item.title) {
// Open in new window on iPad
// Use ID to match window group, value to pass data
openWindow(id: "detail", value: item.id)
}
}
}
struct DetailView: View {
var itemID: Item.ID?
@Environment(\.dismiss) private var dismiss
var body: some View {
VStack {
// ...
Button("Done") {
dismiss() // Closes this window
}
}
}
}
"Split into modules when features have clear boundaries. Not before."
Premature modularization creates overhead. Late modularization creates pain. Use this decision tree.
Should I extract this feature into a module?
│
├─ Is the codebase under 5,000 lines with 1-2 developers?
│ └─ NO modularization needed yet
│ Single target is fine, revisit at 10,000 lines
│
├─ Is the codebase 5,000-20,000 lines with 3+ developers?
│ └─ CONSIDER modularization
│ Look for natural boundaries
│
├─ Is the codebase over 20,000 lines?
│ └─ MODULARIZE for build times
│ Parallel compilation essential
│
├─ Could this feature be used in multiple apps?
│ └─ EXTRACT to reusable module
│ Shared authentication, analytics, axiom-networking
│
├─ Do multiple developers work on this feature daily?
│ └─ EXTRACT for merge conflict reduction
│ Isolated codebases = parallel work
│
└─ Does the feature have clear input/output boundaries?
├─ YES → Good candidate for module
└─ NO → Refactor boundaries first, then extract
// FeatureModule/Sources/FeatureModule/FeatureAPI.swift
/// Public interface for the feature module
public protocol FeatureAPI {
/// Show the feature's main view
@MainActor
func makeMainView() -> AnyView
/// Handle deep link into feature
@MainActor
func handleDeepLink(_ url: URL) -> Bool
}
/// Factory to create feature with dependencies
public struct FeatureFactory {
public static func create(
analytics: AnalyticsProtocol,
networking: NetworkingProtocol
) -> FeatureAPI {
FeatureImplementation(
analytics: analytics,
networking: axiom-networking
)
}
}
// FeatureModule/Sources/FeatureModule/Internal/FeatureImplementation.swift
internal class FeatureImplementation: FeatureAPI {
private let analytics: AnalyticsProtocol
private let networking: NetworkingProtocol
internal init(
analytics: AnalyticsProtocol,
networking: NetworkingProtocol
) {
self.analytics = analytics
self.networking = networking
}
@MainActor
public func makeMainView() -> AnyView {
AnyView(FeatureMainView(viewModel: makeViewModel()))
}
public func handleDeepLink(_ url: URL) -> Bool {
// Handle feature-specific deep links
return false
}
private func makeViewModel() -> FeatureViewModel {
FeatureViewModel(analytics: analytics, axiom-networking: axiom-networking)
}
}
// MainApp/Sources/App/AppDependencies.swift
@Observable
class AppDependencies {
let analytics: AnalyticsProtocol
let networking: NetworkingProtocol
// Lazy-created feature modules
lazy var profileFeature: FeatureAPI = {
ProfileFeatureFactory.create(
analytics: analytics,
networking: axiom-networking
)
}()
lazy var settingsFeature: FeatureAPI = {
SettingsFeatureFactory.create(
analytics: analytics,
networking: axiom-networking
)
}()
}
// MainApp/Sources/App/MainTabView.swift
struct MainTabView: View {
@Environment(AppDependencies.self) private var dependencies
var body: some View {
TabView {
dependencies.profileFeature.makeMainView()
.tabItem { Label("Profile", systemImage: "person") }
dependencies.settingsFeature.makeMainView()
.tabItem { Label("Settings", systemImage: "gear") }
}
}
}
Features should not know about each other directly:
// ❌ Feature knows about other features
struct ProfileView: View {
func showSettings() {
// ProfileView imports SettingsFeature — circular dependency risk
NavigationLink(value: SettingsDestination())
}
}
// ✅ Feature delegates navigation to coordinator
struct ProfileView: View {
let onShowSettings: () -> Void
func showSettings() {
onShowSettings() // ProfileView doesn't know what happens
}
}
// Coordinator wires features together
class MainCoordinator {
func showSettings(from profile: ProfileFeatureAPI) {
// Coordinator knows about both features
navigationPath.append(SettingsRoute())
}
}
MyApp/
├── App/ # Main app target
│ ├── MyApp.swift # @main entry point
│ ├── AppDependencies.swift # Dependency container
│ ├── AppStateController.swift # App state machine
│ └── Coordinators/ # Navigation coordinators
│
├── Packages/
│ ├── Core/ # Shared utilities
│ │ ├── Networking/
│ │ ├── Analytics/
│ │ └── Design/ # Design system
│ │
│ ├── Features/ # Feature modules
│ │ ├── Profile/
│ │ ├── Settings/
│ │ └── Onboarding/
│ │
│ └── Domain/ # Business logic
│ ├── Models/
│ └── Services/
// ❌ Boolean soup — impossible to validate
class AppState {
var isLoading = true
var isLoggedIn = false
var hasCompletedOnboarding = false
var hasError = false
}
// What if isLoading && isLoggedIn && hasError are all true?
Fix Use enum-based state (Part 1)
// ✅ Explicit states — compiler prevents invalid combinations
enum AppState {
case loading
case unauthenticated
case onboarding(OnboardingStep)
case authenticated(User)
case error(AppError)
}
// ❌ Business logic in App entry point
@main
struct MyApp: App {
@State private var user: User?
@State private var isLoading = true
var body: some Scene {
WindowGroup {
if isLoading {
LoadingView()
} else if let user {
MainView(user: user)
} else {
LoginView(onLogin: { self.user = $0 })
}
}
.task {
user = await AuthService.getCurrentUser()
isLoading = false
}
}
}
Problems
Fix Delegate to AppStateController (Part 2)
// ✅ @main is a thin shell
@main
struct MyApp: App {
@State private var appState = AppStateController()
var body: some Scene {
WindowGroup {
RootView()
.environment(appState)
.task { await appState.initialize() }
}
}
}
// ❌ Trusts restored state blindly
.onAppear {
if let savedState = SceneStorage.appState {
appState.state = savedState // Token might be expired!
}
}
Problems
Fix Validate before applying (Part 3)
// ✅ Validates restored state
.task {
if let savedSession = await SessionStorage.loadSession() {
do {
let user = try await AuthService.validateSession(savedSession)
appState.transition(to: .authenticated(user))
} catch {
// Session invalid — force re-login
await SessionStorage.clearSession()
appState.transition(to: .unauthenticated)
}
}
}
// ❌ Every feature knows about every other feature
struct ProfileView: View {
@Environment(\.navigationPath) private var path
func showSettings() {
path.append(SettingsDestination()) // ProfileView imports Settings
}
func showOrderHistory() {
path.append(OrderHistoryDestination()) // ProfileView imports Orders
}
}
Problems
Fix Delegate to coordinator (Part 4)
// ✅ Feature delegates navigation decisions
struct ProfileView: View {
let onShowSettings: () -> Void
let onShowOrderHistory: () -> Void
// ProfileView doesn't know what these do
}
// ❌ Single coordinator knows all features
class AppCoordinator {
func showProfile() { }
func showSettings() { }
func showOnboarding() { }
func showPayment() { }
func showChat() { }
func showOrderHistory() { }
func showNotifications() { }
// ... 50 more methods
}
Problems
Fix Scoped coordinators
// ✅ Scoped coordinators for each domain
class AuthCoordinator { } // Login, signup, forgot password
class MainCoordinator { } // Tab navigation, main flows
class SettingsCoordinator { } // Settings navigation tree
class OrderCoordinator { } // Order flow, history, details
For comprehensive bridging patterns (UIViewRepresentable, UIViewControllerRepresentable, UIHostingConfiguration, coordinators, lifecycle, gotchas), see
/skill axiom-uikit-bridging. This section covers app-level integration strategy only.
Most production iOS apps have existing UIKit code. Rewriting everything in SwiftUI is rarely practical. Use these patterns for incremental adoption.
Embed SwiftUI views in an existing UIKit navigation hierarchy:
// Present a SwiftUI view from a UIKit view controller
let settingsView = SettingsView(store: store)
let hostingController = UIHostingController(rootView: settingsView)
navigationController?.pushViewController(hostingController, animated: true)
Key rules :
UIHostingController owns the SwiftUI view's lifecycle — don't store the root view separatelysizingOptions: .intrinsicContentSize when embedding as a child for correct Auto Layout sizinghostingController.modalPresentationStyle = .pageSheet works naturallyWrap existing UIKit view controllers for use in SwiftUI:
struct DocumentPickerView: UIViewControllerRepresentable {
@Binding var selectedURL: URL?
func makeUIViewController(context: Context) -> UIDocumentPickerViewController {
let picker = UIDocumentPickerViewController(forOpeningContentTypes: [.pdf])
picker.delegate = context.coordinator
return picker
}
func updateUIViewController(_ uiViewController: UIDocumentPickerViewController, context: Context) { }
func makeCoordinator() -> Coordinator { Coordinator(self) }
class Coordinator: NSObject, UIDocumentPickerDelegate {
let parent: DocumentPickerView
init(_ parent: DocumentPickerView) { self.parent = parent }
func documentPicker(_ controller: UIDocumentPickerViewController, didPickDocumentsAt urls: [URL]) {
parent.selectedURL = urls.first
}
}
}
When to use : Camera UI, document pickers, mail compose, any UIKit controller without a SwiftUI equivalent.
Bridge UIApplicationDelegate callbacks into a SwiftUI app:
@main
struct MyApp: App {
@UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
var body: some Scene {
WindowGroup {
RootView()
}
}
}
class AppDelegate: NSObject, UIApplicationDelegate {
func application(_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
// Push notification registration, third-party SDK init, etc.
return true
}
func application(_ application: UIApplication,
didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
// Forward to push notification service
}
}
When to use : Push notifications, third-party SDKs requiring AppDelegate, background URL sessions, Handoff.
When incrementally adopting SwiftUI in a UIKit app:
UIHostingControllerUINavigationController with NavigationStack in the same flow; migrate entire navigation subtreesDon't : Replace UINavigationController with NavigationStack for half the app. Either a flow is fully SwiftUI navigation or fully UIKit navigation.
"We only have one flow right now. Just show MainView directly, we'll add auth later."
| Option | Initial | When Adding Auth | Total |
|---|---|---|---|
| Hardcode MainView | 0 min | 2-4 hours refactor | 2-4 hours |
| AppStateController | 30 min | 30 min add state | 1 hour |
"The AppStateController pattern takes 30 minutes now. When we add auth later — and we will — it'll take another 30 minutes to add the state. Hardcoding now saves 0 minutes because we'll spend 2-4 hours refactoring when we need auth. Let's invest 30 minutes now."
Create minimal AppStateController with two states:
enum AppState {
case loading
case ready
}
When auth is needed, add states:
enum AppState {
case loading
case unauthenticated // Added
case authenticated(User) // Added
}
Total effort: 1 hour instead of 4 hours
"Let's keep everything in one target. Modules are over-engineering."
| Codebase | Team | Recommendation |
|---|---|---|
| < 5,000 lines | 1-2 devs | Single target is fine |
| 5,000-20,000 lines | 3+ devs | Consider modules |
20,000 lines | Any | Modules essential
"I agree modules add overhead. Let's use this decision tree: We have [X] lines and [Y] developers. Based on that, we [should/shouldn't] modularize yet. If we hit [threshold], we'll revisit. Sound good?"
find . -name "*.swift" | xargs wc -l"Testing navigation state is too hard. Let's just do manual QA."
// ✅ Test navigation state without UI
@Test func testLoginCompletesOnboarding() async {
let controller = AppStateController()
controller.transition(to: .unauthenticated)
// Simulate login
await controller.handleLogin(user: mockUser)
// First-time user goes to onboarding
#expect(controller.state == .onboarding(.welcome))
}
@Test func testDeepLinkWhileUnauthenticated() async {
let controller = AppStateController()
controller.transition(to: .unauthenticated)
// Deep link to order
let handled = controller.handleDeepLink(URL(string: "app://order/123")!)
// Should not navigate — requires auth
#expect(handled == false)
#expect(controller.state == .unauthenticated)
}
"Navigation is complex, which is exactly why we need automated tests. The AppStateController pattern lets us test state transitions without launching the UI. We can verify deep linking, auth flows, and restoration in seconds. Manual QA can't catch all the combinations."
isValidTransitionWWDC : 2025-266, 2024-10150, 2023-10149, 2025-256, 2022-10054
Docs : /swiftui/scenephase, /swiftui/scene, /swiftui/scenestorage, /swiftui/windowgroup, /observation/observable()
Skills : axiom-swiftui-architecture, axiom-swiftui-nav, axiom-swift-concurrency
Weekly Installs
99
Repository
GitHub Stars
606
First Seen
Jan 21, 2026
Security Audits
Gen Agent Trust HubPassSocketPassSnykPass
Installed on
opencode85
codex79
claude-code79
gemini-cli77
cursor76
github-copilot74
Kotlin Ktor 服务器模式指南:构建健壮 HTTP API 的架构与最佳实践
968 周安装
OpenClaw 环境安全审计员:一键扫描密钥泄露,审计沙箱配置,保障AI技能运行安全
135 周安装
OpenViking 记忆插件指南:AI助手长期记忆管理与自动上下文注入
135 周安装
ByteRover CLI - 上下文工程平台,为AI编码智能体自动管理项目知识库
135 周安装
Symfony API Platform序列化指南:合约设计、安全防护与渐进式披露
135 周安装
PostgreSQL只读查询技能 - 安全连接AI助手执行数据库查询,支持SSL加密与权限控制
135 周安装
Next.js服务端与客户端组件选择指南:TypeScript最佳实践与性能优化
135 周安装