コンテンツへスキップ

HandySwiftUI View Modifiers:SwiftUI コードをスッキリさせる

スマートな色のコントラスト調整、効率的なエラーハンドリング、簡潔な削除フロー、プラットフォーム固有のスタイリングまで。よくあるボイラープレートを排除し、メンテナンスしやすいアプリ作りを支援する SwiftUI モディファイアをご紹介します。

HandySwiftUI View Modifiers:SwiftUI コードをスッキリさせる

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

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

スマートな色のコントラスト調整

foregroundStyle(_:minContrast:) モディファイアは、色のコントラストを自動調整してテキストの可読性を確保します。動的な色や、カラースキームによってはコントラストが低くなりがちな .yellow のようなシステムカラーに便利です:

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

    var body: some View {
        HStack {
            // コントラスト調整なし
            Text("Maybe hard to read")
                .foregroundStyle(dynamicColor)

            // 自動コントラスト調整あり
            Text("Always readable")
                .foregroundStyle(dynamicColor, minContrast: 0.5)
        }
    }
}

Yellow with contrast

TranslateKit のこの警告インジケーターは .yellow を使用していますが、ライトモードでも読みやすいように適切なコントラストを確保しています。ダークモードではそのまま黄色で表示されます。

minContrast パラメータ(0から1の範囲)は、ライトモードでは白、ダークモードでは黒に対する最小コントラスト比を輝度値(知覚される明るさ)に基づいて決定します。これにより、現在のカラースキームに関係なくテキストの可読性が保たれます。

エラーハンドリング付き Task

throwingTask モディファイアは、SwiftUI ビューでの非同期エラーハンドリングを効率化します。SwiftUI 組み込みの .task モディファイアは手動で do-catch ブロックを書く必要がありますが、throwingTask は専用のエラーハンドラークロージャを提供します:

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

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

このタスクは .task と同様に動作し、ビューの表示時に開始され、非表示時にキャンセルされます。catchError クロージャはオプションなので、エラー処理が不要な場合は省略できます。task モディファイアが埋めきれなかったギャップを補う機能です。

プラットフォーム固有のスタイリング

マルチプラットフォーム UI を細かく制御できるプラットフォームモディファイアのフルセットが用意されています:

struct AdaptiveInterface: View {
    var body: some View {
        ContentView()
            // macOS のみでパディングを追加
            .macOSOnlyPadding(.all, 20)
            // プラットフォーム固有のスタイル
            .macOSOnly { $0.frame(minWidth: 800) }
            .iOSOnly { $0.navigationViewStyle(.stack) }
    }
}

この例では、プラットフォーム固有のスタイリング用モディファイアを紹介しています:

  • .macOSOnlyPadding は、Form などのコンテナにデフォルトのパディングがない macOS でのみパディングを追加します

  • .macOSOnlyFrame は macOS で必要な最小ウィンドウサイズを設定します

  • プラットフォームモディファイア(.iOSOnly.macOSOnly.iOSExcluded など)は iOS、macOS、tvOS、visionOS、watchOS で利用可能で、特定のプラットフォームにのみビュー変更を適用できます

これらのモディファイアにより、コードの可読性とメンテナンス性を保ちながら、プラットフォームに適したインターフェースを作成できます。

角丸付きボーダー

SwiftUI では角丸付きのボーダーを追加する簡単な方法がありません。標準的なアプローチは冗長なオーバーレイコードが必要で、覚えにくいです:

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

HandySwiftUI はこれを便利なボーダーモディファイアで簡素化します:

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

State badges

TranslateKit のバッジでは、このような角丸ボーダーに使用しています。

条件付きモディファイア

条件付きのビュー変更をきれいに処理するためのモディファイアスイートです:

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

    var body: some View {
        ContentView()
            // 条件に基づいて異なるモディファイアを適用
            .applyIf(isEditMode) {
                $0.overlay(EditingTools())
            } else: {
                $0.overlay(ViewingTools())
            }

            // オプショナルが存在する場合のみモディファイアを適用
            .ifLet(accentColor) { view, color in
                view.tint(color)
            }
    }
}

この例では、ブール条件に基づいて異なるビュー変更を適用する .applyIf と、Swift の if let 文のように動作する .ifLet を紹介しています。.ifLet はクロージャ内でオプショナルでない値にアクセスできます。どちらのモディファイアも SwiftUI ビューのボイラープレートコードを削減するのに役立ちます。

アプリのライフサイクル処理

アプリの状態変化にエレガントに対応できます:

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

    var body: some View {
        PlayerContent(player: player)
            .onAppResignActive {
                // アプリがバックグラウンドに移行したら再生を一時停止
                player.pause()
            }
            .onAppBecomeActive {
                // アプリがアクティブになったら状態を復元
                player.checkPlaybackState()
            }
    }
}

これらのモディファイアが連携することで、より流れるようなメンテナンスしやすい SwiftUI 開発体験が実現します。ボイラープレートコードを削減しながら、ユーザーインターフェースの品質と一貫性を向上させます。

削除確認ダイアログ

SwiftUI の確認ダイアログは、削除アクション(特にリストからのアイテム削除)で繰り返しのボイラープレートコードが必要です:

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 は専用のモディファイアでこれを簡素化します:

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

CrossCraft でのパズル削除には、誤削除を防ぐための確認ダイアログが表示されます。

この例では、.confirmDeleteDialog が確認から実行まで削除フロー全体を1つのモディファイアで処理する方法を示しています。ダイアログは約40の言語に自動ローカライズされ、プラットフォームのデザインガイドラインに従います。異なるメッセージを表示したい場合は、オプションの message パラメータを指定できます。リストが関係ないケース用に、ブール値を受け取るオーバーロードもあります。

今すぐ始めよう

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

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

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

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