コンテンツへスキップ

HandySwiftUI Extensions:SwiftUI 開発をもっと便利に

クリーンなオプショナルバインディング、直感的な色管理、XML 風テキストフォーマットなど。ボイラープレートを削減しながら、よりエレガントな SwiftUI コードを書くための実戦テスト済みユーティリティをご紹介します。

HandySwiftUI Extensions:SwiftUI 開発をもっと便利に

これらの API を自分のアプリで4年間ブラッシュアップし続けてきましたが、ついに HandySwiftUI の最初のタグ付きリリースを公開できることを嬉しく思います。このパッケージには、この1年だけで10個のアプリをリリースする上で欠かせなかった様々なユーティリティと便利な API が含まれています。HandySwift が Foundation に対して行っているのと同様に、SwiftUI 開発をより便利にするためのパッケージです。

この記事では、TranslateKitFreemiumKitCrossCraft などのアプリ開発で特に役立ったエクステンションを厳選してご紹介します。HandySwiftUI にはもっと多くのユーティリティが含まれていますが、ここで紹介するエクステンションは実際のアプリケーションで何度もその価値を証明してきたものばかりです。皆さんの SwiftUI プロジェクトでもきっと役に立つはずです。

オプショナル Binding の便利機能

?? 演算子と ! 演算子、そして isPresent モディファイアにより、バインディングでのオプショナル値の扱いが簡単になります:

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

   var body: some View {
       Form {
           // ?? 演算子でオプショナルバインディングにデフォルト値を提供
           TextField("Name", text: $profile?.name ?? "Anonymous")

           // ! 演算子でバインディングの値を反転
           Toggle("Hide Details", isOn: !$showAdvanced)
       }
       // オプショナルバインディングをシート表示に使用
       .sheet(isPresented: $profile.isPresent(wrappedType: Profile.self)) {
           ProfileEditor(profile: $profile)
       }
   }
}

これらの演算子は、モデルのオプショナルデータを扱う際など、あらゆるビューで役立ちます。

色の管理

包括的な色のエクステンションが、色の操作やシステムカラーの活用に強力なツールを提供します:

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

   var body: some View {
       VStack {
           // ベースカラーのバリエーションを作成
           Rectangle()
               .fill(baseColor.change(.luminance, by: -0.2))
           Rectangle()
               .fill(baseColor)
           Rectangle()
               .fill(baseColor.change(.luminance, by: 0.2))

           // 16進数カラーを使用
           Circle()
               .fill(Color(hex: "#FF5733"))

           // カラーコンポーネントを取得
           Text("HSB: \(baseColor.hsbo.hue), \(baseColor.hsbo.saturation), \(baseColor.hsbo.brightness)")
           Text("RGB: \(baseColor.rgbo.red), \(baseColor.rgbo.green), \(baseColor.rgbo.blue)")
       }
       .padding()
       // カスタムのシステム風コンポーネントにセマンティックなシステムカラーを使用
       .background(Color.systemBackground)
   }
}

Colorful view

色の明るさを調整する際は、HSB カラーシステムの .brightness ではなく .luminance を使用してください。輝度は人間が光と闇をどう知覚するかをより正確に表現するため、HandySwiftUI は HLC カラースペースのサポートを含んでいます。

リッチテキストフォーマット

テキストフォーマットのエクステンションは、XML 風のタグにインスパイアされた混合スタイルのリッチテキストを簡単に作成する方法を提供します:

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 を返すことで、追加されたキーが既存のキーを上書き
       )
   }
}

Formatted text

上記の例では、HandySwiftUI に付属する組み込みの .htmlLike スタイリングとカスタムタグを組み合わせています。.htmlLike は以下を返すだけのシンプルなものです:

[
   "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) },
]

/> で終わる XML 風エントリ(上記の例の <star.fill/> など)は SFSymbol としてレンダリングされます。これにより、テキスト内で簡単に SFSymbol を使用できます。

画像処理

UIImageNSImage に対する統一されたエクステンションです:

class ImageProcessor {
   func processImage(_ image: UIImage) {
       // アスペクト比を維持したまま画像をリサイズ
       let resized = image.resized(maxWidth: 800, maxHeight: 600)

       // 異なるフォーマットに変換
       let pngData = image.webpData()
       let jpegData = image.webpData(compressionQuality: 0.8)
       let heicData = image.heicData(compressionQuality: 0.8)
   }
}

これらの API はすべてオプショナル値を返します。システムのメモリが極端に不足している場合などのエッジケースに対応するためですが、ほとんどの場合は成功します。

モデルからビューへの便利な変換

HandySwiftUI は、モデル型を SwiftUI ビューに直接表示するためのイニシャライザの便利機能を提供します:

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()
                // 列挙型のケースから直接タブアイテムを作成
                .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)
        }

        // Text や Image ビューでも使えます
        Text(convertible: selectedTab)  // タブ名を表示
        Image(convertible: selectedTab) // タブアイコンを表示
    }
}

モデルから手動で文字列やシンボル名を取り出す代わりに、CustomStringConvertible(テキスト用)、CustomSymbolConvertible(SF Symbol 用)、CustomLabelConvertible(両方)に準拠させることで、便利なイニシャライザを使って SwiftUI ビューを直接作成できます:

  • Text(convertible:) - CustomStringConvertible に準拠した型からテキストを作成

  • Image(convertible:) - CustomSymbolConvertible に準拠した型から SF Symbol イメージを作成

  • Label(convertible:) - CustomLabelConvertible に準拠した型からテキスト+アイコンのラベルを作成

このパターンは、上記の例のように UI の状態、メニューオプション、タブを表す列挙型と特に相性が良いです。

検索プレフィックスのハイライト

HandySwiftUI は、検索結果内のマッチしたテキストをハイライトするエレガントな方法を提供します。検索クエリのどの部分がマッチしたかをユーザーに分かりやすく表示できます:

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
                // 「go mo」で検索すると「Good morning!」の「Go mo」がハイライトされます
                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

このハイライト機能は、もともと TranslateKit のメニューバー「Common Translations」機能のために開発されました。確認済みの翻訳の中からマッチするフレーズを素早く見つけるのに役立ちます。この関数は検索テキストをトークンに分解し、マッチする各プレフィックスをハイライトします。以下のようなケースに最適です:

  • リストやメニューでの検索結果ハイライト

  • 視覚的フィードバック付きのオートコンプリート候補

  • マッチコンテキストを表示しながらのテキストコレクションのフィルタリング

  • ドキュメントプレビューでの検索マッチの可視化

ハイライトはデフォルトで大文字・小文字を区別せず、ダイアクリティカル(発音区別符号)も無視しますが、ロケールやハイライトに使用するフォントをカスタマイズできます。テキストのマッチ部分を強調したい検索インターフェースに汎用的に使えるツールです。

今すぐ始めよう

これらのエクステンションが、私にとってそうであるように、皆さんのプロジェクトでも役立つことを願っています。改善のアイデアや SwiftUI コミュニティに貢献できる新しいエクステンションの提案があれば、ぜひ GitHub で気軽にコントリビューションしてください:

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

これは HandySwiftUI の機能を紹介する全4回シリーズの第3回です。まだご覧になっていなければ、前回の New TypesView Modifiers の記事もチェックしてみてください。最終回の Styles についての記事もお楽しみに!

この記事が参考になりましたか?BlueskyMastodonでフォローして、Swiftのヒントやインディー開発の最新情報をチェックしてください。