Human Interface Guidelinesに沿った使い回しが効くようなTextFieldを検討しました
- Swift5
- Xcode ver.13.1
TextField
Human Interface Guidelines - TextFields
2021.12.7時点で書いてあることは以下
- 1行である
- 固定高さである
- タップすると自動でキーボードが表示される
- 名前やメールアドレスなどの少量の情報を要求するときに使用する
- 別のラベルではなくプレースホルダーを表示する(プレースホルダーで十分説明可能な時)
- テキストのクリアボタンを末尾につける
- パスワードの入力などには情報を隠すためセキュアなテキストフィールド(SecureField)を使用する
- フィールドの両端にはフィールドの目的を示すイメージや追加機能のボタンを追加できる
SwiftUIのTextFieldを使う際には6.テキストのクリアボタンを末尾につける
、8.フィールドの先頭にはフィールドの目的を示すイメージ、末尾には追加機能のボタンを追加できる
は自前実装する必要があります。
テキストのクリアボタンを末尾につける
編集中のみ×ボタンが表示されるやつです。
※ ↑の画像は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や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ができました。