VibeCoding工程流程学习二:iOS项目架构

242 阅读7分钟

前言

构建一个可以复用的iOS项目结构,要求方便测试方便扩展,组件化,主要构建思想为clean architecure, swiftject 进行解耦,通过spm 拆分组件。详细项目地址为Demo

🛠 技术架构

TechnologyPurpose
Swift 5Language
SwiftUIUI Framework (iOS 15+)
Swift Package ManagerModular dependency management
RxSwift 6.x / RxCocoaReactive programming (Domain, Data, Networking)
CombineReactive UI bindings (Presentation layer only)
Swinject 2.xDependency Injection container
WKWebViewWeb content embedding + JS bridge
URLSessionHTTP networking (via RxSwift wrappers)

EcommerceAppDemo 🛍️

Platform Language Architecture SPM

A production-grade iOS e-commerce application showcasing Clean Architecture with Swift Package Manager modularization. Built with SwiftUI, RxSwift, and Swinject, featuring a Unified Routing System for type-safe navigation and a WebContainer Bridge for JavaScript ↔ Native communication.


📋 Table of Contents


🏗 Architecture Overview

Presentation/Features  →  Domain  →  Abstraction  ←  Data
        │                                           ↑
        │                    ┌──────────────────────┘
        └──── Utilities ─────┘

This project follows Clean Architecture with strict dependency inversion: outer layers depend on inner layers, never the reverse. The innermost layer (Abstraction) contains only protocols — zero implementation details, zero project-internal dependencies.

Layer Dependency Rules

LayerDepends On
Presentation (SwiftUI Features)Domain protocols, Abstraction, Utilities
Domain (Use Cases)Abstraction protocols only, RxSwift
Data (Repositories + Services)Abstraction protocols, Networking
Abstraction (Pure Protocols)Swinject, RxSwift — zero project dependencies
Utilities (Networking / Utils / Analytics / PresentationCore)Independent utilities with minimal external deps

✨ Key Features

  • Product Browsing — Browse a list of products with detailed views
  • Shopping Basket — Add items to basket with quantity controls, view totals
  • User Login — Simple username-based login flow
  • Unified Routing System — Type-safe, protocol-driven navigation with AppRoute + RouterProtocol supporting push/present, modal styles, custom transitions, navigation bar visibility, tab bar hiding, and per-page title styling
  • Auto Page Lifecycle AnalyticsBaseHostingController automatically tracks page dwell time without any boilerplate
  • WebContainer Bridge — Embed web content and enable JS ↔ Native bidirectional communication via a rule-based routing system with wildcard matching and present fallback
  • Dynamic Route Dispatch — Type any VC route name in the WebTest page and navigate to it dynamically; unknown routes show a native alert
  • Analytics Tracking — Event tracking pipeline from the app through to a backend API
  • Dev/Prod Environment Switching — Seamless switch between mock/localhost and production API via launch arguments
  • Mock API Layer — Fully functional mock API provider with realistic product data for offline development

🛠 Tech Stack

TechnologyPurpose
Swift 5Language
SwiftUIUI Framework (iOS 15+)
Swift Package ManagerModular dependency management
RxSwift 6.x / RxCocoaReactive programming (Domain, Data, Networking)
CombineReactive UI bindings (Presentation layer only)
Swinject 2.xDependency Injection container
WKWebViewWeb content embedding + JS bridge
URLSessionHTTP networking (via RxSwift wrappers)
UINavigationBarAppearancePer-page navigation bar styling (iOS 13+)

📁 Project Structure

MyEcommerce/
├── MyEcommerce/                          # App entry point
│   ├── MyEcommerceApp.swift              # @main — DI registration + navigation
│   ├── Routing/
│   │   ├── AppWebRouteFactory.swift      # Composition root for WebContainer routing (legacy)
│   │   └── AppRouteFactoryRegistrar.swift # New routing: register all feature route factories
│   └── Assets.xcassets/                  # App icons, colors, resources
│
├── Packages/                              # ★ All business logic as SPM packages
│   ├── Abstraction/                      # Pure protocol layer
│   │   ├── ProductAbstraction
│   │   ├── BasketAbstraction
│   │   ├── UserAbstraction
│   │   ├── AnalyticsAbstraction
│   │   ├── DIAbstraction
│   │   ├── RoutingAbstraction            # ★ AppRoute, RouterProtocol, RouteFactoryProtocol, etc.
│   │   └── WebContainerAbstraction
│   │
│   ├── Domain/                           # Use case implementations
│   │   ├── ProductDomain / BasketDomain / UserDomain
│   │   ├── AnalyticsDomain
│   │   ├── RoutingDomain                 # ★ NavigateUseCase
│   │   └── WebContainerDomain
│   │
│   ├── Data/                             # Repository + service implementations
│   │   ├── ProductData / BasketData / UserData
│   │   ├── RoutingData                   # ★ AppRouter, RouteFactoryRegistry, TransitioningCoordinator
│   │   └── WebContainerData
│   │
│   ├── Presentation/                     # SwiftUI feature packages
│   │   ├── LoginFeature                  # LoginView + LoginRoute + LoginRouteFactory
│   │   ├── ProductsFeature               # ProductList + ItemDetail + ProductRoute + ProductRouteFactory
│   │   ├── BasketFeature                 # BasketView + BasketRoute + BasketRouteFactory
│   │   └── WebContainerFeature           # WKWebView + JS bridge + WebContainerRoute + WebContainerRouteFactory
│   │
│   └── Utilities/                        # Cross-cutting utilities
│       ├── Networking/API                # HTTP client, mock provider, environment
│       ├── Utils                         # RxSwift → Combine bridge
│       ├── Analytics                     # Event tracking wrapper
│       └── PresentationCore              # ★ BaseHostingController, BaseNavigationController
│
├── MyEcommerceTests/
├── MyEcommerceUITests/
├── CLAUDE.md
├── docs/
│   ├── architecture.md
│   ├── specs/                            # 8 stage specification documents
│   └── plans/                            # Implementation plans
│
└── .claude/

Package Count

12 Package.swift files producing 20+ SPM targets across ~130 source files.


🔍 Layer Details

1. Abstraction Layer

Innermost layer — defines the contracts for the entire application. Contains only protocols with zero implementation logic.

Sub-moduleKey Protocols
ProductAbstractionProductRepositoryProtocol, GetProductsUseCaseProtocol, ProductDomainModelProtocol
BasketAbstractionBasketRepositoryProtocol, AddProductUseCaseProtocol, GetBasketUseCaseProtocol
UserAbstractionUserRepositoryProtocol, LoginUserUseCaseProtocol
AnalyticsAbstractionAnalyticsWrapperProtocol, SendProductDetailAnalyticsDataUsecaseProtocol, TrackPageLifecycleUseCaseProtocol
RoutingAbstractionAppRoute (marker), RouterProtocol (navigate/goBack), RouteFactoryProtocol (create VCs), PageLifecycleTrackable (analytics), + configuration types (RouteConfiguration, RoutePresentationStyle, RouteModalStyle, RouteTransition, RouteBackButtonConfiguration, RouteBarVisibilityConfiguration, RouteTitleConfiguration)
WebContainerAbstractionLoadWebContentUseCaseProtocol, ProcessBridgeCommandUseCaseProtocol, WebRouteFactoryProtocol (legacy), models for WebContent, WebBridgeCommand, NativeBridgeAction
DIAbstractionDIContainer — thin wrapper around Swinject's Container singleton

2. Domain Layer

Business logic layer — implements use case protocols from Abstraction by orchestrating repository calls.

Sub-moduleUse Cases
ProductDomainGetProductsUseCase — fetches all products via repository
BasketDomainAddProductUseCase — adds product to basket; GetBasketUseCase — fetches user's basket
UserDomainLoginUserUseCase — creates/authenticates user
AnalyticsDomainSendProductDetailAnalyticsDataUseCase — tracks product view events; TrackPageLifecycleUseCase — tracks page dwell time
RoutingDomainNavigateUseCase — navigation orchestration with pre-check hooks, delegates to RouterProtocol
WebContainerDomainLoadWebContentUseCase — resolves WebContent to load instructions; ProcessBridgeCommandUseCase — matches bridge commands against rules to produce native actions

Each domain sub-module includes a DI/ directory that registers its use cases into DIContainer.

3. Data Layer

Concrete implementations of repository and service protocols. Contains DTOs, domain model mappings, and network service calls.

Sub-moduleStructure
ProductDataProductRepositoryProductServiceAPIProvider (GET /products)
BasketDataBasketRepositoryBasketServiceAPIProvider (POST /basket/add, GET /basket/view/{userId})
UserDataUserRepositoryUserServiceAPIProvider (POST /users)
RoutingDataAppRouter — implements RouterProtocol with push/present, navigation metadata stack, system + custom transitions, bar visibility, per-page title styling, back button config; RouteFactoryRegistry — aggregates RouteFactoryProtocol instances; TransitioningCoordinator — bridges custom animations to UIKit; FadeScaleAnimator — example custom animation
WebContainerDataWebContentRepositoryImpl — resolves WebContent enum; WebBridgeRuleRepositoryImpl — thread-safe bridge rule store with 7 initial rules

Each module contains: DTO/ (Codable), DomainModel/ (concrete models), Service/ (network calls), Repository/ (protocol implementations), DI/ (registration).

4. Presentation Layer

SwiftUI feature packages — each is independently buildable with its own Package.swift. Each feature now includes a Route/ directory with route enum + factory for the new routing system.

FeatureViewsRouteFactory
LoginFeatureLoginViewLoginRoute.loginLoginRouteFactoryBaseHostingController(LoginView)
ProductsFeatureProductListView, ItemDetailViewProductRoute.productList(userId:) / .productDetail(productId:userId:)ProductRouteFactory
BasketFeatureBasketViewBasketRoute.basket(userId:)BasketRouteFactoryBaseHostingController(BasketView)
WebContainerFeatureWebContainerView, WebTestEntryView, WebTestNativeProbeViewWebContainerRoute.webTestEntry / .webTestNativeProbeWebContainerRouteFactoryBaseHostingController

UI Layer: SwiftUI with ObservableObject ViewModels. RxSwift Observable streams are bridged to Combine Publisher via Utils.asPublisher(), then assign(to: \.published, on: self) drives @Published properties.

5. Utilities Layer

PackageDescription
Networking/APIAPIProvider (URLSession + RxSwift), APIRequest protocol with defaults, APIResponse with parsing, MockAPIProvider with realistic canned data (10 mock products), environment-aware APIConstants
UtilsObservable.asPublisher() — bridges RxSwift Observable → Combine AnyPublisher
AnalyticsAnalyticsWrapper — sends events to POST /analytics/event, prints results to console
PresentationCoreBaseHostingController — UIHostingController subclass with auto page-dwell-time tracking; BaseNavigationController — UINavigationController subclass with unified nav bar appearance

🧭 Unified Routing System

The project includes a complete 8-stage routing abstraction built across Layers:

Architecture

AppRoute (marker protocol)
    │
    ├── ProductRoute / BasketRoute / LoginRoute / WebContainerRoute
    │       │
    ▼       ▼
RouteFactoryRegistry ──iterates──→ RouteFactoryProtocol.canHandle()
    │                                     │
    │                          makeViewController(for:)
    │                                     │
    ▼                                     ▼
AppRouter (RouterProtocol)         BaseHostingController<View>
    │
    ├── navigate(to:configuration:)
    │     ├── .push → UINavigationController.pushViewController
    │     │         + navigation metadata stack (for smart goBack)
    │     │         + CATransition (system animations)
    │     │         + TransitioningCoordinator (custom animations)
    │     │         + bar visibility + title style + back button config
    │     │
    │     └── .present → UIModalPresentationStyle
    │                   + modalTransitionStyle (system animations)
    │                   + TransitioningCoordinator (custom animations)
    │
    └── goBack(animated:)
          ├── from push → popViewController
          └── from present → dismiss

RouteConfiguration — "All nil = system defaults"

FieldTypePurpose
presentationStyleRoutePresentationStyle?.push / .present(modal:)
transitionRouteTransition?.systemDefault / .system(fade|slide|flip) / .custom(animator)
backButtonRouteBackButtonConfiguration?.systemDefault / .hidden / .custom(title:image:)
barVisibilityRouteBarVisibilityConfiguration?hidesNavigationBar, hidesTabBar
titleConfigurationRouteTitleConfiguration?.text / .attributedText / .customTitleView

Flow

Feature RouteFactory  →  RouteFactoryRegistry  →  AppRouter
                                                     │
                                              NavigateUseCase
                                              (pre-check hooks)

Per-Feature Routes (8 route files)

FeatureRoute EnumFactory
LoginFeatureLoginRoute.loginLoginRouteFactory
ProductsFeatureProductRoute.productList(userId:), .productDetail(productId:userId:)ProductRouteFactory
BasketFeatureBasketRoute.basket(userId:)BasketRouteFactory
WebContainerFeatureWebContainerRoute.webTestEntry, .webTestNativeProbeWebContainerRouteFactory

🔄 Data Flow

User taps "Login"
  → LoginView calls loginViewModel.login(username:)
    → LoginViewModel calls LoginUserUseCaseProtocol.start(username:)
      → LoginUserUseCase (Domain) calls UserRepositoryProtocol.addUser(username:)
        → UserRepository (Data) calls UserService.addUser(user:)
          → UserService calls APIProvider.perform(APIRequest)
            → URLSession POST to /users
  ← Observable<UserDomainModel> flows back
  ← asPublisher() bridges to Combine
  ← ViewModel.assign(to: \.userID) + assign(to: \.isConnected)
  ← SwiftUI observes @Published and presents TabView

Navigation Flow

LoginView.fullScreenCover (when isConnected)
  → TabView (TabRouter via @EnvironmentObject)
    ├── Products Tab (ProductListView → push → ItemDetailView)
    ├── Basket Tab (BasketView)
    └── WebTest Tab (WebTestEntryView → WebContainer)

Navigation state is managed by TabRouter (ObservableObject with @Published var screen) via @EnvironmentObject.


🌉 WebContainer Bridge

A standout feature enabling bidirectional JavaScript ↔ Native iOS communication through a rule-based middleware system.

How It Works

JS in web page: window.webkit.messageHandlers.nativeBridge.postMessage({action, target, params})
  → WKWebViewRepresentable → WebScriptMessageHandler
    → Parses JSON into WebBridgeCommand
    → WebContainerViewModel.handleBridgeCommand
      → ProcessBridgeCommandUseCase.execute(command:)
        → WebBridgeRuleRepository: matches command against registered rules
          (exact match by action+target first, then wildcard by action)
        → Returns matched NativeBridgeAction (with dynamic route if wildcard)
      → NativeBridgeRouter.dispatch(action:)           (legacy path)
        → WebRouteFactoryProtocol.makeViewController(route)
        → pushViewController / present

The legacy NativeBridgeRouter path coexists with the new RouterProtocol — both are independently registered and non-interfering.


💉 Dependency Injection

The project uses Swinject as its DI framework with a global singleton pattern:

DIContainer.shared (Swinject Container wrapper)
  ├── Registered by each module's static DI methods
  └── All registrations happen eagerly in MyEcommerceApp.init()

Each layer contributes registrations:

  1. API ProviderregisterAPIProvider()
  2. Servicesregister{Feature}Service()
  3. Repositoriesregister{Feature}Repository()
  4. Use Casesregister{Feature}UseCase()
  5. UtilitiesregisterAnalyticsWrapper(), registerPresentationCore()
  6. New RoutingregisterRouteFactoryRegistry(), registerAppRouter(), registerNavigateUseCase(), registerAllFeatureRouteFactories()
  7. WebContainer (legacy)WebRouteFactoryProtocol, NativeBridgeRouter, WebContainerViewModel

ViewModels resolve dependencies at runtime via DIContainer.shared.resolve().


🌍 Environment Switching

Supports Dev and Production environments, selected via launch argument:

AspectDevProduction
Launch arg-environment dev (default in DEBUG)-environment prd (default in RELEASE)
API Base URLhttp://localhost:8080https://api.myecoapp.com:443
API ProviderMockAPIProvider (canned JSON responses)Real APIProvider (URLSession)
Mock delayConfigurable (1s default)N/A

In DEBUG builds, the environment is read from the -environment launch argument. In RELEASE builds, it always defaults to .prd.


🚀 Getting Started

Prerequisites

  • Xcode 14+
  • iOS 15+ deployment target

Running the App

# Open the project in Xcode
xed .

# Build and run (⌘R)
# Uses Mock API by default in DEBUG mode — no backend required

Switching to Production

Edit the scheme in Xcode and add a launch argument:

-environment prd

Or, to test against a local server:

# Start a local HTTP server on port 8080
cd path/to/backend && swift run

Running Tests

# Run all tests for a specific package
cd Packages/Domain && swift test

# Run a specific test
swift test --filter RoutingDomainTests/testNavigateUseCaseForwardsRoute

🧩 Key Architecture Decisions

1. RxSwift ↔ Combine Bridge

RxSwift is used throughout the Domain and Data layers for reactive networking. At the ViewModel boundary (Presentation layer), Observable streams are bridged to Combine Publisher via Utils/Observable+Extension.swift:asPublisher(). This allowed the project to adopt modern SwiftUI/Combine patterns without rewriting the stable reactive networking layer.

2. Singleton DI with Eager Registration

All dependencies are registered eagerly in MyEcommerceApp.init() using a wrapping singleton (DIContainer.shared). This provides simple, predictable resolution at the cost of memory overhead — acceptable for an e-commerce app with relatively few services.

3. Unified Routing System (8 Stages)

A complete routing abstraction built incrementally:

  • Stage 1RoutingAbstraction: Protocols and configuration types (AppRoute, RouterProtocol, RouteConfiguration, etc.)
  • Stage 2RoutingDomain: NavigateUseCase with pre-check hooks, TrackPageLifecycleUseCase for analytics
  • Stage 3RoutingData: AppRouter with push/present + navigation metadata stack for smart goBack
  • Stage 4PresentationCore: BaseHostingController (auto page-dwell analytics), BaseNavigationController (unified nav bar)
  • Stage 5 — Transition animations: CATransition (system), TransitioningCoordinator (custom)
  • Stage 6 — Bar visibility: hidesBottomBarWhenPushed for synchronized tab bar hide/show
  • Stage 7 — Title styling: per-page UINavigationBarAppearance via navigationItem
  • Stage 8 — Feature migration: 4 route enums + 4 factories + App layer registrar

4. Enum-Based SPM Target Registration

All Package.swift files use a consistent enum-based CaseIterable pattern with typed dependency helpers (.internal(), .external(), .abstraction(), .utility(), .domain()), eliminating repetitive target declarations and keeping target definitions DRY.

5. Mock API First Development

The MockAPIProvider returns realistic canned data for 10 products (MacBook Pro, iPhone, AirPods, etc.) with configurable delay and error simulation. This enables full app development and UI testing without any backend dependency.

6. WebContainer with Deferred Routing (Legacy)

The WebContainer feature has zero knowledge of other features. Route-to-ViewController resolution is deferred to the App layer via WebRouteFactoryProtocol, keeping the WebContainer package independent and reusable.


📄 License

This project is open source and available under the MIT license.


Built with ❤️ using SwiftUI, RxSwift, and Clean Architecture