SwiftUI Cheatsheet

Translated by: gpt-4o-2024-08-06 | Original version

Im Juni 2025 begann ich mit dem Kurs 100 Days of SwiftUI. Es ist ein großartiger Kurs, und ich bin wirklich beeindruckt, wie viel qualitativ hochwertiger Inhalt und Kurse Paul Hudson bereitstellt – und pflegt!! Paul, vielen Dank dafür! 🙏🏼

Aber es ist eine Menge Inhalt, daher hier meine Notizen – hoffentlich in einem leicht navigierbaren Spickzettel-Format. Ich habe eine grobe Struktur im Kopf, werde den Inhalt jedoch nur dann ausfüllen, wenn ich ihn benötige. Erwarten Sie also keine vollständige Übersicht!

Swift

Für einen umfassenden Überblick siehe Lernen Sie essentielles Swift in einer Stunde.

Im folgenden Kapitel habe ich nur die Teile hinzugefügt, die ich mindestens einmal überprüfen musste.

struct & berechnete Eigenschaften

struct Employee {
    let name: String
    var vacationAllocated = 14
    var vacationTaken = 0

    var vacationRemaining: Int {
        vacationAllocated - vacationTaken
    }
}

Optionals

  • Optionals ermöglichen es uns, das Fehlen von Daten darzustellen, was bedeutet, dass wir sagen können: „Dieser Integer hat keinen Wert“ – das ist anders als eine feste Zahl wie 0.
  • Beispiel: var str:String? kann einen String oder nil enthalten
  • Folglich hat alles, was nicht optional ist, definitiv einen Wert, selbst wenn es nur ein leerer String ist.
  • Das Entpacken eines Optionals ist der Prozess, in eine Box zu schauen, um zu sehen, was sie enthält: Wenn ein Wert darin ist, wird er zur Verwendung zurückgegeben, andernfalls wird nil enthalten sein.
  • Wir können if let verwenden, um Code auszuführen, wenn das Optional einen Wert hat, oder guard let, um Code auszuführen, wenn das Optional keinen Wert hat – aber mit guard müssen wir danach immer die Funktion verlassen.
func printSquare(of number: Int?) {
    guard let number = number else {
        print("Eingabe fehlt")
        return
    }

    print("\(number) x \(number) ist \(number * number)")
}
  • Der nil-Koaleszenz-Operator, ??, entpackt und gibt den Wert eines Optionals zurück oder verwendet stattdessen einen Standardwert.
let new = captains["Serenity"] ?? "N/A"
  • Optional Chaining ermöglicht es uns, ein Optional innerhalb eines anderen Optionals mit einer praktischen Syntax zu lesen.
  • Wenn eine Funktion Fehler werfen könnte, können Sie sie mit try? in ein Optional umwandeln – Sie erhalten entweder den Rückgabewert der Funktion oder nil, wenn ein Fehler auftritt.

Protokolle und Erweiterungen

protocol Vehicle {
    func estimateTime(for distance: Int) -> Int
    func travel(distance: Int)
}
extension String {
    func trimmed() -> String {
        self.trimmingCharacters(in: .whitespacesAndNewlines)
    }
}

Arrays & Sortierung

Alle Arrays haben eingebaute sort() und sorted() Methoden, die verwendet werden können, um das Array zu sortieren.

  • sort() sortiert das Array direkt
  • sorted() gibt ein neues, sortiertes Array zurück.

Wenn das Array einfach ist, können Sie einfach sort() direkt aufrufen, um ein Array vor Ort zu sortieren:

var names = ["Jemima", "Peter", "David", "Kelly", "Isabella"]
names.sort()

Wenn Sie komplexere Strukturen haben, müssen Sie den Vergleich angeben:

struct User {
    var firstName: String
}

var users = [
    User(firstName: "Jemima"),
    User(firstName: "Peter"),
    User(firstName: "David"),
    User(firstName: "Kelly"),
    User(firstName: "Isabella")
]

users.sort {
    $0.firstName < $1.firstName
}

Wir können unsere eigenen Typen Comparable konform machen, und wenn wir das tun, erhalten wir auch eine sorted() Methode ohne Parameter. Dies erfordert zwei Schritte:

  1. Fügen Sie die Comparable-Konformität zur Definition von User hinzu.
  2. Fügen Sie eine Methode namens < hinzu, die zwei Benutzer nimmt und true zurückgibt, wenn der erste vor dem zweiten sortiert werden soll.

So sieht das im Code aus:

struct User: Identifiable, Comparable {
    let id = UUID()
    var firstName: String
    var lastName: String

    static func <(lhs: User, rhs: User) -> Bool {
        lhs.lastName < rhs.lastName
    }
}

Strings

  • Sie sind besonders, und es gibt viel zu wissen...
  • Das funktioniert nicht:
let name = "Paul"
let firstLetter = name[0]

Daten

Date, DateComponents und DateFormatter

enum

enum werden so erstellt:

enum Weekday {
    case monday, tuesday, wednesday, thursday, friday
}

Eine switch-Anweisung mit enum sieht so aus:

switch loadingState {
case .loading:
    LoadingView()
case .success:
    SuccessView()
case .failed:
    FailedView()
}

SwiftUI

Ansichten

  • Alles ist eine Ansicht in SwiftUI 😜
  • Code ausführen, wenn eine Ansicht angezeigt wird, mit onAppear().

Sogar ForEach ist eine Ansicht, deshalb können wir schreiben

ForEach(0..<5) {
    Text("Zeile \($0)")
}

Hinweis: Wir können nicht ForEach(0..<5) schreiben, weil ForEach einen Range<Int> erwartet, nicht einen ClosedRange<Int>!

ForEach Ansicht

ForEach ist eine Ansicht, die aus den Unteransichten besteht, die in jeder Schleifeninstanz erstellt werden.

Wir verwenden es typischerweise, um Unteransichten basierend auf einem Zähler oder einem Array zu erstellen.

ForEach mit einem Array:

import SwiftUI

struct ContentView: View {
  let items = ["Apfel", "Banane", "Kirsche"]

  var body: some View {
    List {
      ForEach(items, id: \.self) { item in
        Text(item)
      }
    }
  }
}

Dateneingabe

TextField

Picker

Picker wird verwendet, um eine von vielen möglichen Auswahlen zu treffen.

Ein regulärer Picker sieht so aus:

Picker("Anzahl der Personen", selection: $numberOfPeople) {
    ForEach(2 ..< 100 , id: \.self) {
        Text("\($0) Personen")
    }
}

alt text

Ein Picker kann mit dem PickerStyle-Modifier modifiziert werden:

Picker("Trinkgeldprozentsatz", selection: $tipPercentage) {
    ForEach(tipPercentages, id: \.self) {
        Text($0, format: .percent)
    }
}
.pickerStyle(.segmented)

alt text

Stepper

Stepper Ein Stepper ist ein zweigeteiltes Steuerelement, das Menschen verwenden, um einen inkrementellen Wert zu erhöhen oder zu verringern.

@State private var count: Int = 0

var body: some View {
    Stepper("\(count)",
        value: $count,
        in: 0...100
    )
}
  • DatePicker für Daten. Verwenden Sie den Parameter displayedComponents, um Daten oder Zeiten zu steuern.
  • Form
  • Picker
  • Navigationsleiste

Warnungen & Bestätigungsdialoge

Warnungen

Warnungen sind eine Erweiterung einer Ansicht mit einer Booleschen Variablen, die entscheidet, ob sie angezeigt werden oder nicht.

struct ContentView: View {
    @State private var showingAlert = false

    var body: some View {
        Button("Warnung anzeigen") {
            showingAlert = true
        }
        .alert("Wichtige Nachricht", isPresented: $showingAlert) {
            Button("OK") { }
        }
    }
}

Bestätigungsdialoge

Verwenden Sie sie, wenn viele Schaltflächen/Optionen verfügbar sind. Für Beispielcode siehe dieses Repo.

Hinweis: Vor iOS 26 schoben sie sich von unten ins Bild, in iOS 26 erscheinen sie innerhalb des Bildschirms.

Bestätigungsdialog in iOS 18.5: alt text

Bestätigungsdialog in iOS 26: alt text

Text

Text ist ein Textfeld, das Text beschreibt.

Hinweis: Textfelder mit unterschiedlichem Styling können zusammengefügt werden, um ein großes Textfeld mit Teilen mit unterschiedlichem Styling zu bilden:

Text(page.title)
    .font(.headline)
+ Text(": ") +
Text("Seitenbeschreibung hier")
    .italic()

Und Sie erhalten einen Text mit unterschiedlichen Stilen kombiniert:

alt text

Listen

Scrollende Datenlisten mit List erstellen, insbesondere wie sie Zeilen direkt aus Datenarrays erstellen kann.

List {
    Section {
        Label("Sonne", systemImage: "sun.max")
        Label("Wolke", systemImage: "cloud")
        Label("Regen", systemImage: "cloud.rain")
    }
}

Schaltflächen in Listen: Wenn Sie eine Schaltfläche in eine Liste einfügen, wird das GESAMTE Listenelement anklickbar! Wenn es mehr als eine Schaltfläche in einer Liste gibt, klicken Sie, wo immer Sie auf das Listenelement klicken, ALLE Schaltflächen nacheinander!

Um das zu beheben und das gewünschte Verhalten zu erhalten, verwenden Sie .buttonStyle(.plain)

Dasselbe gilt für HStack:

HStack {
    if label.isEmpty == false {
        Text(label)
    }

    ForEach(1..<maximumRating + 1, id: \.self) { number in
        Button {
            rating = number
        } label: {
            image(for: number)
                .foregroundStyle(number > rating ? offColor : onColor)
        }
    }
}
.buttonStyle(.plain)

Bilder

struct ContentView: View
{
var body: some View {
Image (example)
    .resizable ()
    .scaledToFit ()
    .frame(width: 300, height: 300)}
}

alt text

Ersetzen Sie ScaledToFit durch ScaledToFill und erhalten Sie

alt text

struct ContentView: View {
    var body: some View {
        Image (example)
            .resizable ()
            .scaledToFit()
            .containerRelativeFrame(horizontal) { size, axis in
            size * 0.8
            
    }
}

alt text

Werkzeugleiste

Bundle

Dateien aus unserem App-Bundle lesen, indem wir ihren Pfad mit der Bundle-Klasse nachschlagen, einschließlich des Ladens von Strings von dort.

Animationen

Behandelt in Tag 32-34. TODO Ich muss die Clips noch einmal ansehen, um meine Notizen/Spickzettel zu extrahieren.

  • Animationen implizit mit dem animation()-Modifier erstellen.
  • Animationen mit Verzögerungen und Wiederholungen anpassen und zwischen Ease-in-Ease-out- und Spring-Animationen wählen.
  • Den animation()-Modifier an Bindungen anhängen, damit wir Änderungen direkt von UI-Steuerelementen aus animieren können.
  • withAnimation() verwenden, um explizite Animationen zu erstellen.
  • Mehrere animation()-Modifier an eine einzelne Ansicht anhängen, damit wir den Animationsstapel steuern können.

Daten laden

Wenn es synchron ist:

View...
    .onAppear(loadIt)

?? Wie wird es gemacht, wenn loadIt asynchron ist??

Werte an Ansichten übergeben und zurückgeben

Wie in Auswählen und Bearbeiten von Kartenanmerkungen gesehen

Stellen Sie sich vor, ich habe eine Ansicht, die als Sheet geöffnet wird und ein Location (ein selbst definiertes struct) erhält:

struct EditView: View {
    @Environment(\.dismiss) var dismiss
    var location: Location

    @State private var name: String
    @State private var description: String

    var body: some View {
        NavigationStack {
            Form {
                Section {
                    TextField("Ortsname", text: $name)
                    TextField("Beschreibung", text: $description)
                }
            }
            .navigationTitle("Ortsdetails")
            .toolbar {
                Button("Speichern") {
                    dismiss()
                }
            }
        }
    }
}

Um die Location zu übergeben, mache ich einen zusätzlichen Initialisierer:

init(location: Location) {
    self.location = location

    _name = State(initialValue: location.name)
    _description = State(initialValue: location.description)
}

Um Daten an den aufrufenden Code zurückzugeben, übergebe ich eine Methode onSave. Zuerst erstelle ich eine zusätzliche Variable in meiner View-Struktur:

var onSave: (Location) -> Void

und erweitere dann den Initialisierer wie folgt:

init(location: Location, onSave: @escaping (Location) -> Void) {
    self.location = location
    self.onSave = onSave

    _name = State(initialValue: location.name)
    _description = State(initialValue: location.description)
}

Dieser @escaping-Teil ist wichtig und bedeutet, dass die Funktion für die spätere Verwendung aufbewahrt wird, anstatt sofort aufgerufen zu werden, und es ist hier notwendig, weil die onSave-Funktion nur aufgerufen wird, wenn der Benutzer auf Speichern drückt.

Sheets & NavigationStacks

Um eine Ansicht als Sheet zu öffnen:

struct ContentView: View {
    @State private var showingAddExpense = false

    var body: some View {
        NavigationStack {
            VStack {
                // Some code here
            }
            .navigationTitle("iExpense")
            .toolbar {
                Button("Ausgabe hinzufügen", systemImage: "plus") {
                    showingAddExpense = true
                }
            }
            .sheet(isPresented: $showingAddExpense) {
                AddView(expenses: expenses)
            }
        }
    }
}

Netzwerk

So senden Sie etwas an einen HTTPS-Endpunkt:

 func placeOrder() async {
        guard let encoded = try? JSONEncoder().encode(order) else {
            print("Bestellung konnte nicht codiert werden")
            return
        }

        let url = URL(string: "https://reqres.in/api/cupcakes")!
        var request = URLRequest(url: url)
        request.setValue("application/json", forHTTPHeaderField: "Content-Type")
        request.httpMethod = "POST"

        do {
            let (data, other) = try await URLSession.shared.upload(for: request, from: encoded)

            let decodedOrder = try JSONDecoder().decode(Order.self, from: data)
            confirmationMessage = "Ihre Bestellung über \(decodedOrder.quantity)x \(Order.types[decodedOrder.type].lowercased()) Cupcakes ist auf dem Weg!"
            showingConfirmation = true
        } catch {
            print("Checkout fehlgeschlagen: \(error.localizedDescription)")
        }
    }

Daten speichern

Ich kenne 3 Möglichkeiten, Daten in Swift/UI zu speichern:

  • UserDefaults: Am besten geeignet, um kleine Datenmengen zu speichern. Zum Beispiel App-Einstellungen.
  • TODO Schreiben in das Dokumentenverzeichnis
  • SwiftData

UserDefaults

Wir benötigen ein paar Dinge:

  1. Unsere Daten müssen Codable sein, damit wir später JSONEncoder erstellen können
  2. UserDefaults zum Speichern und Laden unserer Daten
  3. Ein benutzerdefinierter Initialisierer für die Datenklasse, damit sie automatisch geladen wird
  4. Ein didSet für die Daten, damit sie immer automatisch gespeichert werden, wenn Daten hinzugefügt oder geändert werden.

Die Daten Codable zu machen, ist meistens nicht allzu schwer: Solange die Komponenten Codable sind, ist es auch die gesamte Klasse.

So kann das Speichern der Daten in einem didSet aussehen:

var items = [ExpenseItem]() {
    didSet {
        if let encoded = try? JSONEncoder().encode(items) {
            UserDefaults.standard.set(encoded, forKey: "Items")
        }
    }
}

Und so könnte ein Initialisierer aussehen, der die Daten lädt:

init() {
    if let savedItems = UserDefaults.standard.data(forKey: "Items") {
        if let decodedItems = try? JSONDecoder().decode([ExpenseItem].self, from: savedItems) {
            items = decodedItems
            return
        }
    }

    items = []
}

SwiftData

Die beweglichen Teile, die wir haben, sind

  • Das Model: Dies ist die Datenstruktur. Das/die Objekt(e) und seine/ihre Felder
  • Der ModelContainer: Dies ist der persistente Speicher. Denken Sie daran wie die Datei, in die die Daten auf dem Server geschrieben werden.
  • Der ModelContext: Das ist die im Speicher gehaltene Version Ihrer Daten und wo die Datenänderungen gehalten werden, bevor sie im ModelContainer gespeichert werden.

Um Ihre Software für die Verwendung von SwiftData zu aktivieren, erstellen Sie