Lernziel
In diesem Projektteil räumst du die Visitenkarte auf: Du machst aus den wiederholten Kontaktzeilen eine eigene kleine View, trennst Daten und Darstellung mit einer Struct, legst eine eigene Markenfarbe im Asset-Katalog an und prüfst die Karte mit Previews in Dark Mode und großer Schrift.
Ausgangslage
Am Ende von Teil 1 hatten wir eine hübsche Karte, aber auch zwei Schwächen:
- Wiederholung: Die drei Kontaktzeilen sind fast identisch. Willst du die Symbolfarbe ändern, musst du es dreimal tun.
- Daten im Layout: Name, E-Mail und Telefonnummer stehen mitten im Layout-Code. Willst du eine zweite Karte für eine andere Person, musst du alles kopieren.
Beides lösen wir jetzt, mit Werkzeugen aus Stufe 1.
Schritt 1: Eine eigene View für die Kontaktzeile
In Lektion 2.2 hast du gesehen, dass du eigene Views genauso verwenden kannst wie Apples Views, und dass du ihnen Daten über Eigenschaften übergibst.
Aufgabe
Leg unter ContentView eine neue View ContactRow an. Sie bekommt zwei Eigenschaften: icon (der Name des SF Symbols) und text. Ihr body ist der HStack aus Teil 1. Ersetze dann die drei HStacks in ContentView durch drei ContactRows.
LösungErst selbst probieren!
struct ContactRow: View {
let icon: String
let text: String
var body: some View {
HStack(spacing: 12) {
Image(systemName: icon)
.foregroundStyle(.indigo)
.frame(width: 24)
Text(text)
}
}
}Und in ContentView:
VStack(alignment: .leading, spacing: 12) {
ContactRow(icon: "envelope.fill", text: "anna@example.com")
ContactRow(icon: "phone.fill", text: "+43 664 1234567")
ContactRow(icon: "mappin.and.ellipse", text: "Wien, Österreich")
}
.frame(maxWidth: .infinity, alignment: .leading)Aus 18 Zeilen wurden drei. Und die Symbolfarbe änderst du jetzt an einer Stelle.
Tipp
Eine eigene View bekommt auch eine eigene Preview. Mit traits: .sizeThatFitsLayout siehst du nur die Zeile selbst (Lektion 2.3):
#Preview(traits: .sizeThatFitsLayout) {
ContactRow(icon: "envelope.fill", text: "anna@example.com")
.padding()
}Schritt 2: Daten und Darstellung trennen
Die Daten der Karte gehören zusammen und beschreiben eine Person. Das ist ein perfekter Fall für eine Struct (Lektion 1.12).
Aufgabe
- Leg eine Struct
Personmitname,job,email,phoneundcityan. - Ergänze in einer Extension Beispieldaten
static let example(Lektion 1.16). - Benenne
ContentViewinBusinessCardum (Rechtsklick → Refactor → Rename) und gib ihr eine Eigenschaftlet person: Person. - Ersetze alle festen Texte durch die Werte aus
person. - Passe die Preview und die Datei
VisitenkarteApp.swiftan, sodassBusinessCard(person: .example)angezeigt wird.
LösungErst selbst probieren!
struct Person {
let name: String
let job: String
let email: String
let phone: String
let city: String
}
extension Person {
static let example = Person(
name: "Anna Berger",
job: "iOS-Entwicklerin",
email: "anna@example.com",
phone: "+43 664 1234567",
city: "Wien, Österreich"
)
}In BusinessCard:
struct BusinessCard: View {
let person: Person
var body: some View {
VStack(spacing: 8) {
// … Profilbild wie bisher …
Text(person.name)
.font(.title)
.bold()
Text(person.job)
.font(.subheadline)
.foregroundStyle(.secondary)
Divider()
.padding(.vertical, 8)
VStack(alignment: .leading, spacing: 12) {
ContactRow(icon: "envelope.fill", text: person.email)
ContactRow(icon: "phone.fill", text: person.phone)
ContactRow(icon: "mappin.and.ellipse", text: person.city)
}
.frame(maxWidth: .infinity, alignment: .leading)
}
// … Modifier wie bisher …
}
}
#Preview {
BusinessCard(person: .example)
}Und in VisitenkarteApp.swift:
WindowGroup {
BusinessCard(person: .example)
}Jetzt kannst du beliebig viele Karten für verschiedene Personen anzeigen, ohne eine Zeile Layout-Code zu kopieren. Das ist das Prinzip Trennung von Daten und Darstellung, und du wirst es in jeder App anwenden.
Schritt 3: Eine eigene Markenfarbe
Bisher steht .indigo an mehreren Stellen im Code. Was, wenn deine Marke eine ganz bestimmte Farbe hat?
Aufgabe
- Leg im Asset-Katalog eine neue Farbe
CardAccentan (Lektion 2.6). - Stell unter Appearances „Any, Dark“ ein und wähle für den dunklen Modus eine etwas hellere Variante, damit sie auf dunklem Grund gut lesbar ist.
- Ersetze alle Vorkommen von
.indigodurch die neue Farbe.
LösungErst selbst probieren!
Je nach Xcode-Version verwendest du die Farbe als Color("CardAccent") oder, dank der automatisch erzeugten Namen, als Color(.cardAccent):
Image(systemName: icon)
.foregroundStyle(Color(.cardAccent))und für den Hintergrund:
.background(Color(.cardAccent).gradient)Ändere jetzt die Farbe im Asset-Katalog. Die ganze Karte passt sich an, ohne eine Zeile Code anzufassen.
Schritt 4: Mit Previews prüfen
Eine gute Komponente funktioniert in jeder Situation. Prüf das mit Previews (Lektion 2.3).
Aufgabe
Leg drei weitere Beispieldaten an: eine Person mit sehr langem Namen, eine mit sehr langer E-Mail-Adresse und eine mit sehr kurzen Angaben. Schreib dann Previews für:
- das Beispiel im Dark Mode,
- das Beispiel mit
.dynamicTypeSize(.accessibility3), - die drei neuen Beispiele.
Was fällt dir auf?
LösungErst selbst probieren!
extension Person {
static let longName = Person(
name: "Maximiliane Hohenzollern-Sigmaringen",
job: "Leiterin Produktentwicklung",
email: "m.hohenzollern@example.com",
phone: "+43 664 1234567",
city: "Salzburg, Österreich"
)
static let longEmail = Person(
name: "Ben Huber",
job: "Designer",
email: "benjamin.huber.design.studio@example-agentur.com",
phone: "+43 676 7654321",
city: "Graz"
)
static let short = Person(name: "Li", job: "CEO", email: "li@x.at", phone: "1", city: "Linz")
}
#Preview("Dunkel") {
BusinessCard(person: .example)
.preferredColorScheme(.dark)
}
#Preview("Große Schrift") {
BusinessCard(person: .example)
.dynamicTypeSize(.accessibility3)
}
#Preview("Langer Name") {
BusinessCard(person: .longName)
}
#Preview("Lange E-Mail") {
BusinessCard(person: .longEmail)
}
#Preview("Kurz") {
BusinessCard(person: .short)
}Typische Beobachtungen:
- Der lange Name bricht um, steht aber linksbündig statt zentriert. Lösung:
.multilineTextAlignment(.center)am Namen. - Die lange E-Mail-Adresse wird umgebrochen oder abgeschnitten. Für E-Mail-Adressen ist
.lineLimit(1)mit.minimumScaleFactor(0.7)oft eine gute Lösung. - Bei großer Schrift passt die Karte vielleicht nicht mehr auf den Bildschirm. Dafür bräuchte man eine
ScrollView, die du in Lektion 3.18 genauer kennenlernst. Schon jetzt kannst du den äußeren Inhalt inScrollView { ... }einpacken.
Schritt 5: Der letzte Schliff
Aufgabe
Verbessere die Karte nach deinem Geschmack. Ein paar Ideen:
- Die Kontaktzeilen bekommen einen dezenten Hintergrund in
Color(.secondarySystemBackground)mit abgerundeten Ecken. - Unter dem Beruf steht ein kurzer Satz über die Person, kursiv und zentriert.
- Das Profilbild bekommt über
.overlay(alignment: .bottomTrailing)ein kleines grünes „Verfügbar“-Abzeichen. - Du legst ein eigenes App-Symbol im Asset-Katalog unter
AppIconan.
Der fertige Code
Komplette LösungErst selbst probieren!
import SwiftUI
struct Person {
let name: String
let job: String
let email: String
let phone: String
let city: String
}
extension Person {
static let example = Person(
name: "Anna Berger",
job: "iOS-Entwicklerin",
email: "anna@example.com",
phone: "+43 664 1234567",
city: "Wien, Österreich"
)
}
struct BusinessCard: View {
let person: Person
var body: some View {
ScrollView {
VStack(spacing: 8) {
Image(systemName: "person.crop.circle.fill")
.resizable()
.scaledToFit()
.frame(width: 120, height: 120)
.foregroundStyle(.white, Color(.cardAccent).opacity(0.6))
.overlay {
Circle().stroke(.white, lineWidth: 4)
}
.shadow(radius: 8)
Text(person.name)
.font(.title)
.bold()
.multilineTextAlignment(.center)
Text(person.job)
.font(.subheadline)
.foregroundStyle(.secondary)
Divider()
.padding(.vertical, 8)
VStack(alignment: .leading, spacing: 12) {
ContactRow(icon: "envelope.fill", text: person.email)
ContactRow(icon: "phone.fill", text: person.phone)
ContactRow(icon: "mappin.and.ellipse", text: person.city)
}
.frame(maxWidth: .infinity, alignment: .leading)
}
.padding(24)
.frame(maxWidth: .infinity)
.background(Color(.systemBackground))
.clipShape(.rect(cornerRadius: 24))
.shadow(color: .black.opacity(0.2), radius: 16, y: 8)
.padding(24)
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(Color(.cardAccent).gradient)
}
}
struct ContactRow: View {
let icon: String
let text: String
var body: some View {
HStack(spacing: 12) {
Image(systemName: icon)
.foregroundStyle(Color(.cardAccent))
.frame(width: 24)
Text(text)
.lineLimit(1)
.minimumScaleFactor(0.7)
}
}
}
#Preview {
BusinessCard(person: .example)
}Wenn dein Xcode keine automatischen Farbnamen erzeugt, ersetze Color(.cardAccent) durch Color("CardAccent").
Was du in diesem Projekt angewendet hast
| Baustein | Wo im Projekt | Lektion |
|---|---|---|
| App-Aufbau, eigene Views | BusinessCard, ContactRow | 2.2 |
| Previews mit Varianten | Dark Mode, große Schrift, Grenzfälle | 2.3 |
| Modifier-Reihenfolge | Karte mit Hintergrund, Ecken, Schatten | 2.4 |
| Textstile | Name, Beruf | 2.5 |
| Bilder, Symbole, Farben | Profilbild, Kontaktsymbole, Markenfarbe | 2.6 |
| Stacks | gesamtes Layout | 2.7 |
| Abstände, Rahmen | Innen- und Außenabstände, volle Breite | 2.8 |
| Structs, Extensions | Person mit Beispieldaten | 1.12, 1.16 |
Geschafft
Glückwunsch, deine erste richtige SwiftUI-App ist fertig! Starte sie mit ⌘ R im Simulator oder auf deinem iPhone (Lektion 0.4). Ab der nächsten Lektion wird es lebendig: Deine Views lernen, auf Nutzer zu reagieren.
Zusammenfassung
- Wiederholter Layout-Code wird zu einer eigenen View mit Eigenschaften.
- Daten gehören in eine Struct, die Darstellung in eine View, die diese Struct bekommt.
- Eine eigene Farbe im Asset-Katalog macht Designänderungen an einer Stelle möglich und funktioniert in hellem und dunklem Modus.
- Previews mit Grenzfällen decken Darstellungsprobleme auf, bevor Nutzer sie finden.