Skip to content
Prepare to Grow

iOS Architecture

Modular Architecture and Swift Packages

Use Swift package targets and access control to enforce valuable boundaries without creating an expensive module graph.

Advanced6 min read2 guided activities

Before you begin

What you will understand

  • Distinguish packages, products, targets, and modules.
  • Design an acyclic dependency graph.
  • Control public, package, internal, and private API surfaces.
  • Choose module boundaries using measurable benefits and costs.
01

Learning stage

Concept Explanation

A Swift package groups modules developed together. Each target is compiled as a module or test suite, and library products expose selected targets to clients. A local package can remain in the application repository while giving Xcode and the compiler a real dependency boundary.

Modules enforce visibility and dependency direction. Keep APIs small: internal remains the default inside a module, package access can share implementation across targets in the same package, and public exposes declarations to importing modules. A public API creates compatibility and maintenance obligations even when every module lives in one repository.

Design the graph so feature modules depend on stable lower-level capabilities or feature APIs rather than one another's implementation. Avoid cycles and umbrella modules that import everything. Measure incremental build behavior, test startup, preview ergonomics, resource handling, and developer navigation before and after modularization; more modules are not automatically faster or cleaner.

  • Package: group of targets developed and resolved together.
  • Target: build unit that produces a module or test suite.
  • Product: library or executable exposed by a package.
  • Module: importable unit and access-control boundary.
  • Public API: declarations visible to importing modules.
  • Dependency graph: directed relationships among targets or modules.
02

Learning stage

Syntax & Examples

A local package can expose a small feature API while keeping implementation and tests in separate targets.

Package.swiftswift
// swift-tools-version: 6.1
import PackageDescription

let package = Package(
  name: "LearningFeatures",
  platforms: [.iOS(.v17)],
  products: [
    .library(name: "CourseLibrary", targets: ["CourseLibrary"])
  ],
  targets: [
    .target(
      name: "LearningModels"
    ),
    .target(
      name: "CourseLibrary",
      dependencies: ["LearningModels"],
      resources: [.process("Resources")]
    ),
    .testTarget(
      name: "CourseLibraryTests",
      dependencies: ["CourseLibrary"]
    )
  ]
)
The product exposes one feature target, while target dependencies form an explicit acyclic graph.
CourseLibraryFeature.swiftswift
import UIKit

public struct CourseLibraryFeature {
  private let repository: any CourseRepository

  package init(repository: any CourseRepository) {
    self.repository = repository
  }

  @MainActor
  public func makeViewController() -> UIViewController {
    CourseLibraryViewController(repository: repository)
  }
}

package struct CourseLibraryDiagnostics {
  // Shared by targets in this package, hidden from package clients.
}
Public API stays narrow, package access supports cooperating targets, and implementation types remain internal.
03

Learning stage

Real-World Usage

Connect the architectural concept to maintainable iOS features and real change scenarios.

  • Enforce that feature presentation cannot import concrete networking code.
  • Run focused package tests without building the entire app target.
  • Share a design system or domain capability across multiple applications.
  • Give teams explicit ownership and stable integration surfaces.
04

Learning stage

Common Mistakes

Identify abstractions and dependencies that add coupling, ambiguity, or unnecessary ceremony.

  • Creating one target per small type or screen.
  • Making every declaration public to fix compiler errors quickly.
  • Building cyclic feature dependencies through convenience imports.
  • Using one umbrella target that re-exports the entire application.
  • Assuming modularization improves build time without measuring it.
05

Learning stage

Practice Exercises

Apply the design to a small feature and evaluate it through observable change scenarios.

Activity 1

Design an acyclic module graph

Draw App, CourseLibrary, LessonPlayer, LearningModels, Networking, Persistence, and DesignSystem targets. Add arrows and remove any cycle.

Hint: Features should usually meet through stable domain or feature APIs, not import each other's internals.

Activity 2

Extract one local package

Move a stable design-system or domain boundary into a local Swift package with a library product, focused target, resources if needed, and a test target.

Hint: Start with the smallest public API the app actually requires.

06

Learning stage

Knowledge Check

Answer each question before revealing the explanation.

QUESTION 1

What does a Swift package target normally compile into?

  1. A.A module or test suite
  2. B.A storyboard scene
  3. C.A Git branch
  4. D.A runtime singleton
Reveal answer

A module or test suite

Targets are the package's build units and may depend on other targets or external products.

QUESTION 2

Why keep a module's public API small?

Reveal answer

Every public declaration increases coupling, compatibility obligations, and the surface clients can depend on.

Internal and package access let implementation evolve without becoming part of the supported external contract.

07

Learning stage

Interview Preparation

Explain the decision, dependency direction, trade-offs, and evidence—not only the pattern name.

When would you modularize an iOS application with Swift packages?

Short answer: When a real boundary benefits from dependency enforcement, ownership, reuse, focused tests, or measured build improvements.

Explore the detailed answer

I map the desired acyclic graph, extract a stable boundary into a local package, and expose the smallest useful product API. I keep most declarations internal or package-scoped, add target-specific tests and resources, and measure developer workflows. I avoid module count as a goal because graph complexity and public API maintenance carry real cost.

Possible follow-ups

  • • What is the difference between a package and target?
  • • How do you prevent feature-module cycles?
08

Learning stage

Key Takeaways

Swift packages turn logical boundaries into compiler-enforced modules. They are most effective when the dependency graph is intentional and APIs remain small.

  • Packages group targets; targets compile into modules.
  • Products expose selected targets.
  • Use access control to protect implementation.
  • Keep the dependency graph acyclic.
  • Measure modularization benefits against build and maintenance costs.