SwiftUIのTextFieldで日本語入力ができない

Xcode 11.7 + iOS 13.7の環境でTextFieldを使っていると、日本語入力時に挙動がおかしくなることに気がついた。開発中はハードウェアキーボードで英数字のみ入力していたので気がつかなかった。

実行環境

  • Xcode 11.7
  • iOS 13.7

問題の挙動

SwiftUIのTextFieldで日本語(フリック入力?)すると挙動がおかしくなってしまう不具合が発生した。たとえば「わだ」をフリック入力する場合には以下の現象が発生する。

  1. 「わ」を入力後
  2. 「た」を入力
  3. 「わ」が消えて「た」のみ残る

これではまともなに動く画面を作ることができない。

解決編

TextFieldでなんとかするように検討してみたものの難しそうなので諦めてUIKitのUITextFieldをラップして使うことにした。入力中のテキストも綺麗にバインディングできると思っていたがかなり苦労した。

UITextField.textDidChangeNotificationの通知を受け取り、都度バインディングしたtextを書き換えることにした。

import SwiftUI
import UIKit

struct TextFieldNative: UIViewRepresentable {
    
    let hint: String
    @Binding var text: String
    
    @State private var textContentType: UITextContentType? = nil
    @State private var keyboardType: UIKeyboardType = .default
    
    var isFirstResponder: Bool = false
    
    class Coordinator: NSObject, UITextFieldDelegate {
        let target: TextFieldNative
        var didBecomeFirstResponder = false
        
        init(target: TextFieldNative) {
            self.target = target
        }

        func textFieldDidEndEditing(_ textField: UITextField) {
            target.text = textField.text ?? ""
        }
        
        func textFieldShouldReturn(_ textField: UITextField) -> Bool {
            textField.resignFirstResponder()
        }
        
        /// 1文字ずつの変更で呼ばれる(UITextField.textDidChangeNotification)
        @objc func textDidChange(_ textField: UITextField) {
            target.text = textField.text ?? ""
        }
    }

    init(_ hint: String, text: Binding<String>, contentType: UITextContentType?, keyboardType: UIKeyboardType) {
        self.hint = hint
        self._text = text
        self.textContentType = contentType
        self.keyboardType = keyboardType
    }
    
    func makeCoordinator() -> TextFieldNative.Coordinator {
        return Coordinator(target: self)
    }
    
    func makeUIView(context: Context) -> UITextField {
        let textField = UITextField(frame: .zero)
        textField.placeholder = hint
        textField.delegate = context.coordinator
        textField.text = text
        textField.textContentType = textContentType
        textField.keyboardType = keyboardType
        textField.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
        NotificationCenter.default.addObserver(forName: UITextField.textDidChangeNotification, object: textField, queue: nil) { (_) in
            context.coordinator.textDidChange(textField)
        }
//        NotificationCenter.default.addObserver(forName: UITextField.textDidBeginEditingNotification, object: textField, queue: nil) { (_) in
//            context.coordinator.textDidChange(textField)
//        }
//        NotificationCenter.default.addObserver(forName: UITextField.textDidEndEditingNotification, object: textField, queue: nil) { (_) in
//            context.coordinator.textDidChange(textField)
//        }
        return textField
    }

    static func dismantleUIView(_ uiView: UITextField, coordinator: Coordinator) {
        NotificationCenter.default.removeObserver(self, name: UITextField.textDidChangeNotification, object: uiView)
//        NotificationCenter.default.removeObserver(self, name: UITextField.textDidBeginEditingNotification, object: uiView)
//        NotificationCenter.default.removeObserver(self, name: UITextField.textDidEndEditingNotification, object: uiView)
    }
    
    func updateUIView(_ uiView: UITextField, context: Context) {
        if uiView.text != text {
            uiView.text = text
        }
        if isFirstResponder && !context.coordinator.didBecomeFirstResponder  {
            uiView.becomeFirstResponder()
            context.coordinator.didBecomeFirstResponder = true
        }
    }
}

余談ではあるが入力している文字数が長くなると、Viewそのものが横に伸びていくようになってしまった。SwiftUIのView側で指定した制約よりも「強い」ようなので、makeUIView(context:)で、textField.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)として明示的に優先度をさげた。