Swiftで文字数制限をつけた話

iOS側で作品を作ってサーバーに投げるアプリを開発していたときに、
SNSシェア用に作品を一つの画像にしなければいけなくなった。
その時に、「作品のタイトルが長すぎると画像に収まらない」とサーバーサイドで問題が発生した。
この問題を解決するために、フロント側で文字数制限をつけることとなった。

最初にやったこと

textViewのdelegateであるtextView:shouldChangeTextInRange:replacementText:を利用した。
このdelegateは、キーボードをタップしてから実際に表示される前に呼ばれ、boolで表示させるかを管理するものである。
こいつを使い、文字数をオーバーする場合はfalseを返せばOKだと思った。

問題点

  • 文字数をオーバーすると、キーボードが反応しなくなってしまう
  • 日本語のような変換が存在する言語では変換後に文字数制限を行わなければいけない
    • このままでは変換前に弾いてしまう
一つ目の原因

そもそもこのdelegateはキーボードの反応をboolで管理しているものなので、falseを返した時点でキーボードが反応しなくなってしまう。
つまり、文字数をオーバーしたときでもtrueを返さなければならないのだ。

これを解決するには、入力前の文字列を保管しておき、文字数をオーバーする場合は保管しておいた文字列を入れて返してやればいい。

二つ目の原因

日本語などは入力文字を変換した場合、文字数がオーバーしなくなる場合がある。
英語圏ではなにも問題ないが、日本人が利用するためこのままでは不便すぎる。

これを解決するには、変換後(つまり文字の反転が終わってから)の文字数チェックをすればよい。
変換後に呼ばれる(反転しない英語文字では常に呼ばれる)delegateであるtextViewDidChange:を利用する。
呼ばれる順番としてはtextView:shouldChangeTextInRange:replacementText:の次にtextViewDidChange:が呼ばれる。

全ての問題を解決する

全ての問題を解決するために

  • textView:shouldChangeTextInRange:replacementText:では入力文字の保管だけをする
  • textViewDidChange:で文字数のチェックを行い、オーバーする場合は保管してある入力前の文字列をtextViewにセットする。
    • 変換後もオーバーする場合はオーバーした部分は取り除く

以下がこの条件を満たすコードです。

    private let maxLength = 30
    private var previousText = ""
    private var lastReplaceRange: NSRange!
    private var lastReplacementString = ""

    func textView(textView: UITextView, shouldChangeTextInRange range: NSRange, replacementText text: String) -> Bool {
        self.previousText = textView.text
        self.lastReplaceRange = range
        self.lastReplacementString = text
        
        return true
    }

    func textViewDidChange(textView: UITextView) {
        if textView.markedTextRange != nil {
            return
        }
        
        if count(textView.text) > maxLength {
            var offset = maxLength - count(textView.text)
            var replacementString = (lastReplacementString as NSString).substringToIndex(count(lastReplacementString) + offset)
            var text = (previousText as NSString).stringByReplacingCharactersInRange(lastReplaceRange, withString: replacementString)
            var position = textView.positionFromPosition(textView.selectedTextRange!.start, offset: offset)
            var selectedTextRange = textView.textRangeFromPosition(position, toPosition: position)
            
            textView.text = text
            textView.selectedTextRange = selectedTextRange
        }
    }

まとめ

日本語ってめんどくさいね

参考

iOS で文字数制限つきのテキストフィールドをちゃんと作るのは難しいという話 - blog.niw.at