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


# 前置き

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

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


# 考察

## 既存の方法について

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

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

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

https://github.com/siteline/SwiftUI-Introspect

## NotificationCenter を使って監視する

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

![UITextfield notificaiton](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/175242/8aaa4ab2-eb2e-5b38-4896-f9a981f7e870.png)

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

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

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

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

実装を見ていくと、`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)
    }
}
```

今回は `UITextField` の `extension` として拡張しましたが、個々にやりやすいようにしてください。これを `TextView` の `onReceive` で監視することで `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. テキストが未入力時はエンターキーを押させないようにする

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

```.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. 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`が勝手に取れるので、特に解除の処理を入れたりはしていませんが、`textDidEndEditingNotificationPublisher`で`nil`を入れてあげても良いかもしれません。


# 終わりに

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