1
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

SwiftUI: Human Interface Guidelinesに沿ったTextField

Last updated at Posted at 2021-12-07

Human Interface Guidelinesに沿った使い回しが効くようなTextFieldを検討しました

  • Swift5
  • Xcode ver.13.1

TextField

Human Interface Guidelines - TextFields

2021.12.7時点で書いてあることは以下

  1. 1行である
  2. 固定高さである
  3. タップすると自動でキーボードが表示される
  4. 名前やメールアドレスなどの少量の情報を要求するときに使用する
  5. 別のラベルではなくプレースホルダーを表示する(プレースホルダーで十分説明可能な時)
  6. テキストのクリアボタンを末尾につける
  7. パスワードの入力などには情報を隠すためセキュアなテキストフィールド(SecureField)を使用する
  8. フィールドの両端にはフィールドの目的を示すイメージや追加機能のボタンを追加できる

SwiftUIのTextFieldを使う際には6.テキストのクリアボタンを末尾につける8.フィールドの先頭にはフィールドの目的を示すイメージ、末尾には追加機能のボタンを追加できるは自前実装する必要があります。

テキストのクリアボタンを末尾につける

編集中のみ×ボタンが表示されるやつです。

image.png

※ ↑の画像はList内に配置しているものです

import SwiftUI

struct HIGTextField<Leading:View, Trailing:View>: View {
    @Binding public var text:String
    let prompt:String

    @State private var editing:Bool = false

    var body: some View {
        HStack(alignment: .center, spacing: nil, content: {
            // for future iOS
            // TextField("title", text: $task.title, prompt:Text("Routine name"))
            //                            .focused($titleFocused)
            //
            TextField(prompt, text: $text, onEditingChanged: {editing in
                    self.editing = editing
            }, onCommit: {})
            if(editing){
                Button(action: {
                    text = ""
                }, label: {
                    Image(systemName: "xmark.circle.fill")
                        .foregroundColor(.secondary)
                })
            }
        })
    }
}

※onEditingChanged, onCommitを引数とするTextFieldのイニシャライザは将来的に非推奨となるため、後々はfocused()モディファイアを使った方法に変えた方が良さそうです。

フィールド先頭にはフィールドの目的を示すイメージ、末尾には追加機能のボタンを追加できる

さらに、検索バーの虫眼鏡アイコンや、音声入力ボタンなどを実現できるようにします

編集してない時

image.png

編集している時

image.png

※ ×ボタンは維持しつつ、両端のImageやButtonを必要に応じてイニシャライザで定義できるようにします
※ ↑の画像はVStack内に配置し、.background(RoundedRectangle(cornerRadius: 8).fill(.regularMaterial))としたものです

import SwiftUI

struct HIGTextField<Leading:View, Trailing:View>: View {
    typealias Focused = Bool
    @Binding public var text:String
    let prompt:String
    // 第1引数は編集中のテキスト、第2引数は現在TextFieldが編集状態かどうか。
    // 第1引数によって両端に追加するボタンから編集中のテキストにアクセスできるようにし
    // 第2引数によって編集中かどうかによってコントロールの表示を切り替えられるようにする
    let leading:(Binding<String>, Focused) -> Leading
    let trailing:(Binding<String>, Focused) -> Trailing
    
    @State private var editing:Bool = false
    // 両端にUIを追加するパターン
    init(text:Binding<String>,
         prompt:String,
         leading: @escaping (Binding<String>, Focused) -> Leading,
         trailing: @escaping (Binding<String>, Focused) -> Trailing){
        self._text = text
        self.prompt = prompt
        self.leading = leading
        self.trailing = trailing
    }
    
    var body: some View {
        HStack(alignment: .center, spacing: nil, content: {
            leading($text, self.editing)
            // for future iOS
            // TextField("title", text: $task.title, prompt:Text("Routine name"))
            //                            .focused($titleFocused)
            //
            TextField(prompt, text: $text, onEditingChanged: {editing in
                
                    self.editing = editing
                
            }, onCommit: {})
            
            trailing($text, self.editing)
            
            if(editing){
                Button(action: {
                    text = ""
                }, label: {
                    Image(systemName: "xmark.circle.fill")
                        .foregroundColor(.secondary)
                })
            }
        })
    }
}

extension HIGTextField{
    // 前後に何の機能もないパターン
    init(text:Binding<String>, prompt:String) where Leading == EmptyView, Trailing == EmptyView{
        self.init(text: text, prompt: prompt, leading: {(b, f) in EmptyView()}, trailing: {(b, f) in EmptyView()})
    }
    
    // 先頭にUIを追加するパターン
    init(text:Binding<String>, prompt:String, leading:@escaping (Binding<String>, Focused) -> Leading) where Trailing == EmptyView{
        self.init(text: text, prompt: prompt, leading: leading, trailing: {(b, f) in EmptyView()})
    }
    
    // 末尾にUIを追加するパターン
    init(text:Binding<String>, prompt:String, trailing:@escaping (Binding<String>, Focused) -> Trailing) where Leading == EmptyView{
        self.init(text: text, prompt: prompt, leading: {(b, f) in EmptyView()}, trailing: trailing)
    }
}

結果

おおよそのパターンに対応できそうな汎用的なTextFieldができました。

1
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?