MENU

【SwiftUI入門】Formの使い方完全ガイド!設定画面を簡単に作る方法

SwiftUIでアプリを開発していると、ユーザー情報の入力画面や設定画面を作る機会が必ずあります。

「iOSの設定アプリのような画面を作りたいけど、どうすればいいの?」 「入力フォームを綺麗にレイアウトするのが難しい…」

そんな悩みを解決してくれるのが、SwiftUIのFormです。

この記事では、SwiftUI初心者の方に向けて、Formの基本から実践的な使い方まで、サンプルコードとともにわかりやすく解説します。

目次

Formとは?

Formは、SwiftUIで入力フォームや設定画面を作成するためのコンテナビューです。

iOS標準の「設定」アプリのような見た目のUIを、わずか数行のコードで実装できる優れものです。

Formの特徴

Formを使うと、以下のことが自動的に行われます。

  • 適切な背景色とスタイルの適用
  • セルの区切り線の自動表示
  • タップ可能な領域の最適化
  • スクロール機能の自動追加
  • プラットフォームに応じたデザイン調整

つまり、細かいレイアウト調整をしなくても、プロフェッショナルな見た目の画面が作れるのです。

Formの基本的な使い方

最もシンプルなFormは、以下のように書けます。

import SwiftUI

struct BasicFormView: View {
    @State private var username = ""
    @State private var isEnabled = false
    
    var body: some View {
        Form {
            TextField("ユーザー名", text: $username)
            Toggle("通知を有効にする", isOn: $isEnabled)
        }
    }
}

これだけで、入力欄とスイッチが適切に配置されたフォームが完成します。

VStackとの違い

同じコンポーネントをVStackで並べた場合と比較してみましょう。

// VStackの場合
VStack {
    TextField("ユーザー名", text: $username)
    Toggle("通知を有効にする", isOn: $isEnabled)
}
.padding()

// Formの場合
Form {
    TextField("ユーザー名", text: $username)
    Toggle("通知を有効にする", isOn: $isEnabled)
}

Formを使うと、背景色、セルの区切り、タップ領域などが自動的に最適化されるため、より洗練された見た目になります。

Sectionでグループ化する

Formの真価は、Sectionと組み合わせた時に発揮されます。

Sectionを使うと、関連する項目をグループ化して、見出しや注釈を付けることができます。

Form {
    Section(header: Text("アカウント")) {
        TextField("ユーザー名", text: $username)
        TextField("メールアドレス", text: $email)
    }
    
    Section(header: Text("設定")) {
        Toggle("通知", isOn: $notifications)
        Toggle("ダークモード", isOn: $darkMode)
    }
    
    Section(header: Text("その他"), footer: Text("バージョン 1.0.0")) {
        Button("ログアウト") {
            // ログアウト処理
        }
    }
}

Sectionの3つの要素

Sectionには、以下の3つの要素を設定できます。

  1. header:セクションの見出し
  2. footer:セクションの注釈や補足説明
  3. content:セクション内のコンテンツ
Section(
    header: Text("見出し"),
    footer: Text("補足説明")
) {
    // ここにコンテンツ
}

Formで使える主なコンポーネント

Formには、様々な入力コンポーネントを配置できます。ここでは、よく使う7つのコンポーネントを紹介します。

1. TextField:テキスト入力

ユーザーにテキストを入力してもらうための基本的なコンポーネントです。

@State private var name = ""
@State private var email = ""

Form {
    Section(header: Text("プロフィール")) {
        TextField("名前", text: $name)
        
        TextField("メールアドレス", text: $email)
            .keyboardType(.emailAddress)
            .textInputAutocapitalization(.never)
    }
}

主なmodifier:

  • .keyboardType():キーボードの種類を指定
  • .textInputAutocapitalization():自動大文字変換の設定

2. SecureField:パスワード入力

パスワードなど、入力内容を隠したい場合に使います。

@State private var password = ""

Form {
    Section(header: Text("ログイン")) {
        TextField("ユーザー名", text: $username)
        SecureField("パスワード", text: $password)
    }
}

3. Toggle:オン・オフの切り替え

設定のオン・オフを切り替えるスイッチです。

@State private var notifications = true
@State private var darkMode = false

Form {
    Section(header: Text("設定")) {
        Toggle("プッシュ通知", isOn: $notifications)
        Toggle("ダークモード", isOn: $darkMode)
    }
}

4. Picker:選択肢から選ぶ

複数の選択肢から1つを選ぶためのコンポーネントです。

enum Theme: String, CaseIterable, Identifiable {
    case light = "ライト"
    case dark = "ダーク"
    case auto = "自動"
    
    var id: String { self.rawValue }
}

@State private var selectedTheme = Theme.auto

Form {
    Section(header: Text("外観")) {
        Picker("テーマ", selection: $selectedTheme) {
            ForEach(Theme.allCases) { theme in
                Text(theme.rawValue).tag(theme)
            }
        }
    }
}

Pickerをタップすると、自動的に選択画面に遷移します。

5. Stepper:数値の増減

数値を増減させるためのコンポーネントです。

@State private var quantity = 1
@State private var age = 20

Form {
    Section(header: Text("数量")) {
        Stepper("数量: \(quantity)", value: $quantity, in: 1...10)
        Stepper("年齢: \(age)", value: $age, in: 0...100, step: 1)
    }
}

主なパラメータ:

  • in:最小値と最大値の範囲
  • step:増減の単位(デフォルトは1)

6. DatePicker:日付・時刻の選択

日付や時刻を選択するためのコンポーネントです。

@State private var selectedDate = Date()
@State private var selectedTime = Date()

Form {
    Section(header: Text("日時")) {
        DatePicker("日付", selection: $selectedDate, displayedComponents: .date)
        DatePicker("時刻", selection: $selectedTime, displayedComponents: .hourAndMinute)
        DatePicker("日時", selection: $selectedDate)
    }
}

displayedComponents:

  • .date:日付のみ
  • .hourAndMinute:時刻のみ
  • 指定なし:日付と時刻の両方

7. Slider:スライダー

連続的な数値を選択するためのコンポーネントです。

@State private var volume = 50.0
@State private var brightness = 0.5

Form {
    Section(header: Text("設定")) {
        HStack {
            Text("音量")
            Slider(value: $volume, in: 0...100, step: 1)
            Text("\(Int(volume))")
                .frame(width: 40)
        }
        
        HStack {
            Text("明るさ")
            Slider(value: $brightness, in: 0...1)
        }
    }
}

NavigationLinkで画面遷移

Formから別の画面に遷移する場合は、NavigationLinkを使います。

NavigationView {
    Form {
        Section(header: Text("詳細設定")) {
            NavigationLink("アカウント情報") {
                AccountDetailView()
            }
            
            NavigationLink("プライバシー設定") {
                PrivacyView()
            }
            
            NavigationLink("通知設定") {
                NotificationView()
            }
        }
    }
    .navigationTitle("設定")
}

重要: NavigationLinkを使う場合は、FormをNavigationViewで囲む必要があります。

実践例:完全な設定画面を作る

ここまで学んだ内容を組み合わせて、実際の設定画面を作ってみましょう。

struct SettingsView: View {
    // プロフィール
    @State private var username = ""
    @State private var email = ""
    
    // 通知設定
    @State private var notifications = true
    @State private var emailNotifications = false
    
    // 表示設定
    @State private var darkMode = false
    @State private var fontSize = 16.0
    @State private var selectedLanguage = "日本語"
    
    let languages = ["日本語", "English", "中文", "한국어"]
    
    var body: some View {
        NavigationView {
            Form {
                // プロフィールセクション
                Section(header: Text("プロフィール")) {
                    TextField("ユーザー名", text: $username)
                        .textInputAutocapitalization(.never)
                    
                    TextField("メールアドレス", text: $email)
                        .keyboardType(.emailAddress)
                        .textInputAutocapitalization(.never)
                }
                
                // 通知設定セクション
                Section(
                    header: Text("通知"),
                    footer: Text("通知をオフにすると重要なお知らせを受け取れません")
                ) {
                    Toggle("プッシュ通知", isOn: $notifications)
                    Toggle("メール通知", isOn: $emailNotifications)
                        .disabled(!notifications)
                }
                
                // 表示設定セクション
                Section(header: Text("表示")) {
                    Toggle("ダークモード", isOn: $darkMode)
                    
                    HStack {
                        Text("フォントサイズ")
                        Slider(value: $fontSize, in: 12...24, step: 1)
                        Text("\(Int(fontSize))pt")
                            .frame(width: 50)
                            .foregroundColor(.secondary)
                    }
                    
                    Picker("言語", selection: $selectedLanguage) {
                        ForEach(languages, id: \.self) { language in
                            Text(language)
                        }
                    }
                }
                
                // その他セクション
                Section(header: Text("その他")) {
                    NavigationLink("利用規約") {
                        TermsView()
                    }
                    
                    NavigationLink("プライバシーポリシー") {
                        PrivacyPolicyView()
                    }
                    
                    NavigationLink("お問い合わせ") {
                        ContactView()
                    }
                }
                
                // アカウント操作セクション
                Section {
                    Button("ログアウト") {
                        // ログアウト処理
                        print("ログアウトが押されました")
                    }
                    .foregroundColor(.red)
                    
                    Button("アカウント削除", role: .destructive) {
                        // アカウント削除処理
                        print("アカウント削除が押されました")
                    }
                }
                
                // バージョン情報
                Section {
                    HStack {
                        Text("バージョン")
                        Spacer()
                        Text("1.0.0")
                            .foregroundColor(.secondary)
                    }
                }
            }
            .navigationTitle("設定")
        }
    }
}

この設定画面には、以下の機能が含まれています。

  • プロフィール情報の入力
  • 通知のオン・オフ切り替え
  • 表示設定のカスタマイズ
  • 他の画面への遷移
  • アカウント操作ボタン
  • バージョン情報の表示

Formのスタイルをカスタマイズする

Formのデフォルトスタイルも美しいですが、アプリのデザインに合わせてカスタマイズすることも可能です。

リストスタイルの変更

Form {
    // コンテンツ
}
.listStyle(.insetGrouped)  // グループ化スタイル(推奨)

主なリストスタイル:

// グループ化スタイル(iOS標準の設定アプリと同じ)
.listStyle(.insetGrouped)

// グループ化(余白少なめ)
.listStyle(.grouped)

// シンプルなリスト
.listStyle(.plain)

// サイドバー用
.listStyle(.sidebar)

背景色のカスタマイズ

iOS 16以降では、Formの背景色を変更できます。

Form {
    // コンテンツ
}
.scrollContentBackground(.hidden)  // デフォルトの背景を非表示
.background(Color.blue.opacity(0.1))  // カスタム背景色

セクションのスタイル調整

Section(header: Text("設定")) {
    Toggle("通知", isOn: $notifications)
}
.headerProminence(.increased)  // ヘッダーを目立たせる

入力検証を実装する

実際のアプリでは、ユーザーの入力内容を検証する必要があります。

基本的な検証例

struct LoginFormView: View {
    @State private var email = ""
    @State private var password = ""
    @State private var showError = false
    
    var isValidEmail: Bool {
        email.contains("@") && email.contains(".")
    }
    
    var isFormValid: Bool {
        isValidEmail && password.count >= 6
    }
    
    var body: some View {
        NavigationView {
            Form {
                Section(header: Text("ログイン情報")) {
                    TextField("メールアドレス", text: $email)
                        .textInputAutocapitalization(.never)
                        .keyboardType(.emailAddress)
                    
                    SecureField("パスワード", text: $password)
                    
                    if !email.isEmpty && !isValidEmail {
                        Text("正しいメールアドレスを入力してください")
                            .foregroundColor(.red)
                            .font(.caption)
                    }
                    
                    if !password.isEmpty && password.count < 6 {
                        Text("パスワードは6文字以上にしてください")
                            .foregroundColor(.red)
                            .font(.caption)
                    }
                }
                
                Section {
                    Button("ログイン") {
                        // ログイン処理
                        print("ログイン実行")
                    }
                    .disabled(!isFormValid)
                    .frame(maxWidth: .infinity)
                    .foregroundColor(isFormValid ? .blue : .gray)
                }
            }
            .navigationTitle("ログイン")
        }
    }
}

リアルタイム検証

struct RegistrationForm: View {
    @State private var username = ""
    @State private var email = ""
    @State private var password = ""
    @State private var confirmPassword = ""
    
    var passwordsMatch: Bool {
        password == confirmPassword && !password.isEmpty
    }
    
    var body: some View {
        Form {
            Section(header: Text("アカウント作成")) {
                TextField("ユーザー名", text: $username)
                TextField("メールアドレス", text: $email)
                    .textInputAutocapitalization(.never)
                
                SecureField("パスワード", text: $password)
                SecureField("パスワード(確認)", text: $confirmPassword)
                
                if !confirmPassword.isEmpty && !passwordsMatch {
                    Text("パスワードが一致しません")
                        .foregroundColor(.red)
                        .font(.caption)
                }
            }
            
            Section {
                Button("登録") {
                    // 登録処理
                }
                .disabled(username.isEmpty || email.isEmpty || !passwordsMatch)
            }
        }
    }
}

動的なフォームを作る

ユーザーの操作に応じて、フォームの内容を動的に変更することもできます。

項目の追加・削除

struct TodoForm: View {
    @State private var todos = [""]
    
    var body: some View {
        NavigationView {
            Form {
                Section(header: Text("TODOリスト")) {
                    ForEach(todos.indices, id: \.self) { index in
                        HStack {
                            TextField("TODO \(index + 1)", text: $todos[index])
                            
                            if todos.count > 1 {
                                Button(action: {
                                    todos.remove(at: index)
                                }) {
                                    Image(systemName: "minus.circle.fill")
                                        .foregroundColor(.red)
                                }
                            }
                        }
                    }
                    
                    Button(action: {
                        todos.append("")
                    }) {
                        HStack {
                            Image(systemName: "plus.circle.fill")
                            Text("TODOを追加")
                        }
                    }
                }
            }
            .navigationTitle("TODO管理")
        }
    }
}

条件付き表示

struct ConditionalForm: View {
    @State private var hasAccount = false
    @State private var username = ""
    @State private var email = ""
    @State private var password = ""
    
    var body: some View {
        Form {
            Section {
                Toggle("アカウントを持っていますか?", isOn: $hasAccount)
            }
            
            if hasAccount {
                // ログインフォーム
                Section(header: Text("ログイン")) {
                    TextField("ユーザー名", text: $username)
                    SecureField("パスワード", text: $password)
                }
            } else {
                // 新規登録フォーム
                Section(header: Text("新規登録")) {
                    TextField("ユーザー名", text: $username)
                    TextField("メールアドレス", text: $email)
                    SecureField("パスワード", text: $password)
                }
            }
            
            Section {
                Button(hasAccount ? "ログイン" : "登録") {
                    // 処理
                }
            }
        }
    }
}

FormとListの違いと使い分け

SwiftUIには、Formと似たListというコンポーネントもあります。どちらを使えばいいか迷う方も多いでしょう。

Formを使うべき場合

  • ユーザー入力を受け付ける画面
  • 設定画面
  • フォーム形式の画面
  • インタラクティブなコントロールが中心
// Form:設定画面に最適
Form {
    TextField("名前", text: $name)
    Toggle("通知", isOn: $notifications)
    Picker("テーマ", selection: $theme) {
        // 選択肢
    }
}

Listを使うべき場合

  • データの一覧を表示する画面
  • スクロール可能なリスト
  • 大量のデータを扱う場合
  • より柔軟なカスタマイズが必要な場合
// List:一覧表示に最適
List(items) { item in
    HStack {
        Text(item.title)
        Spacer()
        Text(item.date)
            .foregroundColor(.secondary)
    }
}

主な違いまとめ

特徴FormList
主な用途入力フォーム・設定画面データ一覧表示
スタイル入力に最適化表示に最適化
カスタマイズ制限あり柔軟
パフォーマンス少数の項目向け大量データ対応

よくあるエラーと対処法

エラー1: NavigationLinkが動かない

// ❌ 間違い
Form {
    NavigationLink("設定") {
        SettingsView()
    }
}

// ✅ 正解:NavigationViewで囲む
NavigationView {
    Form {
        NavigationLink("設定") {
            SettingsView()
        }
    }
}

エラー2: @Stateのバインディングエラー

// ❌ 間違い:$を忘れている
TextField("名前", text: name)

// ✅ 正解:バインディングには$を付ける
TextField("名前", text: $name)

エラー3: Pickerで選択が反映されない

// ❌ 間違い:tagを付けていない
Picker("テーマ", selection: $theme) {
    ForEach(themes) { theme in
        Text(theme.name)
    }
}

// ✅ 正解:各選択肢にtagを付ける
Picker("テーマ", selection: $theme) {
    ForEach(themes) { theme in
        Text(theme.name).tag(theme)
    }
}

実践的なTips

Tip 1: キーボードを閉じる

フォーム入力後、キーボードを自動で閉じたい場合があります。

struct FormWithKeyboardDismiss: View {
    @State private var text = ""
    @FocusState private var isFocused: Bool
    
    var body: some View {
        Form {
            Section {
                TextField("入力", text: $text)
                    .focused($isFocused)
            }
            
            Section {
                Button("送信") {
                    isFocused = false  // キーボードを閉じる
                    // 送信処理
                }
            }
        }
    }
}

Tip 2: 保存確認ダイアログ

変更内容を保存する際の確認ダイアログを実装できます。

struct FormWithConfirmation: View {
    @State private var name = ""
    @State private var showAlert = false
    
    var body: some View {
        Form {
            TextField("名前", text: $name)
            
            Button("保存") {
                showAlert = true
            }
        }
        .alert("確認", isPresented: $showAlert) {
            Button("キャンセル", role: .cancel) { }
            Button("保存") {
                // 保存処理
            }
        } message: {
            Text("変更内容を保存しますか?")
        }
    }
}

Tip 3: 入力内容をクリア

フォームの内容を一括でクリアする機能を実装できます。

struct ClearableForm: View {
    @State private var name = ""
    @State private var email = ""
    @State private var message = ""
    
    func clearForm() {
        name = ""
        email = ""
        message = ""
    }
    
    var body: some View {
        NavigationView {
            Form {
                Section(header: Text("お問い合わせ")) {
                    TextField("名前", text: $name)
                    TextField("メールアドレス", text: $email)
                    TextField("メッセージ", text: $message, axis: .vertical)
                        .lineLimit(5...10)
                }
                
                Section {
                    Button("送信") {
                        // 送信処理
                        clearForm()
                    }
                    .disabled(name.isEmpty || email.isEmpty)
                    
                    Button("クリア", role: .destructive) {
                        clearForm()
                    }
                }
            }
            .navigationTitle("お問い合わせ")
        }
    }
}

まとめ

SwiftUIのFormについて、以下のポイントを押さえておきましょう。

Formの特徴:

  • 少ないコードで美しい入力画面が作れる
  • iOS標準の設定アプリと同じような見た目
  • 自動的にスクロール、スタイリング、レイアウトが最適化される

主要なコンポーネント:

  • TextField / SecureField:テキスト入力
  • Toggle:オン・オフ切り替え
  • Picker:選択肢から選択
  • Stepper:数値の増減
  • DatePicker:日付・時刻選択
  • Slider:スライダー
  • NavigationLink:画面遷移

使い分け:

  • ユーザー入力や設定画面 → Form
  • データの一覧表示 → List

実装のポイント:

  • Sectionでグループ化して整理する
  • 適切なキーボードタイプを設定する
  • 入力検証を実装する
  • NavigationViewと組み合わせて画面遷移を実現する

Formを使いこなすことで、ユーザーフレンドリーで美しい入力画面を効率的に開発できます。

まずは簡単な設定画面から作り始めて、徐々に複雑なフォームにチャレンジしてみてください!

参考リンク

プログラミングの独学におすすめ
プログラミング言語の人気オンラインコース
独学でプログラミングを学習している方で、エラーなどが発生して効率よく勉強ができないと悩む方は多いはず。Udemyは、プロの講師が動画で実際のプログラムを動かしながら教えてくれるオンライン講座です。講座の価格は、セール期間中には専門書籍を1冊買うよりも安く済むことが多いです。新しく学びたいプログラミング言語がある方は、ぜひUdemyでオンライン講座を探してみてください。
目次