3
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

UITextViewで検索した文字列を選択

Last updated at Posted at 2019-09-11

やりたいこと

自作のエディタアプリに検索機能を組み込みたいと思った。
それで、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))

できあがり!

ezgif.com-crop.gif

range(of:options:range:locale:)がどういうメソッドなのかとか(Stringクラスのメソッドじゃない?)、いろいろ分からないことはあるけど、とにかくやりたいことは実現できました。

3
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?