はつねむさんぽのパンくずたち

エンジニアになる人生で通った道のあしあと

UITextFieldに文字数制限をつける

やりたいこと

  • UITextFieldに入力できる文字数を制限する
  • 変換中は制限されない
  • 確定した時にオーバーしている分は除かれる
  • 確定した時に文字数をカウントする

    f:id:hatsunem:20180921183433g:plain

考えられる解決策

stringByReplacingCharactersInRangeを使う

textField.delegate = selfとして、以下を追加。

extension ViewController: UITextFieldDelegate {
    func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
        // 文字数制限の処理
        return true
    }
}

英語入力の場合は問題ないが、
日本語などの変換がある言語の場合はうまく動かなかった。
関数が実行されるタイミングと変換との相性が悪いようだ。

UIControlEvents.EditingChanged

UIControlEventsというもので、様々なイベントをキャッチできるみたい。
その中に、textFieldChangedなるイベントがあった。
これを感知した時のactionを設定すれば良い。
重要なのはChangedと過去形なところ。

textField.addTarget(self, action: #selector(textFieldDidChange), for: .editingChanged)


markedTextRangeは、変換中(確定前)の文字列である。
今回の私の要求に応えてくれるめっちゃ便利なやつ。

@objc private func textFieldDidChange() {
    guard let text = textField.text else { return }
    // 変換中はスルー
    if textField.markedTextRange != nil { return }    
    if text.count > maxLength {
        textField.text = String(text.prefix(maxLength))
    }
}


UITextFieldTextDidChange

viewDidLoad等でUITextFieldTextDidChange通知を受け取れるように設定。
こっちのやり方でも希望の動作は得られる。
ただ、同一クラス内でNotification使うのはどうなんかな、、

NotificationCenter.default.addObserver(
    self,
    selector: #selector(textFieldDidChange),
    name: NSNotification.Name.UITextFieldTextDidChange,
    object: nil
)


呼ばれる関数の中身はさっきと同じ。

@objc private func textFieldDidChange(notification: NSNotification) {
    guard let text = textField.text else { return }
    if textField.markedTextRange != nil { return }
    if text.count > maxLength {
        textField.text = String(text.prefix(maxLength))
    }
}

ベストな解決策

2つ目のUIControlEvents.EditingChangedを使うのが個人的にベスト。
別ビューに文字列を渡したい時はNotificationを使うのがよそそう。