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つの要素を設定できます。
- header:セクションの見出し
- footer:セクションの注釈や補足説明
- 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)
}
}
主な違いまとめ
| 特徴 | Form | List |
|---|---|---|
| 主な用途 | 入力フォーム・設定画面 | データ一覧表示 |
| スタイル | 入力に最適化 | 表示に最適化 |
| カスタマイズ | 制限あり | 柔軟 |
| パフォーマンス | 少数の項目向け | 大量データ対応 |
よくあるエラーと対処法
エラー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を使いこなすことで、ユーザーフレンドリーで美しい入力画面を効率的に開発できます。
まずは簡単な設定画面から作り始めて、徐々に複雑なフォームにチャレンジしてみてください!