これらの API を自分のアプリで4年間ブラッシュアップし続けてきましたが、ついに HandySwiftUI の最初のタグ付きリリースを公開できることを嬉しく思います。このパッケージには、この1年だけで10個のアプリをリリースする上で欠かせなかった様々なユーティリティと便利な API が含まれています。HandySwift が Foundation に対して行っているのと同様に、SwiftUI 開発をより便利にするためのパッケージです。
この記事では、TranslateKit、FreemiumKit、CrossCraft などのアプリ開発で特に役立ったビューモディファイアを厳選してご紹介します。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)
}
}
}
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)
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 }
}
}
}
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 の記事もチェックしてみてください。今後の Extensions と Styles についての記事もお楽しみに!

