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 modernization:
- UIKit + SwiftUI + Combine (iOS 15+) β
main - Pure SwiftUI + Combine (iOS 16+) β
navigation-stack- PR - Pure SwiftUI + AsyncSequence (iOS 17+) β
async-sequence- PR
Android counterpart: Fun-Android.
| Home | Detail | Profile | Settings |
|---|---|---|---|
![]() |
![]() |
![]() |
![]() |
Three branches demonstrate progressive modernization β same app, three architectural approaches. Choose based on your minimum iOS target. All three produce visually identical apps.
main |
navigation-stack |
async-sequence |
|
|---|---|---|---|
| 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 + @Service | β same | β same |
| LLM | Foundation Models (iOS 26+) | β same | β same |
| Testing | Swift Testing, swift-snapshot-testing | β same | β same |
| Aspect | main (UIKit + SwiftUI) |
navigation-stackΒ /Β async-sequenceΒ (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 / async-sequence (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) |
async-sequence (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, favorites, toast, ai
// Sessions activate/teardown automatically on flow transitions
protocol Session: AnyObject {
func activate() // register services
func teardown() // reset ServiceLocator
}
// ViewModels resolve lazily β no changes needed
@Service(.network) var networkService: NetworkServiceAll services 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 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: Centralized
AppErrorenum with toast notifications - Modern Search: Debounced input, loading states
- Pull-to-Refresh: Native SwiftUI
.refreshable - Dark Mode & Dynamic Type: System-adaptive colors, 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 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 localization
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 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 async-sequence 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





