前置き
多くの会社で SwiftUI
を使っているところが増えてきたのではないでしょうか?
今回は SwiftUI
における TextView
に関して、iOS13 では使える設定が限られバグも多いですが、なんとかハックして使っていくためのTipsを紹介していきます。
考察
既存の方法について
TextView
では使いたいオプションが使えないため、UITextfield
を UIViewRepresentable
でラップするという方法で、SwiftUI
上で使用することが多く見られます。
今回は、TextView の内部にある UITextfield を取り出すアプローチを取ります。
既に取り出す用のライブラリもありますが、中身がだいぶブラックボックスで将来的に負債になりそうなので、個人的にはあまりオススメはしません。
NotificationCenter を使って監視する
UITextfield
は以下の3つを使って監視することができます。
TextView
が内部に持っている UITextfield
の Notification
は監視することで、SwiftUI
上で UITextfield
を取り出すことができるという仕組みです。
ただし、お気づきの方もいるかもしれませんが、OSとして用意されているものが
- 入力開始時
- 入力中
- 入力終了
この3つしかないため、そのタイミングでのみしか拡張できません。
実装を見ていくと、SwiftUI
上では onReceive
で受け取れるので、そのためにPublisher
に変換したものを用意します。
extension UITextField {
static var textDidBeginEditingNotificationPublisher: NotificationCenter.Publisher {
NotificationCenter.default.publisher(for: UITextField.textDidBeginEditingNotification)
}
static var textDidEndEditingNotificationPublisher: NotificationCenter.Publisher {
NotificationCenter.default.publisher(for: UITextField.textDidEndEditingNotification)
}
static var textDidChangeNotificationPublisher: NotificationCenter.Publisher {
NotificationCenter.default.publisher(for: UITextField.textDidChangeNotification)
}
}
今回は UITextField
の extension
として拡張しましたが、個々にやりやすいようにしてください。これを TextView
の onReceive
で監視することで UITextField
を検知することができます。
TextField("holder", text: /* Binding<String> */)
.onReceive(
UITextField.textDidBeginEditingNotificationPublisher,
perform: { notification in
var textField = (notification.object as? UITextField)
.
.
}
)
このようにして UITextField
を取り出すことができます。
実用例
1. キーボードのリターンキーを変更する
iOS15 からは変更可能ですが、iOS13,14 はreturnKey
を設定できないので、先程の実装を使って実現します。
TextField("holder", text: /* Binding<String> */)
.onReceive(
UITextField.textDidBeginEditingNotificationPublisher,
perform: { notification in
(notification.object as? UITextField)?.returnKeyType = .done
}
)
textDidBeginEditingNotificationPublisher
で編集開始時にキーボードの設定を変更しています。
汎用的に使いたいので、Modifier
として切り出すのがオススメです。
struct ReturnKey: ViewModifier {
var returnKeyType: UIReturnKeyType
func body(content: Content) -> some View {
content
.onAppear() // `onAppear`がないと、`Modifier`内の`onReceive`が発火しない
.onReceive(
UITextField.textDidBeginEditingNotificationPublisher,
perform: { ($0.object as? UITextField)?.returnKeyType = returnKeyType }
)
}
}
TextField("holder", text: /* Binding<String> */)
.modifier(ReturnKey(returnKeyType: .done))
かなりコードがスッキリしました。
2. テキストが未入力時はエンターキーを押させないようにする
基本的には1と同じです。
TextField("holder", text: /* Binding<String> */)
.onReceive(
UITextField.textDidBeginEditingNotificationPublisher,
perform: { notification in
(notification.object as? UITextField)?.enablesReturnKeyAutomatically = true
}
)
同じように Modifier
として切り出してあげると良いでしょう。
struct ReturnKeyAutomaticallyEnable: ViewModifier {
var enable: Bool
func body(content: Content) -> some View {
content
.onAppear() // `onAppear`がないと、`Modifier`内の`onReceive`が発火しない
.onReceive(
UITextField.textDidBeginEditingNotificationPublisher,
perform: { ($0.object as? UITextField)?.enablesReturnKeyAutomatically = enable }
)
}
}
TextField("holder", text: /* Binding<String> */)
.modifier(ReturnKeyAutomaticallyEnable(enable: true))
1、2 を見てお気づきだと思いますが、このように TextView
にないものは拡張していくことで設定できるようになります。とはいえ、冒頭で述べた通り、設定できるタイミングが3つしかないためで、全てを拡張できるわけではありません。
3. Delegate を設定する
どうしても細かいハンドリングをしたいことが出てきた場合は、UITextFieldDelegate
を設定しましょう。その際、struct
にはつけれないので、class
で持つことになります。
final class ViewModel: NSObject, UITextFieldDelegate {
// do something handling
}
struct XxxView: View {
private let viewModel = ViewModel()
TextField("holder", text: /* Binding<String> */)
.onReceive(
UITextField.textDidBeginEditingNotificationPublisher,
perform: { notification in
(notification.object as? UITextField)?.delegate = viewModel
}
)
}
画面跨いだりすると再描画が走ってdelegate
が勝手に取れるので、特に解除の処理を入れたりはしていませんが、textDidEndEditingNotificationPublisher
でnil
を入れてあげても良いかもしれません。
終わりに
iOS13では、他にも日本語周りのバグなど使い勝手の悪さがかなりあります。
TextView
が少しでも使いやすくなったのであれば幸いですmm