【Swift】UITextViewで文字数制限と行数制限を行う方法

#はじめに

クイズアプリを作成する上でTextViewを使用したので、
その時に学んだことを備忘録がてら共有できたらなと思います。

環境
・Swift version 5.3
・XCode version 12.3

#完成形
gitや画像を載せようと思ったのですが、月の上限を超していたため載せれませんでした・・・。
申し訳ございませんが文字のみになってしまいます。

完成形の機能としては下記のような機能が実装されています。


**・プレースホルダー**
　TextFieldのように簡単にプレースホルダーを定義できないTextViewですが、
　プレースホルダーの実装をしてみました。
**・文字数制限と文字数カウント**
　今回は５０文字とういう制限で実装しました。
**・行数制限**
　今回は７行という制限で実装しました。
**・キーボードの非表示**
　UITapGestureRecognizerを使い実装しました。

#コード

全てコピペしてもうまく実行されません。
`@IBOutlet`やその変数に関する箇所はご自身の内容にしたがってください。

```swift:ViewController.swift
import UIKit

class ViewController: UIViewController, UITextViewDelegate {
    
    // storyboardのオブジェクトと紐付け
    // 文字入力箇所
    @IBOutlet weak var questionTextView: UITextView!
    // 何文字入力されたか表示するラベル
    @IBOutlet weak var textCountLabel: UILabel!
    
    private let placeholder = "問題文を入力してください。\n\n問題文は、50文字・7行以内で記載してください。"
    private let textLength = 50
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        questionTextView.delegate = self
        questionTextView.text = placeholder
        
        //タップでキーボードを下げる
        let tapGesture = UITapGestureRecognizer(target: self, action: #selector(dismissKeyboard))
        self.view.addGestureRecognizer(tapGesture)
        //下にスワイプでキーボードを下げる
        let swipeDownGesture = UISwipeGestureRecognizer(target: self, action: #selector(dismissKeyboard))
        swipeDownGesture.direction = .down
        self.view.addGestureRecognizer(swipeDownGesture)
    }
    
    @objc func dismissKeyboard() {
        self.view.endEditing(true)
    }
    
    // 文字数制限＆行数制限
    func textView(_ textView: UITextView, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool {
        //既に存在する改行数
        let existingLines = textView.text.components(separatedBy: .newlines)
        //新規改行数
        let newLines = text.components(separatedBy: .newlines)
        //最終改行数。-1は編集したら必ず1改行としてカウントされるから。
        let linesAfterChange = existingLines.count + newLines.count - 1
        
        return linesAfterChange <= 7 && questionTextView.text.count + (text.count - range.length) <= textLength
    }
    
    // TextViewの内容が変わるたびに実行される
    func textViewDidChange(_ textView: UITextView) {
        //既に存在する改行数
        let existingLines = textView.text.components(separatedBy: .newlines)
        if existingLines.count <= 7 {
            self.textCountLabel.text = "\(questionTextView.text.count) / \(textLength)"
        }
    }
    
    // 入力開始時にプレースホルダーの内容が入っていたら空にする
    func textViewDidBeginEditing(_ textView: UITextView) {
        if textView.text == placeholder {
            textView.text = nil
            textView.textColor = .darkText
        }
    }
    
    // 入力終了後に文字が入力されていなかったらプレースホルダー表示
    func textViewDidEndEditing(_ textView: UITextView) {
        if textView.text.isEmpty {
            textView.text = placeholder
            textView.textColor = .darkGray
        }
    }

}

```

##コード説明

デリゲートメソッドを使用するためにプロトコルを追加する必要があります。
また、自身のtextViewにデリゲートの処理を委譲してください。

```swift
class MakeQuizViewController: UIViewController, UITextViewDelegate { 
    override func viewDidLoad() {
        super.viewDidLoad()

        questionTextView.delegate = self
    }
}
```

文字数制限と行数制限は下記のメソッドで行っています。
下記のメソッドはreturnの内容がtrueの限りtextViewの編集が可能になるメソッドです。（多分）

**つまり、返り値がfalseになったら入力ができなくなります。**

今回は、**入力文字数が５０文字以下 かつ 行数が7行以下**の場合はtrueが返ります。

行数の取得は文字列を`components(separatedBy:)`メソッドで区切り取得しています。

`components(separatedBy:)`メソッドを使用すると返り値が配列で返ってきますので、
`existingLines.count`で配列の個数を取得しています。

この個数の合計が現在改行されている合計です。

文字数は、
`questionTextView.text.count`で現在入力されている文字数に、
`(text.count - range.length)`で新しく入力した文字数を足して計算しています。


```swift

    func textView(_ textView: UITextView, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool {
        //既に存在する改行数
        let existingLines = textView.text.components(separatedBy: .newlines)
        //新規改行数
        let newLines = text.components(separatedBy: .newlines)
        //最終改行数。-1は編集したら必ず1改行としてカウントされるから。
        let linesAfterChange = existingLines.count + newLines.count - 1
        
        return linesAfterChange <= 7 && questionTextView.text.count + (text.count - range.length) <= textLength
    }
```

入力されるたびにテキストのカウント用のラベルを更新しています。

```swift
    // TextViewの内容が変わるたびに実行される
    func textViewDidChange(_ textView: UITextView) {
        //既に存在する改行数
        let existingLines = textView.text.components(separatedBy: .newlines)
        if existingLines.count <= 7 {
            self.textCountLabel.text = "\(questionTextView.text.count) / \(textLength)"
        }
    } 
```

`textViewDidBeginEditing(_ textView:)`メソッドは、
入力開始時にプレースホルダーが入力されている場合は、
プレースホルダを消したいので`textView.text = nil`を行っています。

`textViewDidEndEditing(_ textView:)`メソッドは、
入力終了時に、TextViewの値が空だったらプレースホルダーを入力しています。
　

```swift
    func textViewDidBeginEditing(_ textView: UITextView) {
        if textView.text == placeholder {
            textView.text = nil
            textView.textColor = .darkText
        }
    }

    func textViewDidEndEditing(_ textView: UITextView) {
        if textView.text.isEmpty {
            textView.text = placeholder
            textView.textColor = .darkGray
        }
    }
```

入力状態でキーボードが開かれるのでそれを閉じるための機能です。

タップかスワイプが行われたら`dismissKeyboard()`メソッドを実行し、
`self.view.endEditing(true)`でキーボードを閉じます。

```swift

//タップでキーボードを下げる
let tapGesture = UITapGestureRecognizer(target: self, action: #selector(dismissKeyboard))
self.view.addGestureRecognizer(tapGesture)

//下にスワイプでキーボードを下げる
let swipeDownGesture = UISwipeGestureRecognizer(target: self, action: #selector(dismissKeyboard))
swipeDownGesture.direction = .down
self.view.addGestureRecognizer(swipeDownGesture)

@objc func dismissKeyboard() {
    self.view.endEditing(true)
}
```


#さいごに


画像などが無い状態での説明でしたのでわかりにくかったらすみません。

そんなに難しいことは行っていないので、
別の方の記事も参考にしながら実装してみてください！

以上、最後までご覧いただきありがとうございました。