やりたいこと
自作のエディタアプリに検索機能を組み込みたいと思った。
それで、UITextView内で文字列を検索して、検索結果を選択状態にしたいと考えた。
なんか簡単そうだと思ってたけど、すごい大変だったわ!
検索してRange<String.Index>を取得
UITextViewの文中を検索して結果がUITextRageあたりで返ってくるメソッドを探したが、そんなものは無さそう。Stringの方ではどうだろうと思って調べてみると、range(of:options:range:locale:)
とかいうメソッドがあるみたいだった。検索文字列が最初に現れるレンジを返してくれそうなメソッドだが返り値はRange<String.Index>という見慣れないやつで、ややこしそうな臭いがプンプンする。
文字列の検索結果は複数現れる場合も当然あるので、最初に現れるレンジだけ取得しても意味がない。range(of:options:range:locale:)
メソッドの第3引数が検索対象となる文字列の範囲になるので、これを変化させながら全ての結果を取得するのが良さそう。
このスタック・オーバーフローの記事を参考にして、まずは検索結果をRange<String.Index>の配列で取得してみた。
var resultRangeArray = [Range<String.Index>]()
let str = textView.text //本文
let word = "検索文字列"
var nextRange = str.startIndex..<str.endIndex //文字列全体をRange<String.Index>として検索対象に設定
while let range = str.range(of: word, options: [.caseInsensitive], range: nextRange, locale: Locale.current) {
resultRangeArray.append(range) //検索結果を配列に格納
nextRange = range.upperBound..<str.endIndex //見つかった文字列の後から本文の最後までで検索対象を設定
}
これで、検索結果が取得できたわけだが、print(range)
で出力してみると、
Index(_rawBits: 1178992640)..<Index(_rawBits: 1179123712)
こんなのが出て来て、扱いに困る。
UITextRangeに変換
こっちは検索結果をUITextViewの範囲選択で表示したいだけなので、UITextRangeが欲しいのである。UITextViewでコードから選択範囲を指定するには、textView.selectedTextRange
プロパティにUITextRangeをセットしてやるだけで良いのだが、Range<String.Index>からどうやってUITextRangeを作れば良いのか?
let index = 0
let range = resultRangeArray[index]
/* range:Range<String.Index>からオフセットと長さを整数で取得 */
let offset = range.lowerBound.utf16Offset(in: textView.text)
let length = str[range].count
/* offsetとlengthからUITextPositionを作成 */
let startPosition = textView.position(from: textView.beginningOfDocument, offset: offset)
let endPosition = textView.position(from: textView.beginningOfDocument, offset: (offset + length))
/* UITextPositionからUITextRangeを作成 */
textRange = textView.textRange(from: startPosition!, to: endPosition!)
分からないながらこねくり回した結果、こんな風にしてUITextRangeを取得することができました。あとはUITextViewのselectedRangeを設定するばかり。
textView.selectedTextRange = textRange
textView.becomeFirstResponder()
textView.scrollRangeToVisible(NSMakeRange(offset, length))
できあがり!
range(of:options:range:locale:)
がどういうメソッドなのかとか(Stringクラスのメソッドじゃない?)、いろいろ分からないことはあるけど、とにかくやりたいことは実現できました。