9
8

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.

[Swift] SwiftUI の TextView から UITextfield を取り出す

Last updated at Posted at 2021-10-31

前置き

多くの会社で SwiftUI を使っているところが増えてきたのではないでしょうか?

今回は SwiftUI における TextView に関して、iOS13 では使える設定が限られバグも多いですが、なんとかハックして使っていくためのTipsを紹介していきます。

考察

既存の方法について

TextView では使いたいオプションが使えないため、UITextfieldUIViewRepresentable でラップするという方法で、SwiftUI 上で使用することが多く見られます。

今回は、TextView の内部にある UITextfield を取り出すアプローチを取ります。

既に取り出す用のライブラリもありますが、中身がだいぶブラックボックスで将来的に負債になりそうなので、個人的にはあまりオススメはしません。

NotificationCenter を使って監視する

UITextfield は以下の3つを使って監視することができます。

UITextfield notificaiton

TextView が内部に持っている UITextfieldNotification は監視することで、SwiftUI 上で UITextfield を取り出すことができるという仕組みです。

ただし、お気づきの方もいるかもしれませんが、OSとして用意されているものが

  • 入力開始時
  • 入力中
  • 入力終了

この3つしかないため、そのタイミングでのみしか拡張できません。

実装を見ていくと、SwiftUI 上では onReceive で受け取れるので、そのためにPublisherに変換したものを用意します。

.swift
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)
    }
}

今回は UITextFieldextension として拡張しましたが、個々にやりやすいようにしてください。これを TextViewonReceive で監視することで UITextField を検知することができます。

.swift
TextField("holder", text: /* Binding<String> */)
    .onReceive(
        UITextField.textDidBeginEditingNotificationPublisher,
        perform: { notification in
            var textField = (notification.object as? UITextField)
                .
                .
        }
    )

このようにして UITextField を取り出すことができます。

実用例

1. キーボードのリターンキーを変更する

iOS15 からは変更可能ですが、iOS13,14 はreturnKeyを設定できないので、先程の実装を使って実現します。

.swift
TextField("holder", text: /* Binding<String> */)
    .onReceive(
        UITextField.textDidBeginEditingNotificationPublisher,
        perform: { notification in
            (notification.object as? UITextField)?.returnKeyType = .done
        }
    )

textDidBeginEditingNotificationPublisher で編集開始時にキーボードの設定を変更しています。

汎用的に使いたいので、Modifierとして切り出すのがオススメです。

.swift
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 }
            )
    }
}
.swift
TextField("holder", text: /* Binding<String> */)
    .modifier(ReturnKey(returnKeyType: .done))

かなりコードがスッキリしました。

2. テキストが未入力時はエンターキーを押させないようにする

基本的には1と同じです。

.swift
TextField("holder", text: /* Binding<String> */)
    .onReceive(
        UITextField.textDidBeginEditingNotificationPublisher,
        perform: { notification in
            (notification.object as? UITextField)?.enablesReturnKeyAutomatically = true
        }
    )

同じように Modifier として切り出してあげると良いでしょう。

.swift
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 }
            )
    }
}
.swift
TextField("holder", text: /* Binding<String> */)
    .modifier(ReturnKeyAutomaticallyEnable(enable: true))

1、2 を見てお気づきだと思いますが、このように TextView にないものは拡張していくことで設定できるようになります。とはいえ、冒頭で述べた通り、設定できるタイミングが3つしかないためで、全てを拡張できるわけではありません。

3. Delegate を設定する

どうしても細かいハンドリングをしたいことが出てきた場合は、UITextFieldDelegate を設定しましょう。その際、structにはつけれないので、classで持つことになります。

.swift
final class ViewModel: NSObject, UITextFieldDelegate {
    // do something handling
}
.swift
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が勝手に取れるので、特に解除の処理を入れたりはしていませんが、textDidEndEditingNotificationPublishernilを入れてあげても良いかもしれません。

終わりに

iOS13では、他にも日本語周りのバグなど使い勝手の悪さがかなりあります。
TextView が少しでも使いやすくなったのであれば幸いですmm

9
8
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
9
8

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?