コンテンツへスキップ

HandySwiftUI の新しい型:SwiftUI 開発に欠かせないビューと型

#if チェック不要のプラットフォーム固有値から、高機能な選択コントロール、非同期状態管理まで。SwiftUI 開発のよくあるギャップを埋める、実戦で鍛えられたビューと型をご紹介します。

HandySwiftUI の新しい型:SwiftUI 開発に欠かせないビューと型

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

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

プラットフォーム固有の値

HandySwiftUI は、プラットフォームごとの値をエレガントに扱う方法を提供します:

struct AdaptiveView: View {
    enum TextStyle {
        case compact, regular, expanded
    }

    var body: some View {
        VStack {
            // プラットフォームごとに異なる数値
            Text("Welcome")
                .padding(Platform.value(default: 20.0, phone: 12.0))

            // プラットフォームごとに異なる色
            Circle()
                .fill(Platform.value(default: .blue, mac: .indigo, pad: .purple, vision: .cyan))
        }
    }
}

Last30days

FreemiumKit のタイトルで .font(Platform.value(default: .title2, phone: .headline)) を使い、プラットフォーム間で統一感のある見た目を実現しています。

Platform.value はどんな型でも使えます。単純な数値からカラー、フォント、独自のカスタム型まで対応可能です。デフォルト値を指定して、必要なプラットフォームだけオーバーライドすればOKです。特に便利なのが、iPad 専用の pad というケースがあるため、iPhone とタブレットを個別に指定できる点です。

これは私が最も多用している HandySwiftUI のヘルパーで、大量の #if チェックのボイラープレートを省いてくれます。シンプルですが、とても強力です!

読みやすいプレビュー検出

開発中にフェイクデータの提供やローディング状態のシミュレーションが可能です:

Task {
   loadState = .inProgress

   if Xcode.isRunningForPreviews {
       // プレビューでネットワーク遅延をシミュレート
       try await Task.sleep(for: .seconds(1))
       self.data = Data()
       loadState = .successful
   } else {
       do {
           self.data = try await loadFromAPI()
           loadState = .successful
       } catch {
           loadState = .failed(error: error.localizedDescription)
       }
   }
}

Xcode.isRunningForPreviews を使えば、SwiftUI プレビューでのみ実際のネットワークリクエストをスキップし、即座にまたは遅延付きでフェイクレスポンスを返すことができます。プロトタイピングや UI 開発に最適です。また、API のレート制限、統計を歪めるアナリティクスイベント、リクエスト課金のサービスなど、開発中に限りあるリソースの消費を避けたい場合にも便利です。if !Xcode.isRunningForPreviews でラップするだけで済みます。

効率的な画像読み込み

CachedAsyncImage はキャッシュ機能を内蔵した効率的な画像読み込みを提供します:

struct ProductView: View {
    let product: Product

    var body: some View {
        VStack {
            CachedAsyncImage(url: product.imageURL)
                .frame(width: 200, height: 200)
                .clipShape(RoundedRectangle(cornerRadius: 10))

            Text(product.name)
                .font(.headline)
        }
    }
}

内部の Image ビューには .resizable().aspectRatio(contentMode: .fill) が既に適用されています。

高機能な選択コントロール

用途に応じた複数の高機能ピッカーを用意しています:

struct SettingsView: View {
    @State private var selectedMood: Mood?
    @State private var selectedColors: Set<Color> = []
    @State private var selectedEmoji: Emoji?

    var body: some View {
        Form {
            // アイコン付き縦方向オプションピッカー
            VPicker("Select Mood", selection: $selectedMood)

            // カスタムスタイル付き横方向ピッカー
            HPicker("Rate your experience", selection: $selectedMood)

            // プラットフォーム適応型の複数選択
            MultiSelector(
                label: { Text("Colors") },
                optionsTitle: "Select Colors",
                options: [.red, .blue, .green],
                selected: $selectedColors,
                optionToString: \.description
            )

            // 検索可能なグリッドピッカー(絵文字や SF Symbol の選択に)
            SearchableGridPicker(
                title: "Choose Emoji",
                options: Emoji.allCases,
                selection: $selectedEmoji
            )
        }
    }
}

Settings view

HandySwiftUI には一般的な絵文字やシンボルを含む EmojiSFSymbol の列挙型が用意されています。SearchableOption に準拠して各ケースに searchTerms を提供することで、独自の列挙型を作成して検索機能を実現することもできます。

非同期状態管理

ProgressState を使って、型安全な状態管理で非同期処理を追跡できます:

struct DocumentView: View {
    @State private var loadState: ProgressState<String> = .notStarted

    var body: some View {
        Group {
            switch loadState {
            case .notStarted:
                AsyncButton("Load Document") {
                    loadState = .inProgress
                    try await loadDocument()
                    loadState = .successful
                } catchError: { error in
                    loadState = .failed(error: error.localizedDescription)
                }

            case .inProgress:
                ProgressView("Loading document...")

            case .failed(let errorMessage):
                VStack {
                    Text("Failed to load document:")
                        .foregroundStyle(.secondary)
                    Text(errorMessage)
                        .foregroundStyle(.red)

                  AsyncButton("Try Again") {
                      loadState = .inProgress
                      try await loadDocument()
                      loadState = .successful
                  } catchError: { error in
                      loadState = .failed(error: error.localizedDescription)
                  }
                }

            case .successful:
                VStack {
                    DocumentContent()
                }
            }
        }
    }
}

この例では、すべての状態を型安全に処理する方法を示しています:

  • .notStarted は初期のロードボタンを表示

  • .inProgress はローディングインジケーターを表示

  • .failed はエラーとリトライオプションを表示

  • .successful は読み込んだコンテンツを表示

NSOpenPanel を SwiftUI で使う

ネイティブの macOS ファイルアクセスを SwiftUI にブリッジします。セキュリティスコープ付きリソースの処理に特に便利です:

struct SecureFileLoader {
    @State private var apiKey = ""

    func loadKeyFile(at fileURL: URL) async {
        #if os(macOS)
        // macOS ではファイルアクセスにユーザーの同意が必要
        let panel = OpenPanel(
            filesWithMessage: "Provide access to read key file",
            buttonTitle: "Allow Access",
            contentType: .data,
            initialDirectoryUrl: fileURL
        )
        guard let url = await panel.showAndAwaitSingleSelection() else { return }
        #else
        let url = fileURL
        #endif

        guard url.startAccessingSecurityScopedResource() else { return }
        defer { url.stopAccessingSecurityScopedResource() }

        do {
            apiKey = try String(contentsOf: url)
        } catch {
            print("Failed to load file: \(error.localizedDescription)")
        }
    }
}

Open panel

FreemiumKit から直接取った例で、macOS でドラッグされたアイテムのセキュリティスコープ付きファイルアクセスを OpenPanel がどのように簡素化するかを示しています。クロスプラットフォーム互換性も維持されています。

縦方向タブナビゲーション

SwiftUI の TabView の代替として、macOS や iPadOS アプリでよく見られるサイドバースタイルのナビゲーションを実装します:

struct MainView: View {
    enum Tab: String, CaseIterable, Identifiable, CustomLabelConvertible {
        case documents, recents, settings

        var id: Self { self }
        var description: String {
            rawValue.capitalized
        }
        var symbolName: String {
            switch self {
            case .documents: "folder"
            case .recents: "clock"
            case .settings: "gear"
            }
        }
    }

    @State private var selectedTab: Tab = .documents

    var body: some View {
        SideTabView(
            selection: $selectedTab,
            bottomAlignedTabs: 1  // 設定を下部に配置
        ) { tab in
            switch tab {
            case .documents:
                DocumentList()
            case .recents:
                RecentsList()
            case .settings:
                SettingsView()
            }
        }
    }
}

Side tab view

SideTabView は、アイコンとラベル付きの縦方向サイドバーを提供し、大画面向けに最適化されています。下部に配置するタブのサポートも含まれており、プラットフォーム固有のスタイリングやホバー効果も自動的に処理されます。

今すぐ始めよう

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

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

これは HandySwiftUI の機能を紹介する全4回シリーズの第1回です。今後の View ModifiersExtensionsStyles についての記事もお楽しみに!

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