SwiftUI - ユーザー入力と編集

1. はじめに

ここでは、SwiftUIチュートリアルのユーザー入力と編集について書いています。少しアレンジして作り直したものも記載しています。

2. 準備

使用するファイルを先に作成しておきます。ファイル名や役割は下記のようにしています。

新規ファイル

  • Profile.swift(Swift File) - 編集するファイルの設定
  • ModelData.swift(Swift File)- 入力の変更を監視
  • ProfileSummary.swift(SwiftUI View)- 出力ページ
  • ProfileEditor.swift(SwiftUI View) - 編集ページ
  • ProfileHost.swift(SwiftUI View)- 出力ページと編集ページの切り替えを行う

デフォルトファイル

  • TestApp.swift - クラスのインスタンス化(プロジェクト名App)
  • ContentView.swift - ProfileHost.swiftを出力

ユーザー入力と編集

alt

各ファイルに下記のコードを記述するとGif画像のような編集ボタン(Edit)、編集確定ボタン(Done)、編集中断ボタン(Cancel)が出力できます。データ変更を監視やデータ共有については以前の記事にまとめているので気になる方は見てみて下さい。

//Profile.swift

import Foundation

struct Profile {
  var name: String
  
  static let `default` = Profile(name: "")
}
//ModelData.swift

import Foundation
import Combine

final class ModelData: ObservableObject {
  @Published var profile = Profile.default
}
//TestApp.swift

import SwiftUI

@main
struct TestApp: App {
  @StateObject private var modelData = ModelData()
    var body: some Scene {
        WindowGroup {
            ContentView()
              .environmentObject(modelData)
        }
    }
}
//ContentView.swift

import SwiftUI

struct ContentView: View {
  
  var body: some View {
    ProfileHost()
  }
}

struct ContentView_Previews: PreviewProvider {
  static var previews: some View {
    ContentView()
      .environmentObject(ModelData())
  }
}
//ProfileSummary.swift

import SwiftUI

struct ProfileSummary: View {
  var profile: Profile
  
  var body: some View {
    List {
      Text("\(profile.name)")
    }
  }
}

struct ProfileSummary_Previews: PreviewProvider {
  static var previews: some View {
    ProfileSummary(profile: Profile.default)
  }
}
//ProfileEditor.swift

import SwiftUI

struct ProfileEditor: View {
  @Binding var profile: Profile
  
  var body: some View {
    List {
      TextField("お名前", text: $profile.name)
    }
  }
}

struct ProfileEditor_Previews: PreviewProvider {
  static var previews: some View {
    ProfileEditor(profile: .constant(.default))
  }
}
//ProfileHost.swift

import SwiftUI

struct ProfileHost: View {
  @Environment(\.editMode) var editMode
  @EnvironmentObject var modelData: ModelData
  @State private var draftProfile = Profile.default
  
  var body: some View {
    VStack {
      HStack {
        if editMode?.wrappedValue == .active {
          Button("Cancel") {
            draftProfile = modelData.profile
            editMode?.animation().wrappedValue = .inactive
          }
        }
        Spacer()
        EditButton()
      }
      if editMode?.wrappedValue == .inactive {
        ProfileSummary(profile: modelData.profile)
      } else {
        ProfileEditor(profile: $draftProfile)
          .onAppear {
            draftProfile = modelData.profile
          }
          .onDisappear {
            modelData.profile = draftProfile
          }
      }
    }
  }
}

struct ProfileHost_Previews: PreviewProvider {
  static var previews: some View {
    ProfileHost()
      .environmentObject(ModelData())
  }
}

@Environment - ビューの環境から値を読み取るプロパティラッパー
.active - ビューの内容を編集可
.inactive - ビューの内容は編集不可
.onAppear - ビューが表示されたときのアクション
.onDisappear - ビューが消えたときに実行するアクション

3. 少しアレンジをしてみる

alt

チュートリアルのコードに、項目を追加したり装飾を少しして、iPhoneの設定画面のようなデザインでプロフィールを編集する画面を作成してみました。(入力項目のチェックなどはなく編集のみです)下記に書いていないファイルは上記と同じコードを使用しています。

//Profile.swift

import Foundation

struct Profile {
  var name: String
  var phone: String
  var email: String
  var blood = Blood.a
  var birthday = Date()
  var notice = true
  var notificationDate = Date()
  
  static let `default` = Profile(name: "hoge", phone: "00011112222", email: "xxxxx@xxxx")
  
  enum Blood: String, CaseIterable, Identifiable {
    case a = "A"
    case b = "B"
    case o = "O"
    case ab = "AB"
    
    var id: String { self.rawValue }
  }
}
//ProfileSummary.swift

import SwiftUI

struct ProfileSummary: View {
  var profile: Profile
  
  var dateFormat: DateFormatter {
    let format = DateFormatter()
    format.dateStyle = .long
    format.locale = Locale(identifier: "ja_JP")
    return format
  }
  
  var body: some View {
    List {
      Section(header: Text("名前")) {
        Text("\(profile.name)")
      }
      Section(header: Text("連絡先")) {
        Text("\(profile.phone)")
        Text("\(profile.email)")
      }
      Section(header: Text("生年月日")) {
        Text(dateFormat.string(from: profile.birthday))
      }
      Section(header: Text("血液型")) {
        Text("\(profile.blood.rawValue)")
      }
      Section(header: Text("お知らせ")) {
        Text("\(profile.notice ? "ON" : "OFF")")

        if profile.notice {
          Text(dateFormat.string(from: profile.notificationDate))
        }
      }
    }
    .listStyle(GroupedListStyle())
  }
}

struct ProfileSummary_Previews: PreviewProvider {
  static var previews: some View {
    ProfileSummary(profile: Profile.default)
  }
}
//ProfileEditor.swift

import SwiftUI

struct ProfileEditor: View {
  @Binding var profile: Profile
  
  var dateRange: ClosedRange<Date> {
    let min = Calendar.current.date(byAdding: .year, value: -1, to: profile.notificationDate)!
    let max = Calendar.current.date(byAdding: .year, value: 1, to: profile.notificationDate)!
    return min...max
  }
  
  
  var body: some View {
    List {
      Section(header: Text("名前")) {
        TextField("お名前", text: $profile.name)
      }
      Section(header: Text("連絡先")) {
        TextField("電話番号", text: $profile.phone)
          .keyboardType(.phonePad)
        TextField("メールアドレス", text: $profile.email)
          .keyboardType(.emailAddress)
          .autocapitalization(.none)
      }
      Section(header: Text("生年月日")) {
        DatePicker(
          "",
          selection: $profile.birthday,
          displayedComponents: [.date]
        )
        .datePickerStyle(WheelDatePickerStyle())
        .environment(\.locale, Locale(identifier: "ja_JP"))
        
      }
      Section(header: Text("血液型")) {
        Picker("Blood", selection: $profile.blood) {
          ForEach(Profile.Blood.allCases) { item in
            Text(item.rawValue).tag(item)
          }
        }
        .pickerStyle(SegmentedPickerStyle())
      }
      Section(header: Text("お知らせ")) {
        Toggle("お知らせ", isOn: $profile.notice)
        
        if profile.notice {
          DatePicker(
            "通知日",
            selection: $profile.notificationDate,
            in: dateRange,
            displayedComponents: [.date]
          ).environment(\.locale, Locale(identifier: "ja_JP"))
        }
      }
    }
    .listStyle(GroupedListStyle())
  }
}

struct ProfileEditor_Previews: PreviewProvider {
  static var previews: some View {
    ProfileEditor(profile: .constant(.default))
  }
}
//ProfileHost.swift

import SwiftUI

struct ProfileHost: View {
  @Environment(\.editMode) var editMode
  @EnvironmentObject var modelData: ModelData
  @State private var draftProfile = Profile.default
  
  var body: some View {
    ZStack {
      Color(red: 242/255, green: 242/255, blue: 247/255, opacity: 1.0)
        .edgesIgnoringSafeArea(.all)
      VStack {
        HStack {
          if editMode?.wrappedValue == .active {
            Button("Cancel") {
              draftProfile = modelData.profile
              editMode?.animation().wrappedValue = .inactive
            }.padding(.horizontal, 10)
          }
          Spacer()
          EditButton().padding(.horizontal, 10)
        }
        if editMode?.wrappedValue == .inactive {
          ProfileSummary(profile: modelData.profile)
        } else {
          ProfileEditor(profile: $draftProfile)
            .onAppear {
              draftProfile = modelData.profile
            }
            .onDisappear {
              modelData.profile = draftProfile
            }
        }
      }
    }
  }
}

struct ProfileHost_Previews: PreviewProvider {
  static var previews: some View {
    ProfileHost()
      .environmentObject(ModelData())
  }
}

Environment | Apple Developer Documentation
EditMode | Apple Developer Documentation
wrappedValue | Apple Developer Documentation