Zum Inhalt springen

HandySwiftUI Extensions: SwiftUI-Entwicklung noch komfortabler

Entdecke leistungsstarke SwiftUI-Extensions für saubere optionale Bindings, intuitive Farbverwaltung, XML-basierte Textformatierung und mehr. Diese praxiserprobten Hilfsfunktionen helfen dir, eleganteren SwiftUI-Code zu schreiben und Boilerplate in deinen Apps zu reduzieren.

HandySwiftUI Extensions: SwiftUI-Entwicklung noch komfortabler

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 Extensions 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 Extensions haben sich in der Praxis immer wieder als besonders wertvoll erwiesen und könnten auch für deine SwiftUI-Projekte nützlich sein.

Komfortfunktionen für optionale Bindings

Die Operatoren ?? und ! sowie der isPresent-Modifier vereinfachen die Arbeit mit optionalen Werten in Bindings:

struct EditableProfile: View {
   @State private var profile: Profile?
   @State private var showAdvanced = false

   var body: some View {
       Form {
           // Standardwert für optionales Binding über den `??`-Operator
           TextField("Name", text: $profile?.name ?? "Anonymous")

           // Binding-Wert mit `!`-Operator negieren
           Toggle("Hide Details", isOn: !$showAdvanced)
       }
       // Optionales Binding für Sheet-Präsentation nutzen
       .sheet(isPresented: $profile.isPresent(wrappedType: Profile.self)) {
           ProfileEditor(profile: $profile)
       }
   }
}

Die Operatoren sind in allen möglichen Views nützlich, wenn du zum Beispiel mit optionalen Daten in Models arbeitest.

Farbverwaltung

Die umfassenden Color-Extensions bieten leistungsstarke Werkzeuge zur Farbmanipulation und Nutzung von Systemfarben:

struct ColorfulView: View {
   @State private var baseColor = Color.blue

   var body: some View {
       VStack {
           // Variationen der Basisfarbe erzeugen
           Rectangle()
               .fill(baseColor.change(.luminance, by: -0.2))
           Rectangle()
               .fill(baseColor)
           Rectangle()
               .fill(baseColor.change(.luminance, by: 0.2))

           // Mit Hex-Farben arbeiten
           Circle()
               .fill(Color(hex: "#FF5733"))

           // Farbkomponenten verwenden
           Text("HSB: \(baseColor.hsbo.hue), \(baseColor.hsbo.saturation), \(baseColor.hsbo.brightness)")
           Text("RGB: \(baseColor.rgbo.red), \(baseColor.rgbo.green), \(baseColor.rgbo.blue)")
       }
       .padding()
       // Semantische Systemfarben für eigene systemähnliche Komponenten nutzen
       .background(Color.systemBackground)
   }
}

Colorful view

Wenn du die Helligkeit einer Farbe anpasst, verwende .luminance statt .brightness aus dem HSB-Farbsystem. Luminanz bildet besser ab, wie Menschen Licht und Dunkelheit wahrnehmen – deshalb unterstützt HandySwiftUI auch den HLC-Farbraum.

Rich-Text-Formatierung

Die Textformatierungs-Extensions bieten eine praktische Möglichkeit, Rich Text mit gemischten Styles zu erstellen, inspiriert von XML-artigen Tags:

struct FormattedText: View {
   var body: some View {
       Text(
           format: "A <b>bold</b> new way to <i>style</i> your text with <star.fill/> and <b>mixed</b> <red>formatting</red>.",
           partialStyling: Dictionary.htmlLike.merging([
               "red": { $0.foregroundColor(.red) },
               "star.fill": { $0.foregroundColor(.yellow) }
           ]) { $1 }  // $1 zurückgeben (statt $0) bedeutet, dass hinzugefügte Keys bestehende überschreiben
       )
   }
}

Formatted text

Im obigen Beispiel wird das mitgelieferte .htmlLike-Styling von HandySwiftUI mit eigenen Tags kombiniert. Beachte, dass .htmlLike einfach Folgendes zurückgibt:

[
   "b": { $0.bold() },
   "sb": { $0.fontWeight(.semibold) },
   "i": { $0.italic() },
   "bi": { $0.bold().italic() },
   "sbi": { $0.fontWeight(.semibold).italic() },
   "del": { $0.strikethrough() },
   "ins": { $0.underline() },
   "sub": { $0.baselineOffset(-4) },
   "sup": { $0.baselineOffset(6) },
]

Alle XML-artigen Einträge, die mit /> enden, wie <star.fill/> im Beispiel oben, werden als SFSymbol gerendert. So kannst du SFSymbols ganz einfach direkt in deinem Text verwenden.

Bildverarbeitung

Einheitliche Extensions für die Bildverarbeitung mit UIImage und NSImage:

class ImageProcessor {
   func processImage(_ image: UIImage) {
       // Bild unter Beibehaltung des Seitenverhältnisses skalieren
       let resized = image.resized(maxWidth: 800, maxHeight: 600)

       // In verschiedene Formate konvertieren
       let pngData = image.webpData()
       let jpegData = image.webpData(compressionQuality: 0.8)
       let heicData = image.heicData(compressionQuality: 0.8)
   }
}

Beachte, dass all diese APIs optionale Werte zurückgeben – für Grenzfälle wie extrem niedrigen Speicher – aber in den meisten Fällen erfolgreich durchlaufen.

Komfortable Model-zu-View-Konvertierungen

HandySwiftUI bietet Initializer-Komfortfunktionen, die es einfach machen, deine Model-Typen direkt in SwiftUI-Views darzustellen:

enum Tab: CustomLabelConvertible {
    case home, profile, settings

    var description: String {
        switch self {
        case .home: "Home"
        case .profile: "Profile"
        case .settings: "Settings"
        }
    }

    var symbolName: String {
        switch self {
        case .home: "house.fill"
        case .profile: "person.circle"
        case .settings: "gear"
        }
    }
}

struct ContentView: View {
    @State private var selectedTab: Tab = .home

    var body: some View {
        TabView(selection: $selectedTab) {
            HomeView()
                // Tab-Item direkt aus Enum-Case erstellen
                .tabItem { Label(convertible: Tab.home) }
                .tag(Tab.home)

            ProfileView()
                .tabItem { Label(convertible: Tab.profile) }
                .tag(Tab.profile)

            SettingsView()
                .tabItem { Label(convertible: Tab.settings) }
                .tag(Tab.settings)
        }

        // Funktioniert auch mit Text- und Image-Views
        Text(convertible: selectedTab)  // Zeigt den Tab-Namen
        Image(convertible: selectedTab) // Zeigt das Tab-Icon
    }
}

Statt manuell Strings und Symbolnamen aus deinen Models zu extrahieren, kannst du sie auf CustomStringConvertible für Text, CustomSymbolConvertible für SF Symbols oder CustomLabelConvertible für beides konformieren. Dann nutze die komfortablen Initializer, um SwiftUI-Views direkt zu erstellen:

  • Text(convertible:) – Erstellt Text aus jedem CustomStringConvertible

  • Image(convertible:) – Erstellt SF-Symbol-Bilder aus jedem CustomSymbolConvertible

  • Label(convertible:) – Erstellt Text+Icon-Labels aus jedem CustomLabelConvertible

Dieses Muster funktioniert besonders gut mit Enums, die UI-Zustände, Menüoptionen oder Tabs repräsentieren, wie im Beispiel oben gezeigt.

Suchprefix-Hervorhebung

HandySwiftUI bietet eine elegante Möglichkeit, übereinstimmenden Text in Suchergebnissen hervorzuheben, damit Nutzer sofort sehen, welche Teile des Textes zu ihrer Suchanfrage passen:

struct SearchResultsView: View {
    @State private var searchText = ""
    let translations = [
        "Good morning!",
        "Good evening!",
        "How are you?",
        "Thank you very much!"
    ]

    var body: some View {
        List {
            ForEach(translations.filtered(by: searchText), id: \.self) { translation in
                // Bei Suche nach "go mo" wird "Go mo" in "Good morning!" hervorgehoben
                Text(translation.highlightMatchingTokenizedPrefixes(in: searchText))
            }
        }
        .searchable(text: $searchText)
    }
}

extension [String] {
    func filtered(by searchText: String) -> [String] {
        guard !searchText.isEmpty else { return Array(self) }
        return filter { $0.localizedCaseInsensitiveContains(searchText) }
    }
}

Common translations

Diese Hervorhebungsfunktion wurde ursprünglich für das „Common Translations”-Feature in der Menüleiste von TranslateKit entwickelt, wo sie Nutzern hilft, passende Phrasen in bestätigten Übersetzungen schnell zu erkennen. Die Funktion zerlegt den Suchtext in Tokens und hebt jeden passenden Prefix hervor – perfekt für:

  • Hervorhebung von Suchergebnissen in Listen oder Menüs

  • Autocomplete-Vorschläge mit visuellem Feedback

  • Filtern durch Textsammlungen mit sichtbarem Trefferkontext

  • Bessere Sichtbarkeit von Suchtreffern in Dokumentvorschauen

Die Hervorhebung ist standardmäßig case-insensitive und diakritik-insensitive, aber du kannst die Locale und den Font für die Hervorhebung anpassen. Das macht sie zu einem vielseitigen Werkzeug für jede Suchoberfläche, in der du übereinstimmende Textabschnitte betonen möchtest.

Leg noch heute los

Ich hoffe, du findest diese Extensions genauso nützlich in deinen Projekten wie ich in meinen. Wenn du Ideen für Verbesserungen oder zusätzliche Extensions 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 dritte von vier Artikeln, die die Features von HandySwiftUI erkunden. Schau dir die vorherigen Artikel über Neue Typen und View Modifier an, falls du sie noch nicht gelesen hast, und bleib dran für den letzten Beitrag über Styles!

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