Lernziel
Nach dieser Lektion kannst du mit Swift Packages arbeiten: fremde Pakete einbinden und Versionen sinnvoll festlegen, eigene lokale Pakete anlegen, dein Projekt in Module wie Designsystem, Netzwerk und Features aufteilen, Sichtbarkeit mit public und package steuern und Ressourcen sowie Tests in Paketen unterbringen.
Das Problem: Alles in einem großen Topf
Am Anfang besteht eine App aus ein paar Dateien in einem Ordner. Nach ein paar Monaten sind es hundert. Alles kann alles sehen: Die Rezeptliste greift direkt auf Hilfsfunktionen der Netzwerkschicht zu, das Designsystem kennt plötzlich ein Modell aus der Datenbank, und eine kleine Änderung an einer Datei lässt Xcode die halbe App neu kompilieren.
Typische Folgen:
- Unübersichtlich: Wo gehört eine neue Datei hin? Wovon hängt was ab?
- Langsame Builds: Jede Änderung kann viel neu kompilieren.
- Ungewollte Abhängigkeiten: Teile, die nichts voneinander wissen sollten, sind eng verflochten.
- Kein Wiederverwenden: Dein schönes Designsystem aus Lektion 3.29 steckt in einer App und lässt sich nicht in die nächste mitnehmen.
Die Lösung ist Modularisierung: die App in unabhängige Bausteine aufteilen, die klare Grenzen haben. In der Swift-Welt sind diese Bausteine Swift Packages.
Was ist ein Swift Package?
Ein Swift Package ist ein Ordner mit Swift-Code und einer Beschreibungsdatei namens Package.swift. Diese Datei legt fest:
- wie das Paket heißt,
- für welche Plattformen es gedacht ist,
- welche Module (Targets) es enthält,
- was es nach außen anbietet (Products),
- und wovon es selbst abhängt.
Pakete kannst du auf zwei Arten nutzen: fremde Pakete einbinden, die andere geschrieben haben, und eigene Pakete anlegen, um dein Projekt aufzuteilen.
Fremde Pakete einbinden
Viele nützliche Bibliotheken gibt es als Swift Package, oft auf GitHub. So bindest du eines ein:
- Datei → Add Package Dependencies …
- Gib oben rechts die Adresse des Pakets ein, etwa
https://github.com/apple/swift-collections. - Wähle eine Dependency Rule (dazu gleich mehr) und klick auf Add Package.
- Wähle die gewünschten Bibliotheken und das Target, also deine App, und bestätige.
Danach kannst du sie mit import verwenden:
import Collections
var recent = Deque<String>()
recent.prepend("Pfannkuchen")Xcode lädt das Paket herunter und zeigt es im Projektnavigator unter Package Dependencies.
Versionen festlegen
Pakete haben Versionsnummern nach dem Schema Major.Minor.Patch, etwa 1.4.2:
- Patch (1.4.2): Fehlerbehebungen. Nichts ändert sich an der Verwendung.
- Minor (1.4.0): Neue Funktionen. Bestehender Code funktioniert weiter.
- Major (2.0.0): Größere Änderungen. Bestehender Code muss eventuell angepasst werden.
Die Dependency Rule bestimmt, welche Versionen Xcode verwenden darf:
| Regel | Bedeutung |
|---|---|
| Up to Next Major Version | ab 1.4.0, aber unter 2.0.0. Der empfohlene Standard. |
| Up to Next Minor Version | ab 1.4.0, aber unter 1.5.0. Vorsichtiger. |
| Exact Version | genau 1.4.2. Nur in Ausnahmefällen. |
| Branch / Commit | ein bestimmter Stand. Für Experimente, nicht für veröffentlichte Apps. |
Die tatsächlich verwendeten Versionen hält Xcode in einer Datei Package.resolved fest. Sie gehört ins Git-Repository, damit alle im Team und deine Build-Server dieselben Versionen verwenden. Aktualisieren kannst du mit Datei → Packages → Update to Latest Package Versions.
Fremde Pakete mit Bedacht wählen
Jedes Paket ist Code, den du nicht selbst geschrieben hast, der aber in deiner App läuft. Prüfe vorher: Wird es noch gepflegt? Wie viele Leute verwenden es? Was steht in der Lizenz? Brauchst du es wirklich, oder reichen ein paar Zeilen eigener Code? Viele Funktionen, für die man früher Bibliotheken brauchte, sind heute in SwiftUI und Foundation eingebaut.
Eigene Pakete: Das Projekt aufteilen
Der zweite, mindestens so wichtige Einsatz: Du teilst dein eigenes Projekt in Pakete auf.
Wie aufteilen?
Eine bewährte Aufteilung für eine mittelgroße App:
Rezepte (App-Target)
├── RecipeKit (lokales Paket)
│ ├── DesignSystem Farben, Abstände, Komponenten
│ ├── Models Datentypen wie Meal, Category
│ ├── MealAPI Netzwerkschicht, hängt von Models ab
│ └── RecipeFeature Bildschirme, hängt von allem oben abDie Regel: Abhängigkeiten zeigen nur in eine Richtung, von oben nach unten. DesignSystem weiß nichts von MealAPI. Models weiß nichts von Views. Das App-Target selbst wird ganz dünn: Es stellt nur noch alles zusammen (Lektion 4.13).
Ein lokales Paket anlegen
- Datei → Neu → Package …
- Wähle Library, nenne es
RecipeKitund speichere es im Ordner deines Projekts. - Wähle unten bei Add to dein Projekt und bei Group die oberste Ebene.
Xcode legt einen Ordner mit Package.swift, einem Ordner Sources und einem Ordner Tests an.
Package.swift
Ersetze den Inhalt von Package.swift durch:
// swift-tools-version: 6.0
import PackageDescription
let package = Package(
name: "RecipeKit",
platforms: [.iOS(.v18)],
products: [
.library(name: "DesignSystem", targets: ["DesignSystem"]),
.library(name: "Models", targets: ["Models"]),
.library(name: "MealAPI", targets: ["MealAPI"])
],
targets: [
.target(name: "DesignSystem", resources: [.process("Resources")]),
.target(name: "Models"),
.target(name: "MealAPI", dependencies: ["Models"]),
.testTarget(name: "MealAPITests", dependencies: ["MealAPI"])
]
)swift-tools-version: 6.0in der ersten Zeile ist kein Kommentar, sondern Pflicht: Sie legt fest, welche Version der Paket-Beschreibung gilt. Ab 6.0 kompilieren Pakete standardmäßig im Swift-6-Sprachmodus mit voller Concurrency-Prüfung (Lektion 3.8).platforms: Mindestversion. Ohne Angabe verwendet Swift eine sehr alte Version, und moderne SwiftUI-APIs wären nicht verfügbar.targets: die Module. Jedes Target ist ein Ordner unterSourcesmit demselben Namen, etwaSources/MealAPI/.dependencieseines Targets: Wovon hängt es ab?MealAPIbrauchtModels.products: was das Paket nach außen anbietet. Nur Products kann die App einbinden.
Das Paket in der App verwenden
- Klick im Projektnavigator auf das Projekt und wähle das App-Target.
- Im Reiter General unter Frameworks, Libraries, and Embedded Content auf + klicken.
- Die gewünschten Bibliotheken, etwa
DesignSystemundMealAPI, auswählen.
Danach in der App:
import SwiftUI
import DesignSystem
import MealAPISichtbarkeit: public, package, internal
Jetzt kommt die Stelle, an der fast jeder beim ersten Mal stolpert. Du verschiebst Card aus Lektion 3.29 in das Modul DesignSystem und versuchst es in der App zu verwenden:
Cannot find 'Card' in scopeDer Grund: In Swift ist alles standardmäßig internal, also nur innerhalb des eigenen Moduls sichtbar. Solange alles in der App lag, war das egal. Jetzt ist das Designsystem ein eigenes Modul, und die App kann nur sehen, was ausdrücklich freigegeben ist.
| Stufe | Sichtbar … |
|---|---|
private | nur im selben Typ bzw. Abschnitt |
fileprivate | nur in derselben Datei |
internal (Standard) | im selben Modul |
package | in allen Modulen desselben Pakets |
public | überall, wo das Modul importiert wird |
Damit die App Card verwenden kann:
public struct Card<Content: View>: View {
let content: Content
public init(@ViewBuilder content: () -> Content) {
self.content = content()
}
public var body: some View {
VStack(alignment: .leading, spacing: Spacing.s) {
content
}
.padding(Spacing.m)
.background(Color(.secondarySystemGroupedBackground), in: .rect(cornerRadius: Radius.medium))
}
}Drei Dinge brauchen public:
- der Typ selbst,
- der Initializer: Den automatisch erzeugten Initializer eines Structs gibt es nur
internal. Für andere Module musst du ihn selbst schreiben undpublicmachen, body, weilViewes verlangt.
Gute Grenzen ziehen
Das ist kein lästiger Zwang, sondern der eigentliche Gewinn: Du entscheidest bewusst, was die Schnittstelle eines Moduls ist. Alles andere bleibt Implementierungsdetail, das du jederzeit ändern kannst, ohne dass irgendetwas außerhalb kaputtgeht.
package ist der Mittelweg: Ein Typ, den MealAPI und Models gemeinsam brauchen, den die App aber nicht sehen soll, wird package.
Ressourcen in Paketen
Bilder, Farben, JSON-Dateien und Schriften legst du in einen Ordner im Target, etwa Sources/DesignSystem/Resources/, und meldest ihn in Package.swift mit resources: [.process("Resources")] an.
Im Code greifst du über Bundle.module darauf zu, nicht über Bundle.main:
public extension Color {
static let brandPrimary = Color("BrandPrimary", bundle: .module)
}
let url = Bundle.module.url(forResource: "sample", withExtension: "json")Bundle.module wird automatisch für jedes Target mit Ressourcen erzeugt und zeigt auf dessen Ressourcen.
Tests und Previews in Paketen
Jedes Modul kann eigene Tests haben: ein testTarget in Package.swift und ein passender Ordner unter Tests. Das ist einer der größten Vorteile: Die Tests für MealAPI laufen in Sekunden, ohne dass die ganze App gebaut werden muss.
import Testing
@testable import MealAPI
@Test func decodesMeal() throws { ... }Auch Previews funktionieren in Paketen. Wähle dafür oben in Xcode das Schema des Pakets (etwa DesignSystem) statt der App. Weil nur das kleine Modul kompiliert wird, sind Previews dort oft deutlich schneller.
Typische Fehler
Cannot find 'X' in scope nach dem Verschieben
Typ, Initializer oder benötigte Eigenschaften sind nicht public. Oder die Bibliothek ist nicht unter Frameworks, Libraries, and Embedded Content eingetragen.
'X' is only available in iOS 17 or newer
platforms fehlt in Package.swift oder nennt eine zu alte Version.
Bild oder Farbe aus dem Paket wird nicht gefunden
Der Zugriff geht über Bundle.main statt Bundle.module, oder der Ressourcen-Ordner ist nicht in Package.swift angemeldet.
Kreisförmige Abhängigkeit
Models hängt von MealAPI ab und MealAPI von Models. Das erlaubt Swift nicht. Zieh das, was beide brauchen, in ein drittes Modul, oder dreh eine der Abhängigkeiten um, etwa mit einem Protokoll.
FAQ
Anfänger
Was bedeutet das überhaupt?Ab wann lohnt sich Modularisierung? Ein erstes Paket für das Designsystem lohnt sich früh, weil du es in jeder weiteren App wiederverwenden kannst. Die ganze App aufzuteilen, lohnt sich ab mittlerer Größe oder sobald mehrere Leute daran arbeiten. Für kleine Übungsapps ist es nicht nötig.
Was ist mit CocoaPods und Carthage? Das sind ältere Paketverwaltungen, die du in bestehenden Projekten noch sehen wirst. Für neue Projekte ist der Swift Package Manager der Standard, direkt in Xcode eingebaut.
Fortgeschritten
Warum verhält es sich so?Kann ich SwiftData-Modelle in ein Paket legen?
Ja. Die @Model-Klassen und ihre Eigenschaften müssen dann public sein, inklusive Initializer. Die .modelContainer-Einrichtung bleibt meist in der App.
Wie teile ich ein Paket zwischen mehreren Apps? Leg es in ein eigenes Git-Repository und binde es in beiden Apps über Add Package Dependencies mit der Repository-Adresse ein. Für private Repositorys fragt Xcode nach deinen Zugangsdaten bzw. verwendet deinen GitHub-Account aus den Xcode-Einstellungen.
Profi
Was passiert unter der Haube?Wie wirkt sich Modularisierung auf Build-Zeiten aus?
Xcode kompiliert nur Module neu, deren Code sich geändert hat oder deren öffentliche Schnittstelle sich bei einer Abhängigkeit geändert hat. Änderungen an internal-Code eines Moduls zwingen abhängige Module nicht zum Neukompilieren. Je kleiner die öffentliche Schnittstelle, desto mehr profitierst du. Zusätzlich können unabhängige Module parallel kompiliert werden.
Was sind Paket-Plugins?
Pakete können Plugins enthalten, die beim Bauen Code erzeugen oder prüfen, etwa einen Formatierer oder einen Generator für Typen aus einer Schnittstellenbeschreibung. Sie werden in Package.swift mit .plugin deklariert und laufen in einer abgeschirmten Umgebung.
Übungen
Übung 1: Ein fremdes Paket
Binde https://github.com/apple/swift-collections in ein Testprojekt ein, mit der Regel Up to Next Major Version. Verwende ein OrderedSet für eine Liste zuletzt angesehener Rezepte, in der kein Rezept doppelt vorkommt und die Reihenfolge erhalten bleibt.
Übung 2: Das Designsystem auslagern
Leg ein lokales Paket AppDesign mit einem Target DesignSystem an. Verschiebe Spacing, Radius, Card und den PrimaryButtonStyle aus Projekt E hinein und mach sie public. Verwende sie wieder in der App.
Lösung Übung 2Erst selbst probieren!
Package.swift:
// swift-tools-version: 6.0
import PackageDescription
let package = Package(
name: "AppDesign",
platforms: [.iOS(.v18)],
products: [.library(name: "DesignSystem", targets: ["DesignSystem"])],
targets: [.target(name: "DesignSystem")]
)Sources/DesignSystem/Spacing.swift:
import CoreGraphics
public enum Spacing {
public static let xs: CGFloat = 4
public static let s: CGFloat = 8
public static let m: CGFloat = 16
public static let l: CGFloat = 24
}Sources/DesignSystem/PrimaryButtonStyle.swift:
import SwiftUI
public struct PrimaryButtonStyle: ButtonStyle {
public init() {}
public func makeBody(configuration: Configuration) -> some View {
configuration.label
.font(.headline)
.foregroundStyle(.white)
.padding(.vertical, Spacing.m - 2)
.frame(maxWidth: .infinity)
.background(.tint, in: .rect(cornerRadius: 14))
.scaleEffect(configuration.isPressed ? 0.97 : 1)
}
}
public extension ButtonStyle where Self == PrimaryButtonStyle {
static var primary: PrimaryButtonStyle { PrimaryButtonStyle() }
}Statt der Asset-Farbe der App verwendet der Stil hier .tint, also die Akzentfarbe der App, in die er eingebunden wird. So bleibt das Paket unabhängig von einer bestimmten App.
Übung 3: Die Rezept-App modularisieren
Teile die Rezept-App aus Projekt D in ein Paket RecipeKit mit den Modulen Models, MealAPI und RecipeFeature auf. MealAPI darf nur von Models abhängen, RecipeFeature von beiden. Verschiebe die Tests für das Dekodieren in MealAPITests und führe sie im Paket-Schema aus. Wie viel schneller laufen sie als im App-Target?
Zusammenfassung
- Swift Packages sind Bausteine mit einer
Package.swift, die Targets, Products, Plattformen und Abhängigkeiten beschreibt. - Fremde Pakete über Add Package Dependencies, mit der Regel Up to Next Major Version.
Package.resolvedgehört ins Repository. - Eigene Pakete teilen die App in Module mit Abhängigkeiten in nur eine Richtung.
- Standard ist
internal: Was andere Module sehen sollen, musspublicsein, inklusive Initializer.packagefür gemeinsame Teile innerhalb eines Pakets. - Ressourcen über
Bundle.module, Tests und Previews direkt im Paket. platformsnicht vergessen, abswift-tools-version: 6.0gilt der Swift-6-Modus.
Spickzettel
// swift-tools-version: 6.0
import PackageDescription
let package = Package(
name: "RecipeKit",
platforms: [.iOS(.v18)],
products: [.library(name: "MealAPI", targets: ["MealAPI"])],
dependencies: [
.package(url: "https://github.com/apple/swift-collections", from: "1.1.0")
],
targets: [
.target(name: "Models"),
.target(name: "MealAPI", dependencies: [
"Models",
.product(name: "Collections", package: "swift-collections")
]),
.testTarget(name: "MealAPITests", dependencies: ["MealAPI"])
]
)
public struct Card: View {
public init() {}
public var body: some View { ... }
}
Color("Brand", bundle: .module)