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)
}
}
}
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:
.macOSOnlyPaddingfügt Padding nur auf macOS hinzu, wo Container wieFormkein Standard-Padding haben.macOSOnlyFramesetzt die auf macOS nötigen MindestfenstergrößenPlattform-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)
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 }
}
}
}
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!

