🦅

[SwiftUI]パスワード入力欄の作り方

2024/01/16に公開
2

入力内容の表示・非表示を切り替えられるパスワード入力欄の作り方をまとめます。

要件

  1. 入力内容の表示・非表示を切り替えられる
  2. 表示・非表示の切り替え時に、キーボードが閉じない

定義元

  1. 入力内容の表示・非表示を切り替えられる

TextFieldとSecureFieldの切り替えによって実装します。

https://developer.apple.com/documentation/swiftui/textfield

https://developer.apple.com/documentation/swiftui/securefield

  1. 表示・非表示の切り替え時に、キーボードが閉じない

@FocusStateの切り替えによって実装します。

PasswordField.swift
  struct PasswordField: View {
      let titleKey: LocalizedStringKey
      @Binding var text: String
  
      @State var isShowSecure = false
      @FocusState var isTextFieldFocused: Bool
      @FocusState var isSecureFieldFocused: Bool
  
      init(_ titleKey: LocalizedStringKey, text: Binding<String>) {
          self.titleKey = titleKey
          _text = text
      }
  
      var body: some View {
          HStack {
              ZStack {
                  TextField(titleKey, text: $text)
                      .focused($isTextFieldFocused)
                      .keyboardType(.asciiCapable)
                      .autocorrectionDisabled(true)
                      .textInputAutocapitalization(.none)
                      .opacity(isShowSecure ? 1 : 0)
  
                  SecureField(titleKey, text: $text)
                      .focused($isSecureFieldFocused)
                      .opacity(isShowSecure ? 0 : 1)
              }
  
              Button {
                  if isShowSecure {
                      isShowSecure = false
                      isSecureFieldFocused = true
                  } else {
                      isShowSecure = true
                      isTextFieldFocused = true
                  }
  
              } label: {
                  Image(systemName: isShowSecure ? "eye" : "eye.slash")
              }
              .buttonStyle(.plain)
          }
          .padding()
          .overlay(
              RoundedRectangle(cornerRadius: 16)
                  .stroke(Color.primary, lineWidth: 1)
          )
      }
  }

呼び出し元

ContentView.swift
PasswordField("PasswordField", text: $password)
表示(TextField) 非表示(SecureField)

入力内容が自動で削除されないようにする

ここまで実装された方や、同じような実装方法でアプリを作った方は一度

  1. 入力内容の表示
  2. ABCと入力
  3. 入力内容の非表示
  4. Dと入力

のような動作をしてみてください。
ABCDとしたいところ、Dとなったのではないでしょうか?

これはSecureFieldの、

フォーカス後の最初の入力の場合、入力内容が上書きされる

という挙動が関係しています。
この動作を検知して、上書きしていないように振る舞わせてます。

PasswordField.swift
 struct PasswordField: View {
     let titleKey: LocalizedStringKey
     @Binding var text: String
 
     @State var isShowSecure = false
     @FocusState var isTextFieldFocused: Bool
     @FocusState var isSecureFieldFocused: Bool
+   @State var isFirstEntryAfterToggle = false
 
     init(_ titleKey: LocalizedStringKey, text: Binding<String>) {
         self.titleKey = titleKey
         _text = text
     }
 
     var body: some View {
         HStack {
             ZStack {
                 TextField(titleKey, text: $text)
                     .focused($isTextFieldFocused)
                     .keyboardType(.asciiCapable)
                     .autocorrectionDisabled(true)
                     .textInputAutocapitalization(.none)
                     .opacity(isShowSecure ? 1 : 0)
 
                 SecureField(titleKey, text: $text)
                     .focused($isSecureFieldFocused)
                     .opacity(isShowSecure ? 0 : 1)
             }
 
             Button {
                 if isShowSecure {
                     isShowSecure = false
                     isSecureFieldFocused = true
+                   if !text.isEmpty {
+                       isFirstEntryAfterToggle = true
+                   }
                 } else {
                     isShowSecure = true
                     isTextFieldFocused = true
+                   isFirstEntryAfterToggle = false
                 }
 
             } label: {
                 Image(systemName: isShowSecure ? "eye" : "eye.slash")
             }
             .buttonStyle(.plain)
         }
         .padding()
         .overlay(
             RoundedRectangle(cornerRadius: 16)
                 .stroke(Color.primary, lineWidth: 1)
         )
+       .onChange(of: text) { oldValue, newValue in
+           if newValue.count == 1 && isFirstEntryAfterToggle {
+               UINotificationFeedbackGenerator().notificationOccurred(.success)
+               text = oldValue + newValue
+           }
+           isFirstEntryAfterToggle = false
+       }
     }
 }

最適なパスワード入力欄について考える

1つ前の章で、入力内容が自動で削除されないようにする方法を紹介しましたが、なぜそのような問題が起きるのでしょうか?
そもそも、最適なパスワード入力欄とはどのようなものなのでしょうか?

まずは、HIGを見ます。

https://developer.apple.com/jp/design/human-interface-guidelines/entering-data

必要に応じてセキュリティ保護されたテキスト入力フィールドを使う。機密データを必要とするアプリやゲームでは、ユーザの入力内容が表示されないフィールドを使用します(通常、入力文字の代わりに小さい「●」が表示される)。デベロッパ向けのガイダンスは、SecureFieldを参照してください。

パスワードの入力には、SecureFieldを使用することが推奨されています。
HIGにはSecureFieldの詳しい効果は書かれていませんが、
SecureFieldには、

  • サードパーティーキーボードのブロック
  • コピーの禁止
  • スクリーンショット、画面収録時の情報の保護

等のセキュリティ保護機能が備わっているため、SecureFieldを推奨していると考えています。

パスワードフィールドの内容は自動入力しないようにする。パスワードの入力または生体認証やキーチェーン認証の使用は必ずユーザに要求するようにしてください。

少し解釈は異なりますが、今回紹介したパスワード入力欄は、SecureFieldから見ると自動入力された判定になってしまっています。

  1. 入力内容の表示
  2. ABCと入力(TextFieldに値を入力している)
  3. 入力内容の非表示(SecureFieldにTextFieldの値を自動入力している)

このルールが原因で、入力内容が自動で削除される問題が起きていると考えています。

これらを元に、最適なパスワード入力欄を考えてみました。

要件

  1. セキュリティが保護されている
  2. 入力間違いのリスクを減少できる
  3. OSやバージョンの差がなく使用できる

案1: SecureFieldで2度パスワードを打ってもらう

SecureFieldを2つ実装し、2つの入力内容が同じ時に次に進めるようにします。
これが一番安全な方法ですが、入力内容の確認ができないデメリットがあります。

案2: 入力内容の表示中は編集できないようにする

今までの入力内容の表示・非表示は、TextFieldとSecureFieldの切り替えによって実装していましたが、TextとSecureFieldの切り替えによって実装するように変更します。
入力内容の確認ができますが、入力内容が画面収録やスクリーンショットに映ります。

表示(Text) 非表示(SecureField)

宣伝

株式会社アルクでは、ディズニー ファンタスピークの開発をしています。
ディズニー ファンタスピークはディズニーの作品や音楽を楽しみながら、英語学習ができるアプリです。
英語を勉強したいけど、教科書みたいなのはちょっと…という方におすすめです。

気になった方は下記からインストールお願いします。

Discussion

とんとんぼとんとんぼ

ここの箇所

- UINotificationFeedbackGenerator().notificationOccurred(.+ uccess)
+ UINotificationFeedbackGenerator().notificationOccurred(.success)

と思うのですが、いかがでしょうか 🙇