home / skills / thebushidocollective / han / uikit-architecture

This skill helps you implement UIKit architecture using MVVM, Coordinator, and dependency injection to build scalable iOS apps.

npx playbooks add skill thebushidocollective/han --skill uikit-architecture

Review the files below or copy the command above to add this skill to your agents.

Files (1)
SKILL.md
9.3 KB
---
name: ios-uikit-architecture
user-invocable: false
description: Use when building iOS apps with UIKit, implementing MVVM/MVC/Coordinator patterns, or integrating UIKit with SwiftUI.
allowed-tools:
  - Read
  - Write
  - Edit
  - Bash
  - Grep
  - Glob
---

# iOS - UIKit Architecture

Architectural patterns and best practices for UIKit-based iOS applications.

## Key Concepts

### MVVM Architecture

The Model-View-ViewModel pattern separates concerns:

- **Model**: Data and business logic
- **View**: UIViewController and UIView subclasses
- **ViewModel**: Presentation logic, transforms model data for display

```swift
// Model
struct User {
    let id: String
    let firstName: String
    let lastName: String
    let email: String
}

// ViewModel
class UserProfileViewModel {
    private let user: User

    var displayName: String {
        "\(user.firstName) \(user.lastName)"
    }

    var emailDisplay: String {
        user.email.lowercased()
    }

    init(user: User) {
        self.user = user
    }
}

// View
class UserProfileViewController: UIViewController {
    private let viewModel: UserProfileViewModel

    init(viewModel: UserProfileViewModel) {
        self.viewModel = viewModel
        super.init(nibName: nil, bundle: nil)
    }

    override func viewDidLoad() {
        super.viewDidLoad()
        nameLabel.text = viewModel.displayName
        emailLabel.text = viewModel.emailDisplay
    }
}
```

### Coordinator Pattern

Coordinators handle navigation flow, removing navigation logic from view controllers:

```swift
protocol Coordinator: AnyObject {
    var childCoordinators: [Coordinator] { get set }
    var navigationController: UINavigationController { get }
    func start()
}

class AppCoordinator: Coordinator {
    var childCoordinators: [Coordinator] = []
    var navigationController: UINavigationController

    init(navigationController: UINavigationController) {
        self.navigationController = navigationController
    }

    func start() {
        let vc = HomeViewController()
        vc.coordinator = self
        navigationController.pushViewController(vc, animated: false)
    }

    func showDetail(for item: Item) {
        let detailCoordinator = DetailCoordinator(
            navigationController: navigationController,
            item: item
        )
        childCoordinators.append(detailCoordinator)
        detailCoordinator.start()
    }
}
```

### Dependency Injection

Inject dependencies through initializers for testability:

```swift
protocol UserServiceProtocol {
    func fetchUser(id: String) async throws -> User
}

class UserViewController: UIViewController {
    private let userService: UserServiceProtocol
    private let userId: String

    init(userService: UserServiceProtocol, userId: String) {
        self.userService = userService
        self.userId = userId
        super.init(nibName: nil, bundle: nil)
    }

    required init?(coder: NSCoder) {
        fatalError("init(coder:) not supported")
    }
}
```

## Best Practices

### Programmatic UI with Auto Layout

```swift
class ProfileView: UIView {
    private let avatarImageView: UIImageView = {
        let imageView = UIImageView()
        imageView.contentMode = .scaleAspectFill
        imageView.clipsToBounds = true
        imageView.translatesAutoresizingMaskIntoConstraints = false
        return imageView
    }()

    private let nameLabel: UILabel = {
        let label = UILabel()
        label.font = .preferredFont(forTextStyle: .headline)
        label.translatesAutoresizingMaskIntoConstraints = false
        return label
    }()

    override init(frame: CGRect) {
        super.init(frame: frame)
        setupViews()
        setupConstraints()
    }

    private func setupViews() {
        addSubview(avatarImageView)
        addSubview(nameLabel)
    }

    private func setupConstraints() {
        NSLayoutConstraint.activate([
            avatarImageView.topAnchor.constraint(equalTo: topAnchor, constant: 16),
            avatarImageView.centerXAnchor.constraint(equalTo: centerXAnchor),
            avatarImageView.widthAnchor.constraint(equalToConstant: 80),
            avatarImageView.heightAnchor.constraint(equalToConstant: 80),

            nameLabel.topAnchor.constraint(equalTo: avatarImageView.bottomAnchor, constant: 12),
            nameLabel.centerXAnchor.constraint(equalTo: centerXAnchor),
        ])
    }
}
```

### Modern Collection Views with Diffable Data Source

```swift
class ItemListViewController: UIViewController {
    enum Section { case main }

    private var dataSource: UICollectionViewDiffableDataSource<Section, Item>!
    private var collectionView: UICollectionView!

    override func viewDidLoad() {
        super.viewDidLoad()
        configureCollectionView()
        configureDataSource()
    }

    private func configureCollectionView() {
        let config = UICollectionLayoutListConfiguration(appearance: .insetGrouped)
        let layout = UICollectionViewCompositionalLayout.list(using: config)
        collectionView = UICollectionView(frame: view.bounds, collectionViewLayout: layout)
        collectionView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
        view.addSubview(collectionView)
    }

    private func configureDataSource() {
        let cellRegistration = UICollectionView.CellRegistration<UICollectionViewListCell, Item> { cell, indexPath, item in
            var content = cell.defaultContentConfiguration()
            content.text = item.title
            content.secondaryText = item.subtitle
            cell.contentConfiguration = content
        }

        dataSource = UICollectionViewDiffableDataSource<Section, Item>(collectionView: collectionView) {
            collectionView, indexPath, item in
            collectionView.dequeueConfiguredReusableCell(using: cellRegistration, for: indexPath, item: item)
        }
    }

    func updateItems(_ items: [Item]) {
        var snapshot = NSDiffableDataSourceSnapshot<Section, Item>()
        snapshot.appendSections([.main])
        snapshot.appendItems(items)
        dataSource.apply(snapshot, animatingDifferences: true)
    }
}
```

### UIKit and SwiftUI Integration

Hosting SwiftUI in UIKit:

```swift
class SettingsViewController: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()

        let swiftUIView = SettingsView()
        let hostingController = UIHostingController(rootView: swiftUIView)

        addChild(hostingController)
        view.addSubview(hostingController.view)
        hostingController.view.frame = view.bounds
        hostingController.view.autoresizingMask = [.flexibleWidth, .flexibleHeight]
        hostingController.didMove(toParent: self)
    }
}
```

Wrapping UIKit in SwiftUI:

```swift
struct MapViewRepresentable: UIViewRepresentable {
    @Binding var region: MKCoordinateRegion

    func makeUIView(context: Context) -> MKMapView {
        let mapView = MKMapView()
        mapView.delegate = context.coordinator
        return mapView
    }

    func updateUIView(_ mapView: MKMapView, context: Context) {
        mapView.setRegion(region, animated: true)
    }

    func makeCoordinator() -> Coordinator {
        Coordinator(self)
    }

    class Coordinator: NSObject, MKMapViewDelegate {
        var parent: MapViewRepresentable

        init(_ parent: MapViewRepresentable) {
            self.parent = parent
        }
    }
}
```

## Common Patterns

### View Controller Lifecycle Management

```swift
class DataViewController: UIViewController {
    private var loadTask: Task<Void, Never>?

    override func viewDidLoad() {
        super.viewDidLoad()
        setupViews()
    }

    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)
        loadData()
    }

    override func viewDidDisappear(_ animated: Bool) {
        super.viewDidDisappear(animated)
        loadTask?.cancel()
    }

    private func loadData() {
        loadTask = Task {
            do {
                let data = try await fetchData()
                guard !Task.isCancelled else { return }
                updateUI(with: data)
            } catch {
                showError(error)
            }
        }
    }
}
```

### Memory Management with Closures

```swift
class NetworkViewController: UIViewController {
    private let networkService: NetworkService

    func fetchData() {
        // Use [weak self] to prevent retain cycles
        networkService.fetch { [weak self] result in
            guard let self else { return }

            switch result {
            case .success(let data):
                self.handleData(data)
            case .failure(let error):
                self.showError(error)
            }
        }
    }
}
```

## Anti-Patterns

### Massive View Controllers

Bad: Putting everything in one view controller.

Good: Extract into separate types:

- ViewModels for presentation logic
- Coordinators for navigation
- Custom views for UI components
- Services for network/data operations

### Storyboard Segue Spaghetti

Bad: Complex storyboard with many segues.

Good: Use coordinators with programmatic navigation.

### Force Casting Cells

Bad:

```swift
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell") as! CustomCell
```

Good:

```swift
guard let cell = tableView.dequeueReusableCell(withIdentifier: "Cell") as? CustomCell else {
    fatalError("Unable to dequeue CustomCell")
}
```

## Related Skills

- **ios-swiftui-patterns**: Modern declarative UI
- **ios-swift-concurrency**: Async data loading

Overview

This skill captures practical architectural patterns and best practices for building UIKit-based iOS apps. It focuses on MVVM, Coordinator-based navigation, dependency injection, programmatic UI, modern collection views, and UIKit<->SwiftUI integration. The guidance helps teams keep view controllers lean, improve testability, and adopt modern APIs like diffable data sources and Swift concurrency.

How this skill works

The skill inspects common UIKit app structure and recommends pattern-specific implementations: separate Models, Views, and ViewModels for presentation logic; use Coordinators to own navigation flows; and inject services via initializers for testability. It illustrates programmatic Auto Layout, diffable collection view data sources, lifecycle-aware async tasks, and both hosting SwiftUI inside UIKit and wrapping UIKit for SwiftUI use. It also highlights memory-management techniques to avoid retain cycles.

When to use it

  • Building or refactoring UIKit apps to reduce massive view controllers.
  • Implementing MVVM to move presentation logic out of view controllers.
  • Centralizing navigation with Coordinators for multi-flow apps.
  • Adopting modern collection views with diffable data sources.
  • Integrating existing UIKit screens with new SwiftUI components.
  • Improving testability through dependency injection.

Best practices

  • Prefer programmatic UI with Auto Layout and reusable custom views over large storyboards.
  • Keep ViewControllers thin: move business logic to ViewModels and network logic to services.
  • Use Coordinators to own navigation and manage child coordinator lifecycles.
  • Inject dependencies through initializers and depend on protocols for mocking.
  • Use UICollectionViewDiffableDataSource and compositional layouts for reliable list updates.
  • Cancel async tasks in lifecycle callbacks and capture self weakly in closures.

Example use cases

  • Refactor a screen that mixes networking, parsing, and UI updates into ViewModel + Service + ViewController.
  • Introduce an AppCoordinator to manage onboarding, authentication, and main app flows.
  • Replace table view updates with a diffable data source to get animated, correct updates.
  • Host a SwiftUI settings view inside an existing UIKit settings screen with UIHostingController.
  • Expose a legacy MKMapView to SwiftUI using UIViewRepresentable and a Coordinator.

FAQ

When should I choose MVC vs MVVM?

Use MVC for simple screens where separation overhead is unnecessary. Use MVVM when presentation logic grows or you need easier unit testing and clearer UI-state transformations.

How do Coordinators interact with ViewControllers?

Coordinators own navigation controllers and create/present view controllers. ViewControllers can hold a weak coordinator reference to delegate navigation actions back to the coordinator.