Swift5 UITextFieldやUITextViewで全角文字数制限、半角文字数制限（全角数の倍）

#はじめに
UITextFieldやUITextViewで入力された文字数を制限する方法は簡単にできますが

半角全角混在した文字列を全角で50文字(半角だと100文字)みたいな
簡単にできそうな事がswiftで実現するには苦労しました

自分なりに試行錯誤しながら考えた方法ですが
下記の方法以外にいい方法があったら教えてください

#ゴール

#####最大文字数が全角で50文字(半角で100文字)以内という条件の時

全角で50文字、半角で100文字を越えるとそれ以上入力できない
もしくは超えた分を削除


#####詰まるところ半角全角が混在した文章がshiftJISで100バイトを超えない事


#コード全文

この後に冗長的な解説がありますので...取り急ぎコードのみでわかる方用に全文載せておきます


```swift:ViewController.swift
//swift5


import UIKit

class ViewController: UIViewController, UITextFieldDelegate, UITextViewDelegate {
    
    let limitTextField = UITextField()
    let maxLength: Int = 100
    
    override func viewDidLoad() {
        super.viewDidLoad()
        NotificationCenter.default.addObserver(
            self,
            selector: #selector(inputTextdDidChange),
            name: UITextField.textDidChangeNotification,
            object: nil
        )
        configureViews()
    }
    
    func configureViews(){
        limitTextField.keyboardType = UIKeyboardType.default
        limitTextField.borderStyle = UITextField.BorderStyle.roundedRect
        limitTextField.returnKeyType = UIReturnKeyType.done
        limitTextField.clearButtonMode = UITextField.ViewMode.whileEditing
        limitTextField.translatesAutoresizingMaskIntoConstraints = false
        limitTextField.delegate = self
        self.view.addSubview(limitTextField)
        limitTextField.widthAnchor.constraint(equalToConstant: 200).isActive = true
        limitTextField.topAnchor.constraint(equalTo: self.view.topAnchor, constant: 16).isActive = true
        limitTextField.centerXAnchor.constraint(equalTo: self.view.centerXAnchor).isActive = true
    }
    
    @objc func inputTextdDidChange(notification: NSNotification) {
        let textField = notification.object as! UITextField
        if textField == limitTextField {
            if let text = textField.text {
                var eachCharacter = [Int]()
                for i in 0..<text.count {
                    let textIndex = text.index(text.startIndex, offsetBy: i)
                    eachCharacter.append(String(text[textIndex]).lengthOfBytes(using: String.Encoding.shiftJIS))
                }

                if textField.markedTextRange == nil && text.lengthOfBytes(using: String.Encoding.shiftJIS) > maxLength {
                    var countByte = 0
                    var countCharacter = 0
                    for n in eachCharacter {
                        if countByte < maxLength - 1 {
                            countByte += n
                            countCharacter += 1
                        }
                    }
                    textField.text = text.prefix(countCharacter).description
                }
            }
        }else{
            return
        }
    }
    override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
        self.view.endEditing(true)
    }
}


```
#詳細な解説(駄文)
###UITextFieldの状態をリアルタイムで監視する
textField独自の
textField:shouldChangeCharactersInRange:replacementString:
はなにやら問題があるみたいなので
NotificationCenterでUITextFieldが変更されるたびにくる通知(UITextField.textDidChangeNotification)を監視して
関数を実行します
>[iOS で文字数制限つきのテキストフィールドをちゃんと作るのは難しいという話](https://blog.niw.at/post/92806309874)
※上記リンク先ではでは入力後、さらに文中に文字を挿入した時に制限以上になった場合直前の文字列から削除していく方法が載っています
本稿では単に制限文字以上入力できない、または制限文字数以上は後ろから削除という仕様です





```swift:ViewController.viewDidLoad()

        NotificationCenter.default.addObserver(
            self,
            selector: #selector(inputTextdDidChange),  //textFielに変更があると呼びだすユーザー関数
            name: UITextField.textDidChangeNotification,  //Swiftが持ってる通知(textFielに変更があるかを監視)
            object: nil
        )
```

##UITextFieldが変更されるたびに実行する関数
半角か全角どちらか一方のみを文字数制限する場合

```swift:ViewController.inputTextdDidChange

    @objc func inputTextdDidChange(notification: NSNotification) {
        let textField = notification.object as! UITextField
        if textField == limitTextField {
            if let text = textField.text {
                if textField.markedTextRange == nil && text.count > maxLength {
                    textField.text = text.prefix(maxLength).description
                }
            }
        }else{
            return
        }
    }
```

`textField.markedTextRange == nil && text.count > maxLength　//文字変換が終了した時点で設定していた最大文字数を超えていた場合
textField.text = text.prefix(maxLength).description //textFieldの文字にtextFieldの文字から最大文字数までの文字を入力(最大文字数以上削除して再入力)
※textField.markedTextRange == nil　　//　文字変換が終了したかどうか`

このtext.countと.prefixが半角全角を区別せずに文字を数えてしまいますので
半角全角混在していなければこちらのコードでも文字数制限できます
(例:  "123あいう"はtext.countも.prefixも6文字、実際は半角で9文字,全角で4.5文字)
##こっから本題
###text.countで半角全角を区別します
.countの代わりに.lengthOfBytes(using: String.Encoding.shiftJIS)を使い
shiftJISにエンコードしてバイト数を数えるので(shiftJISでは半角＝１バイト、全角＝２バイト以外ない)
"123あいう"は9バイトが返ってきました

###.prefixでは半角全角を区別できません
.countは.lengthOfBytes(using: String.Encoding.shiftJIS)で半角全角が区別できましたが
.prefixには代用手段がありませんでしたので.prefixでは半角全角を区別できません（あったら教えて欲しえてください）

仮に全角で5文字以内の文字数制限の時
入力した文字列が全部半角なら
```if text.lengthOfBytes(using: String.Encoding.shiftJIS) > MaxLength {```
のmaxLengthを10にすれば半角文字が10個以上入力された時(10バイト＝全角5文字)
```textField.text = text.prefix(maxLength).description```
maxLengthの文字数までを自分のtextFieldに入力する(これ以上入力してもまたここまで戻ってくるのでこれ以上入れられない)


この条件で全角文字を入れると
`if text.lengthOfBytes(using: String.Encoding.shiftJIS) > MaxLength {`
"あいうえお"　全角で5文字の時(10バイトの時)
`textField.text = text.prefix(maxLength).description`
maxLengthの文字数までを自分のtextFieldに入力する
ここで.prefixは半角全角を区別しないので全角で10文字(20バイト)まで入ってしまいます
そこで`text.prefix(maxLength).description`
のmaxLengthを半角の文字数と全角の文字数によって変えなければいけません。
“1234567890”なら10
“12345678あ”なら9
“123456あい”なら8
.
.
.
“12あいうえ”なら6
のように可変的に変えるには


#.prefixでは半角全角を区別できないので可変的に最大文字数を取得し制限文字数として設定

```swift:
    @objc func inputTextdDidChange(notification: NSNotification) {
        let textField = notification.object as! UITextField
        if textField == limitTextField {
            if let text = textField.text {
                var eachCharacter = [Int]()
                for i in 0..<text.count {
                    let textIndex = text.index(text.startIndex, offsetBy: i)
                    eachCharacter.append(String(text[textIndex]).lengthOfBytes(using: String.Encoding.shiftJIS))
                }

                if textField.markedTextRange == nil && text.lengthOfBytes(using: String.Encoding.shiftJIS) > maxLength {
                    var countByte = 0
                    var countCharacter = 0
                    for n in eachCharacter {
                        if countByte < maxLength - 1 {
                            countByte += n
                            countCharacter += 1
                        }
                    }
                    textField.text = text.prefix(countCharacter).description
                }
            }
        }else{
            return
        }
    }
```

この関数自体がUITextFieldが変更されるたびに呼び出されるので
UITextFieldが変更されるたびに全角か半角か判定します

```swift:
var eachCharacter = [Int]()
    for i in 0..<text.count {
        let textIndex = text.index(text.startIndex, offsetBy: i)
        eachCharacter.append(String(text[textIndex]).lengthOfBytes(using: String.Encoding.shiftJIS))
    }
```
UITextFieldが変更されるたび
全ての文字一つずつを毎回1バイトか2バイトか判定して、配列に要素として追加
毎回検証するのは途中で文字が消されたりしても、実際の文字列と配列の要素数を乖離させないため

```swift:
    if textField.markedTextRange == nil && text.lengthOfBytes(using: String.Encoding.shiftJIS) > maxLength {
        var countByte = 0
        var countCharacter = 0
        for n in eachCharacter {
            if countByte < maxLength - 1 {
                countByte += n
                countCharacter += 1
            }
        }
        textField.text = text.prefix(countCharacter).description
    }
```
上で文字列を要素として追加した配列をfor inで分解して

バイト数を変数countCharacterに足していく
と同時に文字数を変数countByteでカウント
変数countCharacterのバイト数が上限(maxLength)に達したら終了
変数countCharacterがバイト数が上限に達した時の文字数(countByte)を最大文字数として
.prefixで文字を制限


これで半角全角問わず、バイト数を上限に可変的に最大文字数を取得し
半角全角混在した文字列でも文字数で制限することができました

#UITextViewでの方法

UITextViewのコードは割愛します
多分ほとんどUITextFieldと同じだと思います
タイトル詐欺です。すみません