SwiftUI: Human Interface Guidelinesに沿ったTextField

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

- Swift5
- Xcode ver.13.1

# TextField

[Human Interface Guidelines - TextFields](https://developer.apple.com/design/human-interface-guidelines/ios/controls/text-fields/)

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

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

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

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

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

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2088399/55452ad9-3ca5-bb81-fc25-88137631a25a.png)

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

```swift
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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2088399/eac5ce93-be36-ec6c-be53-7a3dc99172dc.png)

編集している時

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2088399/d8e87e46-af1f-29d4-a340-7a976de2abcf.png)

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

```swift
import SwiftUI

struct HIGTextField<Leading:View, Trailing:View>: View {
    typealias Focused = Bool
    @Binding public var text:String
    let prompt:String
    // 第１引数は編集中のテキスト、第２引数は現在TextFieldが編集状態かどうか。
    // 第１引数によって両端に追加するボタンから編集中のテキストにアクセスできるようにし
    // 第２引数によって編集中かどうかによってコントロールの表示を切り替えられるようにする
    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ができました。