ios-mvvm-coordinator/SKILL.md
Implement MVVM-C (Model-View-ViewModel-Coordinator) pattern for iOS with navigation coordinators and decoupled view controllers. Use when implementing navigation flows, creating coordinators, decoupling views from navigation logic, or managing complex navigation hierarchies. Triggers on MVVM-C, Coordinator, navigation, Router, flow, NavigationStack, coordinator pattern, view model, presentation.
npx skillsauth add abanoub-ashraf/manus-skills-import ios-mvvm-coordinatorInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
4 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
You are an expert in MVVM-C architecture for iOS. When this skill activates, help implement proper navigation patterns using Coordinators.
| Problem | Coordinator Solution | |---------|---------------------| | Views know about navigation | Views only emit events | | Hard to change navigation flows | Change coordinator, not views | | Deep linking is complex | Coordinators handle all routing | | Testing navigation is hard | Test coordinators independently | | Massive view controllers | Navigation code extracted |
┌─────────────────────────────────────────────────────┐
│ Coordinator │
│ (Owns ViewModels, Handles Navigation) │
└─────────────────────────────────────────────────────┘
│ │
▼ ▼
┌─────────────────┐ ┌─────────────────┐
│ ViewModel │ │ ViewModel │
│ (Business Logic)│ │ (Business Logic)│
└─────────────────┘ └─────────────────┘
│ │
▼ ▼
┌─────────────────┐ ┌─────────────────┐
│ View │ │ View │
│ (UI Only) │ │ (UI Only) │
└─────────────────┘ └─────────────────┘
import SwiftUI
@MainActor
protocol Coordinator: AnyObject {
associatedtype RootView: View
var navigationPath: NavigationPath { get set }
@ViewBuilder func start() -> RootView
}
@Observable
@MainActor
final class AppCoordinator: Coordinator {
// MARK: - Navigation State
var navigationPath = NavigationPath()
// Child coordinators
var authCoordinator: AuthCoordinator?
var mainCoordinator: MainCoordinator?
// MARK: - App State
private(set) var isAuthenticated = false
// MARK: - Dependencies
private let dependencies: AppDependencies
init(dependencies: AppDependencies) {
self.dependencies = dependencies
}
// MARK: - Root View
@ViewBuilder
func start() -> some View {
if isAuthenticated {
mainFlow()
} else {
authFlow()
}
}
// MARK: - Flows
@ViewBuilder
private func authFlow() -> some View {
let coordinator = AuthCoordinator(
dependencies: dependencies,
onAuthenticated: { [weak self] in
self?.isAuthenticated = true
}
)
self.authCoordinator = coordinator
coordinator.start()
}
@ViewBuilder
private func mainFlow() -> some View {
let coordinator = MainCoordinator(
dependencies: dependencies,
onLogout: { [weak self] in
self?.isAuthenticated = false
}
)
self.mainCoordinator = coordinator
coordinator.start()
}
}
@Observable
@MainActor
final class HomeCoordinator: Coordinator {
// MARK: - Navigation
var navigationPath = NavigationPath()
// MARK: - Destinations
enum Destination: Hashable {
case productDetail(id: String)
case productList(category: String)
case userProfile(id: String)
case settings
}
// MARK: - Dependencies
private let dependencies: HomeDependencies
// MARK: - Callbacks
var onShowAuth: (() -> Void)?
init(dependencies: HomeDependencies) {
self.dependencies = dependencies
}
// MARK: - Navigation Actions
func showProductDetail(id: String) {
navigationPath.append(Destination.productDetail(id: id))
}
func showProductList(category: String) {
navigationPath.append(Destination.productList(category: category))
}
func showUserProfile(id: String) {
navigationPath.append(Destination.userProfile(id: id))
}
func showSettings() {
navigationPath.append(Destination.settings)
}
func pop() {
if !navigationPath.isEmpty {
navigationPath.removeLast()
}
}
func popToRoot() {
navigationPath.removeLast(navigationPath.count)
}
// MARK: - Root View
@ViewBuilder
func start() -> some View {
NavigationStack(path: $navigationPath) {
homeView()
.navigationDestination(for: Destination.self) { destination in
view(for: destination)
}
}
}
// MARK: - View Factory
@ViewBuilder
private func homeView() -> some View {
let viewModel = HomeViewModel(
getProductsUseCase: dependencies.getProductsUseCase,
onProductSelected: { [weak self] id in
self?.showProductDetail(id: id)
},
onCategorySelected: { [weak self] category in
self?.showProductList(category: category)
}
)
HomeView(viewModel: viewModel)
}
@ViewBuilder
private func view(for destination: Destination) -> some View {
switch destination {
case .productDetail(let id):
productDetailView(id: id)
case .productList(let category):
productListView(category: category)
case .userProfile(let id):
userProfileView(id: id)
case .settings:
settingsView()
}
}
@ViewBuilder
private func productDetailView(id: String) -> some View {
let viewModel = ProductDetailViewModel(
productID: id,
getProductUseCase: dependencies.getProductUseCase,
onBuyTapped: { [weak self] _ in
// Handle purchase flow
},
onBack: { [weak self] in
self?.pop()
}
)
ProductDetailView(viewModel: viewModel)
}
@ViewBuilder
private func productListView(category: String) -> some View {
let viewModel = ProductListViewModel(
category: category,
getProductsUseCase: dependencies.getProductsUseCase,
onProductSelected: { [weak self] id in
self?.showProductDetail(id: id)
}
)
ProductListView(viewModel: viewModel)
}
@ViewBuilder
private func userProfileView(id: String) -> some View {
let viewModel = UserProfileViewModel(
userID: id,
getUserUseCase: dependencies.getUserUseCase
)
UserProfileView(viewModel: viewModel)
}
@ViewBuilder
private func settingsView() -> some View {
let viewModel = SettingsViewModel(
onLogout: { [weak self] in
self?.onShowAuth?()
}
)
SettingsView(viewModel: viewModel)
}
}
@Observable
@MainActor
final class HomeViewModel {
// MARK: - State
private(set) var products: [Product] = []
private(set) var categories: [Category] = []
private(set) var isLoading = false
private(set) var error: Error?
// MARK: - Dependencies
private let getProductsUseCase: GetProductsUseCaseProtocol
// MARK: - Navigation Callbacks
var onProductSelected: ((String) -> Void)?
var onCategorySelected: ((String) -> Void)?
init(
getProductsUseCase: GetProductsUseCaseProtocol,
onProductSelected: ((String) -> Void)? = nil,
onCategorySelected: ((String) -> Void)? = nil
) {
self.getProductsUseCase = getProductsUseCase
self.onProductSelected = onProductSelected
self.onCategorySelected = onCategorySelected
}
// MARK: - Actions
func loadProducts() async {
isLoading = true
do {
products = try await getProductsUseCase.execute()
} catch {
self.error = error
}
isLoading = false
}
func selectProduct(_ id: String) {
onProductSelected?(id)
}
func selectCategory(_ category: String) {
onCategorySelected?(category)
}
}
struct HomeView: View {
@State var viewModel: HomeViewModel
var body: some View {
List {
Section("Categories") {
ForEach(viewModel.categories) { category in
Button(category.name) {
viewModel.selectCategory(category.id)
}
}
}
Section("Products") {
ForEach(viewModel.products) { product in
ProductRow(product: product) {
viewModel.selectProduct(product.id)
}
}
}
}
.task {
await viewModel.loadProducts()
}
}
}
// View doesn't know HOW navigation happens - just calls viewModel
@Observable
@MainActor
final class MainTabCoordinator: Coordinator {
// MARK: - State
var selectedTab: Tab = .home
enum Tab: Int, CaseIterable {
case home
case search
case favorites
case profile
}
// MARK: - Child Coordinators
private(set) var homeCoordinator: HomeCoordinator
private(set) var searchCoordinator: SearchCoordinator
private(set) var favoritesCoordinator: FavoritesCoordinator
private(set) var profileCoordinator: ProfileCoordinator
// MARK: - Navigation Path (for deep linking)
var navigationPath = NavigationPath()
private let dependencies: AppDependencies
init(dependencies: AppDependencies) {
self.dependencies = dependencies
self.homeCoordinator = HomeCoordinator(dependencies: dependencies.home)
self.searchCoordinator = SearchCoordinator(dependencies: dependencies.search)
self.favoritesCoordinator = FavoritesCoordinator(dependencies: dependencies.favorites)
self.profileCoordinator = ProfileCoordinator(dependencies: dependencies.profile)
}
@ViewBuilder
func start() -> some View {
TabView(selection: $selectedTab) {
homeCoordinator.start()
.tabItem {
Label("Home", systemImage: "house")
}
.tag(Tab.home)
searchCoordinator.start()
.tabItem {
Label("Search", systemImage: "magnifyingglass")
}
.tag(Tab.search)
favoritesCoordinator.start()
.tabItem {
Label("Favorites", systemImage: "heart")
}
.tag(Tab.favorites)
profileCoordinator.start()
.tabItem {
Label("Profile", systemImage: "person")
}
.tag(Tab.profile)
}
}
// MARK: - Deep Linking
func handleDeepLink(_ url: URL) {
guard let components = URLComponents(url: url, resolvingAgainstBaseURL: false) else {
return
}
switch components.path {
case "/product":
if let id = components.queryItems?.first(where: { $0.name == "id" })?.value {
selectedTab = .home
homeCoordinator.showProductDetail(id: id)
}
case "/profile":
selectedTab = .profile
case "/search":
if let query = components.queryItems?.first(where: { $0.name == "q" })?.value {
selectedTab = .search
searchCoordinator.search(query: query)
}
default:
break
}
}
}
@Observable
@MainActor
final class HomeCoordinator {
// ... navigation path ...
// MARK: - Sheet State
var presentedSheet: Sheet?
enum Sheet: Identifiable {
case addProduct
case editProduct(Product)
case filter(current: FilterOptions)
var id: String {
switch self {
case .addProduct: return "add"
case .editProduct(let p): return "edit-\(p.id)"
case .filter: return "filter"
}
}
}
// MARK: - Sheet Actions
func showAddProduct() {
presentedSheet = .addProduct
}
func showEditProduct(_ product: Product) {
presentedSheet = .editProduct(product)
}
func showFilter(current: FilterOptions) {
presentedSheet = .filter(current: current)
}
func dismissSheet() {
presentedSheet = nil
}
// MARK: - View
@ViewBuilder
func start() -> some View {
NavigationStack(path: $navigationPath) {
homeView()
.sheet(item: $presentedSheet) { sheet in
sheetView(for: sheet)
}
}
}
@ViewBuilder
private func sheetView(for sheet: Sheet) -> some View {
switch sheet {
case .addProduct:
AddProductView(
viewModel: AddProductViewModel(
onSave: { [weak self] _ in
self?.dismissSheet()
},
onCancel: { [weak self] in
self?.dismissSheet()
}
)
)
case .editProduct(let product):
EditProductView(
viewModel: EditProductViewModel(
product: product,
onSave: { [weak self] _ in
self?.dismissSheet()
},
onCancel: { [weak self] in
self?.dismissSheet()
}
)
)
case .filter(let current):
FilterView(
viewModel: FilterViewModel(
initialOptions: current,
onApply: { [weak self] options in
self?.applyFilter(options)
self?.dismissSheet()
}
)
)
}
}
}
@main
struct MyApp: App {
@State private var appCoordinator: AppCoordinator
init() {
let dependencies = AppDependencies()
_appCoordinator = State(initialValue: AppCoordinator(dependencies: dependencies))
}
var body: some Scene {
WindowGroup {
appCoordinator.start()
.onOpenURL { url in
appCoordinator.handleDeepLink(url)
}
}
}
}
@MainActor
final class HomeCoordinatorTests: XCTestCase {
var sut: HomeCoordinator!
var mockDependencies: MockHomeDependencies!
override func setUp() {
mockDependencies = MockHomeDependencies()
sut = HomeCoordinator(dependencies: mockDependencies)
}
func test_showProductDetail_appendsToPath() {
// When
sut.showProductDetail(id: "123")
// Then
XCTAssertEqual(sut.navigationPath.count, 1)
}
func test_popToRoot_clearsPath() {
// Given
sut.showProductDetail(id: "1")
sut.showProductDetail(id: "2")
XCTAssertEqual(sut.navigationPath.count, 2)
// When
sut.popToRoot()
// Then
XCTAssertEqual(sut.navigationPath.count, 0)
}
func test_handleDeepLink_product_navigatesCorrectly() {
// Given
let url = URL(string: "myapp://product?id=456")!
// When
sut.handleDeepLink(url)
// Then
XCTAssertEqual(sut.navigationPath.count, 1)
}
}
App/
├── Coordinators/
│ ├── AppCoordinator.swift
│ ├── MainTabCoordinator.swift
│ └── Features/
│ ├── HomeCoordinator.swift
│ ├── SearchCoordinator.swift
│ └── ProfileCoordinator.swift
├── Features/
│ ├── Home/
│ │ ├── HomeView.swift
│ │ └── HomeViewModel.swift
│ └── Profile/
│ ├── ProfileView.swift
│ └── ProfileViewModel.swift
└── MyApp.swift
development
Design principles for building polished, native-feeling SwiftUI apps and widgets. Use this skill when creating or modifying SwiftUI views, iOS widgets (WidgetKit), or any native Apple UI. Ensures proper spacing, typography, colors, and widget implementations that look and feel like quality apps rather than AI-generated slop.
data-ai
Design and implement SwiftUI views, components, and app architecture. Use when creating new SwiftUI views, implementing MVVM/TCA patterns, managing state with @Observable, @State, @Binding, or @Environment, designing navigation flows, or structuring iOS app architecture. Triggers on SwiftUI, view model, state management, navigation, coordinator pattern.
development
Implement, review, or improve SwiftUI animations and transitions. Use when adding implicit or explicit animations with withAnimation, configuring spring animations (.smooth, .snappy, .bouncy), building phase or keyframe animations with PhaseAnimator/KeyframeAnimator, creating hero transitions with matchedGeometryEffect or matchedTransitionSource, adding SF Symbol effects (bounce, pulse, variableColor, breathe, rotate, wiggle), implementing custom Transition or CustomAnimation types, or ensuring animations respect accessibilityReduceMotion.
testing
Audit SwiftUI views for accessibility (iOS + macOS) with patch-ready fixes