A modern iOS application demonstrating clean architecture (MVVM-C), Swift Concurrency, modular design with Swift Package Manager, and best practices for scalable iOS development.
Three branches show progressive modernisation:
- UIKit + SwiftUI + Combine (iOS 15+) —
main - Pure SwiftUI + Combine (iOS 16+) —
navigation-stack- PR - Pure SwiftUI + Observation (iOS 17+) —
observation- PR
Android counterpart: Fun-Android.
| Home | Detail | Profile | Settings |
|---|---|---|---|
![]() |
![]() |
![]() |
![]() |
Three branches demonstrate progressive modernisation — same app, three architectural approaches. Choose based on your minimum iOS target. All three produce visually identical apps.
main |
navigation-stack |
observation |
|
|---|---|---|---|
| Best for | iOS 15+ | ||
| UI framework | UIKit + SwiftUI | SwiftUI |
← same |
| Reactive | Combine | ← same | AsyncSequence |
| ViewModel | ObservableObject + @Published |
← same | @Observable macro |
| View binding | @ObservedObject |
← same | @Bindable / @State |
| Service events | AnyPublisher + Subject |
← same | AsyncStream + StreamBroadcaster |
| Architecture | MVVM + Coordinator | ← same | ← same |
| Coordinator → ViewModel | Closures | ← same | ← same |
| Language | Swift 6.0 | ← same | ← same |
| DI | Session-Scoped + Constructor Injection | ← same | ← same |
| LLM | Foundation Models (iOS 26+) | ← same | ← same |
| Testing | Swift Testing, swift-snapshot-testing | ← same | ← same |
| Aspect | main (UIKit + SwiftUI) |
navigation-stack / observation (Pure SwiftUI) |
|---|---|---|
| App entry point | AppDelegate + SceneDelegate |
SwiftUI @main App |
| Tab bar | UITabBarController subclass |
SwiftUI TabView |
| Navigation stack | UINavigationController |
NavigationStack + NavigationPath |
| Push navigation | pushViewController(_:animated:) |
path.append(item) |
| Modal presentation | present(_:animated:) |
.sheet(isPresented:) |
| Views | SwiftUI hosted in UIHostingController |
Native SwiftUI views |
| View controllers | UIKit VCs wrap SwiftUI views | None |
| Coordinators | Multiple TabCoordinators |
Single AppCoordinator (ObservableObject) |
| Deep links | scene(_:openURLContexts:) |
.onOpenURL { } |
| Transition control | Full (UINavigationControllerDelegate) |
Limited (no custom transition API) |
The three branches are visually identical, but architectural differences produce minor behavioural variations:
| Behaviour | main (UIKit) |
navigation-stack / observation (SwiftUI) |
Why |
|---|---|---|---|
| Items tab first load | No loading spinner — data ready before tab appears | Brief loading spinner on first tap | UIKit coordinators are classes created eagerly at launch; SwiftUI view structs (and their @StateObject/@State ViewModels) are created lazily on first render |
| Share sheet position | Bottom sheet (native UIActivityViewController) |
Popover anchored to toolbar button | ShareLink in a ToolbarItem presents as a popover on iPhone — Apple controls this internally; no SwiftUI modifier can force bottom-sheet without import UIKit |
| Aspect | main / navigation-stack (Combine) |
observation (AsyncSequence) |
|---|---|---|
| Service publisher | AnyPublisher<Set<String>, Never> |
AsyncStream<Set<String>> |
| Multi-consumer | CurrentValueSubject / PassthroughSubject |
StreamBroadcaster (custom, in Core) |
| Subscribe | .sink { }.store(in: &cancellables) |
Task { for await value in stream { } } |
| Lifecycle cleanup | Set<AnyCancellable> + cancellables = [] |
Task cancellation (task.cancel()) |
| Debounced search | .debounce(for:scheduler:) operator |
didSet + Task.sleep with cancellation |
| Initial value | @Published emits on subscribe |
Read property directly, stream emits future changes |
| ViewModel observation | ObservableObject(per-object invalidation) |
@Observable(per-property tracking) |
Fun-iOS/
├── FunApp/ # iOS app target (Xcode project)
├── Coordinator/ # Navigation coordinators
├── UI/ # SwiftUI views & UIKit controllers
├── ViewModel/ # Business logic (MVVM)
├── Model/ # Data models & protocols
├── Services/ # Concrete service implementations
└── Core/ # Utilities, DI container, L10n
All modules except FunApp are Swift packages. FunApp is the Xcode project that consumes them.
Dependency Hierarchy:
Modules only import from layers below them.
┌─────────────────────────────────────────┐
│ FunApp │
├──────────┬──────────────────────────────┤
│ │ Coordinator │
│ ├──────────────────────────────┤
│ Services │ UI │
│ ├──────────────────────────────┤
│ │ ViewModel │
├──────────┴──────────────────────────────┤
│ Model │
├─────────────────────────────────────────┤
│ Core │
└─────────────────────────────────────────┘
| Module | Direct Dependencies |
|---|---|
| Core | — |
| Model | Core |
| ViewModel | Model, Core |
| Services | Model, Core |
| UI | ViewModel, Model, Core |
| Coordinator | UI, ViewModel, Model, Core |
| FunApp | All 6 |
- Model: Data models, protocols, domain logic
- ViewModel: Business logic, state management
- View: Pure UI (SwiftUI)
- Coordinator: Navigation flow, screen transitions
Each app flow gets its own session with a dedicated set of services. When the flow changes, the old session tears down and a fresh one activates — no stale state leaks between login and main.
LoginSession: logger, network, featureToggles
AuthenticatedSession: logger, network, featureToggles, favourites, toast, ai
// Each session owns its own ServiceLocator — released on session transition
protocol Session: AnyObject, ServiceLocatorProvider {
func activate() // register services
func teardown() // clean up session state
}
// @Service resolves from session.serviceLocator automatically
// via SessionProvider protocol extension — no global singleton
class HomeViewModel: ObservableObject, SessionProvider {
let session: Session
@Service(.network) private var networkService: NetworkServiceProtocol
init(session: Session) {
self.session = session
}
}The current @Service property wrapper uses static subscript(_enclosingInstance:) to resolve from the enclosing instance's serviceLocator. This eliminates the global singleton (ServiceLocator.shared) — each Session creates its own ServiceLocator, and coordinators/ViewModels receive the current session's locator via constructor injection. On session transition, the old locator is released with the session.
Future: A Swift Macro could auto-generate ServiceLocatorProvider conformance + the serviceLocator stored property, eliminating the remaining boilerplate. On @Observable classes it could also auto-add @ObservationIgnored to each @Service property.
All services are defined as protocols in Model, implementations in Services.
AppCoordinator
├── LoginCoordinator
├── HomeCoordinator (detail + profile screens)
├── ItemsCoordinator (detail screens)
└── SettingsCoordinator
3 tab coordinators handle all screens in their navigation stack directly. ViewModels communicate via closures (onShowDetail, onShowProfile, onPop, onShare, onDismiss, onLogin) — no coordinator protocols.
AppCoordinator manages login/main flow transitions with the session lifecycle.
URL scheme funapp:// for navigation:
funapp://tab/items- Switch to Items tabfunapp://item/swiftui- Open item detailfunapp://profile- Open profile
Test from terminal:
xcrun simctl openurl booted "funapp://tab/items"
xcrun simctl openurl booted "funapp://item/swiftui"
xcrun simctl openurl booted "funapp://profile"Deep links received during login are queued and executed after authentication.
- Session-Scoped DI: Clean service lifecycle per app flow — no stale state
- Reactive Data Flow: Combine framework with
@Publishedproperties - Feature Toggles: Runtime flags persisted via services
- AI Summary: On-device LLM summarisation using Apple Intelligence / Foundation Models (iOS 26+)
- Error Handling: Centralised
AppErrorenum with toast notifications - Modern Search: Debounced input, loading states
- Pull-to-Refresh: Native SwiftUI
.refreshable - Dark Mode & Dynamic Type: System-adaptive colours, semantic font styles, System/Light/Dark appearance picker
- iOS 17+ APIs: Symbol effects, sensory feedback (backwards compatible)
- Unit Tests: ViewModels, services, and session lifecycle
- Session DI Tests: Activation, teardown, transitions, state isolation
- Snapshot Tests: Visual regression testing for all views
- Parameterized Tests: Swift Testing with custom scenarios
- Xcode 16.0+
- iOS 15.0+
- Swift 6.0
git clone https://github.com/g-enius/Fun-iOS.git
cd Fun-iOS
open Fun.xcworkspace- Open
Fun.xcworkspace - Select
FunAppscheme - Choose a simulator (iPhone 17 Pro recommended)
Cmd + Rto build and run
xcodebuild test -workspace Fun.xcworkspace -scheme FunApp \
-destination 'platform=iOS Simulator,name=iPhone 17 Pro'- SwiftLint with strict rules (no force unwraps)
- GitHub Actions CI (lint, build, test)
- OSLog structured logging
- SwiftGen for type-safe localisation
This project demonstrates AI-assisted iOS development using Claude Code with project-level configuration for team-shareable guardrails, branch-aware rules, and custom workflows.
Architecture and patterns designed by the developer. Claude Code assists with feature implementation, bug fixes, testing, cross-platform parity checks, and code review — guided by project-level rules that enforce the architecture.
Commits with AI assistance include Co-Authored-By: Claude attribution.
.claude/
├── settings.json # Team-shared permissions (auto-approve build/test/lint)
├── skills/
│ ├── review/SKILL.md # /review — architecture + similar-pattern search
│ ├── fix-issue/SKILL.md # /fix-issue — end-to-end GitHub issue workflow
│ ├── cross-platform/SKILL.md # /cross-platform — iOS vs Android parity check
│ ├── pull-request/SKILL.md # /pull-request — draft PR with tests + accessibility
│ └── sync/SKILL.md # /sync — rebase feature branches onto main with AI conflict resolution
└── agents/
└── change-reviewer.md # Branch-aware code review agent
CLAUDE.md # Architecture rules, anti-patterns, build commands
ai-rules/
├── general.md # MVVM-C patterns, DI, sessions, testing reference
├── swift-style.md # Swift 6 concurrency, naming, reactive patterns
└── ci-cd.md # GitHub Actions CI workflow patterns
Branch-aware: Each branch has its own CLAUDE.md and ai-rules/ adapted for that branch's architecture. The change-reviewer agent knows which patterns to enforce — e.g., flagging import Combine on the observation branch, or import UIKit on the SwiftUI branches.
Multi-branch workflow: Shared changes commit to main first, then feature branches rebase — enforced via project-level rules. The /sync skill and scripts/sync-branches.sh automate this: push main, rebase both feature branches, force-push, with retry logic for Xcode index.lock contention. When conflicts arise, /sync resolves them with AI.
Cross-platform: The /cross-platform skill compares iOS and Android implementations to catch unintentional UI/behavior divergences.
MIT License





