これらの API を自分のアプリで4年間ブラッシュアップし続けてきましたが、ついに HandySwiftUI の最初のタグ付きリリースを公開できることを嬉しく思います。このパッケージには、この1年だけで10個のアプリをリリースする上で欠かせなかった様々なユーティリティと便利な API が含まれています。HandySwift が Foundation に対して行っているのと同様に、SwiftUI 開発をより便利にするためのパッケージです。
この記事では、TranslateKit、FreemiumKit、CrossCraft などのアプリ開発で特に役立った新しい型を厳選してご紹介します。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))
}
}
}
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
)
}
}
}
HandySwiftUI には一般的な絵文字やシンボルを含む Emoji と SFSymbol の列挙型が用意されています。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)")
}
}
}
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()
}
}
}
}
SideTabView は、アイコンとラベル付きの縦方向サイドバーを提供し、大画面向けに最適化されています。下部に配置するタブのサポートも含まれており、プラットフォーム固有のスタイリングやホバー効果も自動的に処理されます。
今すぐ始めよう
これらの型が、私にとってそうであるように、皆さんのプロジェクトでも役立つことを願っています。改善のアイデアや SwiftUI コミュニティに貢献できる新しい型の提案があれば、ぜひ GitHub で気軽にコントリビューションしてください:
github.comFlineDev / HandySwiftUIHandy SwiftUI features that didn’t make it into SwiftUI (yet)
これは HandySwiftUI の機能を紹介する全4回シリーズの第1回です。今後の View Modifiers、Extensions、Styles についての記事もお楽しみに!

