【Swift】TextViewで選択した部分の文字列を取得

実現したいこと

  • 先頭~途中/末尾までの文字を取得
  • 途中~末尾までの文字を取得
  • 途中~途中までの文字を取得

仕組み

  1. textViewの文字列を選択
  2. 選択した文字列の開始位置、長さを取得
  3. カーソルが1文字以上を選択してるかを条件分岐
  4. 選択した文字列の開始位置、終了位置を取得
  5. 部分文字列を取得

textViewの文字列を選択

f:id:nekokichi_yos2:20211231210431p:plain

選択した文字列の開始位置、長さを取得

// 開始位置
let location = textView.selectedRange.location
// 長さ
let length = textView.selectedRange.length

文字列の取得に必要。

カーソルが1文字以上を選択してるかを条件分岐

if length <= 0 {
    return
} else {
        // 文字列を取得する処理
}

文字選択の際に表示されるカーソルには、何も選択していない下記の状態が存在する。

f:id:nekokichi_yos2:20211231210435p:plain

もし条件分岐をせずに文字列を取得しようとしたら、下記のエラーが発生。

Thread 1: Fatal error: String index is out of bounds

エラー箇所は文字列を取得する下記のコード。

(原因:文字列が選択されていない → locationとlengthが0 → offsetByが-1)

let endIndex = textView.text.index(strIndex,
                     offsetBy: location+length-1, limitedBy: textView.text.endIndex)

上記のエラーを回避するため、

  • カーソルが文字列を選択
  • カーソルが文字列を選択してない

場合で処理を分岐させた。

選択した文字列の開始位置、終了位置を取得

// strIndex:0番目(Index型の0)
let strIndex = textView.text.startIndex
// Index型の開始位置、終了位置
guard let startIndex = textView.text.index(strIndex,
                     offsetBy: location, limitedBy: textView.text.endIndex),
            let endIndex = textView.text.index(strIndex,
                     offsetBy: location+length-1, limitedBy: textView.text.endIndex) else {
    return
}

仕組みを例えると、

  1. String型“123456”がある
  2. “45”を選択
  3. location = 3, length = 2
  4. startIndex = 3, endIndex = 4

startIndex, endIndexはIndex型。

f:id:nekokichi_yos2:20211231210438p:plainAppleのドキュメントより)

Index型とは、文字の場所を示す値。

printで出力すると、不規則な数字が並ぶので、中身は気にしなくていい。

print(startIndex) // Index(_rawBits: 131329)

部分文字列を取得

textView.textの要素(文字)を、Index型の変数(startIndex, endIndex)を使い、範囲で指定。

print(String(textView.text[startIndex...endIndex])) // 選択した文字列
print(type(of: textView.text[startIndex...endIndex])) // SubString

仕組みを例えると、

  1. 変数text = “123456”がある
  2. “45”を選択
  3. location = 3, length = 2
  4. startIndex = 3, endIndex = 4
  5. text[startIndex...endIndex] = “45”

startIndexとendIndexでどの範囲の文字列を取得するかを指定している。

Appleのドキュメント、SubStringはStringを分割したもの。

そのままでもprint()で出力はできても、Labelのテキストなどに代入するなら、String型にキャストする必要がある。

(でないと、下記のエラーが出る) ↓

Subscript 'subscript(_:)' requires the types 'String.Index' and 'Int' be equivalent

ストーリーボード

f:id:nekokichi_yos2:20211231210428p:plain

ソースコード

import UIKit

class ViewController: UIViewController {

    @IBOutlet weak var selectedString: UILabel!
    @IBOutlet weak var textView: UITextView!    

    override func viewDidLoad() {
        super.viewDidLoad()
        textView.delegate = self
    }
}

extension ViewController: UITextViewDelegate {
    // textViewDidChangeSelection:textViewの文字が選択されたら処理
    func textViewDidChangeSelection(_ textView: UITextView) {
        let location = textView.selectedRange.location
        let length = textView.selectedRange.length
        if length <= 0 {
        selectedString.text?.removeAll()
            return
        } else {
            let strIndex = textView.text.startIndex
            guard let startIndex = textView.text.index(strIndex, 
                                            offsetBy: location, limitedBy: textView.text.endIndex),
                  let endIndex = textView.text.index(strIndex, 
                                            offsetBy: location+length-1, limitedBy: textView.text.endIndex) else {
                return
            }
        selectedString.text = String(textView.text[startIndex...endIndex])
        }
    }
}

参考

developer.apple.com

softmoco.com

kimagureneet.hatenablog.com