Zum Inhalt springen

HandySwiftUI View Modifier: Dein SwiftUI-Code wird schlanker

Von intelligentem Farbkontrast über vereinfachtes Error-Handling bis hin zu Lösch-Workflows und plattformspezifischem Styling – entdecke die SwiftUI-Modifier, die gängigen Boilerplate-Code eliminieren und dir helfen, wartbarere Apps zu entwickeln.

HandySwiftUI View Modifier: Dein SwiftUI-Code wird schlanker

Nach 4 Jahren Feinschliff an diesen APIs in meinen eigenen Apps freue ich mich, das erste getaggte Release von HandySwiftUI zu teilen. Dieses Paket enthält verschiedene Hilfsfunktionen und Convenience-APIs, die mir enorm dabei geholfen haben, allein im letzten Jahr 10 Apps auszuliefern. Es bietet Komfortfunktionen für die SwiftUI-Entwicklung, ähnlich wie mein HandySwift-Paket für Foundation.

In diesem Artikel stelle ich eine Auswahl der View Modifier vor, die sich in meiner täglichen Arbeit an Apps wie TranslateKit, FreemiumKit und CrossCraft am meisten bewährt haben. HandySwiftUI enthält zwar noch viele weitere Hilfsfunktionen, aber diese Modifier haben sich in der Praxis immer wieder als besonders wertvoll erwiesen und könnten auch für deine SwiftUI-Projekte nützlich sein.

Intelligenter Farbkontrast

Der foregroundStyle(_:minContrast:)-Modifier sorgt dafür, dass Text lesbar bleibt, indem er den Farbkontrast automatisch anpasst. Das ist nützlich für dynamische Farben oder Systemfarben wie .yellow, die in bestimmten Farbschemata schlechten Kontrast haben können:

struct AdaptiveText: View {
    @State private var dynamicColor: Color = .yellow

    var body: some View {
        HStack {
            // Ohne Kontrastanpassung
            Text("Maybe hard to read")
                .foregroundStyle(dynamicColor)

            // Mit automatischer Kontrastanpassung
            Text("Always readable")
                .foregroundStyle(dynamicColor, minContrast: 0.5)
        }
    }
}

Yellow with contrast

Dieser Warnungsindikator in TranslateKit nutzt .yellow, stellt aber einen guten Kontrast auch im Light Mode sicher, um lesbar zu bleiben. Im Dark Mode bleibt es gelb.

Der minContrast-Parameter (Bereich von 0 bis 1) bestimmt das minimale Kontrastverhältnis gegenüber Weiß (im Light Mode) oder Schwarz (im Dark Mode) anhand des Luminanzwerts (wahrgenommene Helligkeit). So bleibt Text unabhängig vom aktuellen Farbschema lesbar.

Tasks mit Error-Handling

Der throwingTask-Modifier vereinfacht das asynchrone Error-Handling in SwiftUI-Views. Anders als SwiftUIs eingebauter .task-Modifier, der manuelle do-catch-Blöcke erfordert, bietet throwingTask einen dedizierten Error-Handler-Closure:

struct DataView: View {
    @State private var error: Error?

    var body: some View {
        ContentView()
            .throwingTask {
                try await loadData()
            } catchError: { error in
                self.error = error
            }
    }
}

Der Task verhält sich ähnlich wie .task – er startet, wenn die View erscheint, und wird abgebrochen, wenn sie verschwindet. Der catchError-Closure ist optional, du kannst ihn also weglassen, wenn du Fehler nicht behandeln musst – und füllst damit genau die Lücke, die der task-Modifier offengelassen hat.

Plattformspezifisches Styling

Ein vollständiger Satz plattformspezifischer Modifier ermöglicht präzise Kontrolle über Multi-Platform-UI:

struct AdaptiveInterface: View {
    var body: some View {
        ContentView()
            // Padding nur auf macOS hinzufügen
            .macOSOnlyPadding(.all, 20)
            // Plattformspezifische Styles
            .macOSOnly { $0.frame(minWidth: 800) }
            .iOSOnly { $0.navigationViewStyle(.stack) }
    }
}

Das Beispiel zeigt Modifier für plattformspezifisches Styling:

  • .macOSOnlyPadding fügt Padding nur auf macOS hinzu, wo Container wie Form kein Standard-Padding haben

  • .macOSOnlyFrame setzt die auf macOS nötigen Mindestfenstergrößen

  • Plattform-Modifier (.iOSOnly, .macOSOnly, .iOSExcluded, etc.) sind für iOS, macOS, tvOS, visionOS und watchOS verfügbar und erlauben die gezielte Anwendung von View-Modifikationen auf bestimmten Plattformen

Diese Modifier helfen dir, plattformgerechte Oberflächen zu erstellen und dabei den Code sauber und wartbar zu halten.

Border mit Eckenradius

SwiftUI bietet keinen einfachen Weg, einer View eine Border mit Eckenradius hinzuzufügen. Der Standardansatz erfordert umständlichen Overlay-Code, den man sich schwer merken kann:

Text("Without HandySwiftUI")
    .padding()
    .overlay(
        RoundedRectangle(cornerRadius: 12)
            .strokeBorder(.blue, lineWidth: 2)
    )

HandySwiftUI vereinfacht das mit einem praktischen Border-Modifier:

Text("With HandySwiftUI")
    .padding()
    .roundedRectangleBorder(.blue, cornerRadius: 12, lineWidth: 2)

State badges

Badges in TranslateKit nutzen das zum Beispiel für abgerundete Rahmen.

Bedingte Modifier

Eine Reihe von Modifiern für den sauberen Umgang mit bedingten View-Anpassungen:

struct DynamicContent: View {
    @State private var isEditMode = false
    @State private var accentColor: Color?

    var body: some View {
        ContentView()
            // Unterschiedliche Modifier je nach Bedingung anwenden
            .applyIf(isEditMode) {
                $0.overlay(EditingTools())
            } else: {
                $0.overlay(ViewingTools())
            }

            // Modifier nur anwenden, wenn Optional einen Wert hat
            .ifLet(accentColor) { view, color in
                view.tint(color)
            }
    }
}

Das Beispiel zeigt .applyIf, das verschiedene View-Modifikationen basierend auf einer Boolean-Bedingung anwendet, und .ifLet, das wie Swifts if let-Statement funktioniert – es bietet nicht-optionalen Zugriff auf optionale Werte innerhalb des Closures. Beide Modifier helfen, Boilerplate-Code in SwiftUI-Views zu reduzieren.

App-Lifecycle-Handling

Reagiere elegant auf App-Zustandsänderungen:

struct MediaPlayerView: View {
    @StateObject private var player = VideoPlayer()

    var body: some View {
        PlayerContent(player: player)
            .onAppResignActive {
                // Wiedergabe pausieren, wenn die App in den Hintergrund geht
                player.pause()
            }
            .onAppBecomeActive {
                // Zustand wiederherstellen, wenn die App aktiv wird
                player.checkPlaybackState()
            }
    }
}

Diese Modifier arbeiten zusammen, um eine flüssigere und wartbarere SwiftUI-Entwicklung zu ermöglichen, indem sie Boilerplate-Code reduzieren und gleichzeitig Qualität und Konsistenz deiner Benutzeroberfläche verbessern.

Löschbestätigungs-Dialoge

SwiftUIs Bestätigungsdialoge erfordern bei Löschaktionen immer wieder den gleichen Boilerplate-Code, besonders beim Löschen von Elementen aus einer Liste:

struct TodoView: View {
    @State private var showDeleteConfirmation = false
    @State private var todos = ["Buy milk", "Walk dog"]
    @State private var todoToDelete: String?

    var body: some View {
        List {
            ForEach(todos, id: \.self) { todo in
                Text(todo)
                    .swipeActions {
                        Button("Delete", role: .destructive) {
                            todoToDelete = todo
                            showDeleteConfirmation = true
                        }
                    }
            }
        }
        .confirmationDialog("Are you sure?", isPresented: $showDeleteConfirmation) {
            Button("Delete", role: .destructive) {
                if let todo = todoToDelete {
                    todos.removeAll { $0 == todo }
                    todoToDelete = nil
                }
            }
            Button("Cancel", role: .cancel) {
                todoToDelete = nil
            }
        } message: {
            Text("This delete action cannot be undone. Continue?")
        }
    }
}

HandySwiftUI vereinfacht das mit einem dedizierten Modifier:

struct TodoView: View {
    @State private var todoToDelete: String?
    @State private var todos = ["Buy milk", "Walk dog"]

    var body: some View {
        List {
            ForEach(todos, id: \.self) { todo in
                Text(todo)
                    .swipeActions {
                        Button("Delete", role: .destructive) {
                            todoToDelete = todo
                        }
                    }
            }
        }
        .confirmDeleteDialog(item: $todoToDelete) { item in
            todos.removeAll { $0 == item }
        }
    }
}

Confirm delete

Puzzle-Löschung in CrossCraft mit einem Bestätigungsdialog, um versehentliches Löschen zu vermeiden.

Das Beispiel zeigt, wie .confirmDeleteDialog den gesamten Lösch-Workflow – von der Bestätigung bis zur Ausführung – mit einem einzigen Modifier abwickelt. Der Dialog wird automatisch in ca. 40 Sprachen lokalisiert und folgt den Plattform-Designrichtlinien. Du kannst einen optionalen message-Parameter angeben, falls du eine andere Nachricht anzeigen möchtest. Es gibt auch eine Overload-Variante, die einen Boolean für Situationen nimmt, in denen keine Liste involviert ist.

Leg noch heute los

Ich hoffe, du findest diese Modifier genauso nützlich in deinen Projekten wie ich in meinen. Wenn du Ideen für Verbesserungen oder zusätzliche Modifier hast, die der SwiftUI-Community zugutekommen könnten, trage gerne auf GitHub bei:

github.comFlineDev / HandySwiftUIHandy SwiftUI features that didn’t make it into SwiftUI (yet)

Dies ist der zweite von vier Artikeln, die die Features von HandySwiftUI erkunden. Schau dir den vorherigen Artikel über Neue Typen an, falls du ihn noch nicht gelesen hast, und bleib dran für die kommenden Beiträge über Extensions und Styles!

Hat dir dieser Beitrag gefallen? Folge mir auf Bluesky und Mastodon für mehr Swift-Tipps und Indie-Dev-Updates.