NSRangeからRange <String.Index>へ


258

どのようにして変換することができますNSRangeRange<String.Index>スウィフトに?

次のUITextFieldDelegate方法を使用します。

    func textField(textField: UITextField!,
        shouldChangeCharactersInRange range: NSRange,
        replacementString string: String!) -> Bool {

textField.text.stringByReplacingCharactersInRange(???, withString: string)

ここに画像の説明を入力してください


71
新しいRangeクラスを提供してくれたAppleに感謝しますが、それを使用するためのNSRegularExpressionなどの文字列ユーティリティクラスの更新はありません。
パズル14

回答:


269

NSStringバージョン(Swift Stringではなく)はをreplacingCharacters(in: NSRange, with: NSString)受け入れるためNSRange、1つの簡単な解決策は最初に変換するStringことNSStringです。デリゲートと置換メソッドの名前は、Swift 3と2では若干異なるため、使用しているSwiftによって異なります。

Swift 3.0

func textField(_ textField: UITextField,
               shouldChangeCharactersIn range: NSRange,
               replacementString string: String) -> Bool {

  let nsString = textField.text as NSString?
  let newString = nsString?.replacingCharacters(in: range, with: string)
}

Swift 2.x

func textField(textField: UITextField,
               shouldChangeCharactersInRange range: NSRange,
               replacementString string: String) -> Bool {

    let nsString = textField.text as NSString?
    let newString = nsString?.stringByReplacingCharactersInRange(range, withString: string)
}

14
textField絵文字などのマルチコードユニットのUnicode文字が含まれている場合、これは機能しません。絵文字(😂)や、フラグ(🇪🇸)などのマルチコードユニット文字の他のページ1文字は、ユーザーが入力できる入力フィールドであるためです。
zaph 2014

2
@Zaph:範囲はNSStringに対してObj-Cで記述されたUITextFieldから取得されるため、文字列内のUnicode文字に基づく有効な範囲のみがデリゲートコールバックによって提供されるので、これは安全に使用できます、しかし私はそれを個人的にテストしていません。
Alex Pretzlav、2015年

2
@Zaph、それは真実です、私のポイントは、コールバックはユーザーがキーボードから文字を入力することに基づいており、一部だけを入力することはできないため、UITextFieldDelegate' textField:shouldChangeCharactersInRange:replacementString:メソッドはUnicode文字内で開始または終了する範囲を提供しないということです絵文字のユニコード文字の。デリゲートがObj-Cで記述されている場合、同じ問題が発生することに注意してください。
Alex Pretzlav、2015年

4
最良の答えではありません。コードを読み取るのが最も簡単ですが、@ martin-rが正解です。
Rog

3
実際、皆さんはこれを行う必要があります(textField.text as NSString?)?.stringByReplacingCharactersInRange(range, withString: string)
Wanbok Choi

361

以下のようスイフト4(Xcodeの9)、スウィフト標準ライブラリは、スイフト列範囲(との間で変換する方法を提供するRange<String.Index>)およびNSString(範囲NSRange)。例:

let str = "a👿b🇩🇪c"
let r1 = str.range(of: "🇩🇪")!

// String range to NSRange:
let n1 = NSRange(r1, in: str)
print((str as NSString).substring(with: n1)) // 🇩🇪

// NSRange back to String range:
let r2 = Range(n1, in: str)!
print(str[r2]) // 🇩🇪

したがって、テキストフィールドデリゲートメソッドのテキスト置換は、次のように実行できます。

func textField(_ textField: UITextField,
               shouldChangeCharactersIn range: NSRange,
               replacementString string: String) -> Bool {

    if let oldString = textField.text {
        let newString = oldString.replacingCharacters(in: Range(range, in: oldString)!,
                                                      with: string)
        // ...
    }
    // ...
}

(Swift 3以前の古い回答:)

Swift 1.2以降String.Index、イニシャライザを持っています

init?(_ utf16Index: UTF16Index, within characters: String)

これは、に中間変換することなく、正しく変換するNSRangeために使用できますRange<String.Index>(絵文字、地域インジケーター、またはその他の拡張書記素クラスターのすべてのケースを含む)NSString

extension String {
    func rangeFromNSRange(nsRange : NSRange) -> Range<String.Index>? {
        let from16 = advance(utf16.startIndex, nsRange.location, utf16.endIndex)
        let to16 = advance(from16, nsRange.length, utf16.endIndex)
        if let from = String.Index(from16, within: self),
            let to = String.Index(to16, within: self) {
                return from ..< to
        }
        return nil
    }
}

このメソッドは、すべてではないため、オプションの文字列範囲を返しますNSRangeのが特定のSwift文字列に対して有効ではます。

UITextFieldDelegateデリゲートメソッドは次のように書くことができます

func textField(textField: UITextField, shouldChangeCharactersInRange range: NSRange, replacementString string: String) -> Bool {

    if let swRange = textField.text.rangeFromNSRange(range) {
        let newString = textField.text.stringByReplacingCharactersInRange(swRange, withString: string)
        // ...
    }
    return true
}

逆変換は

extension String {
    func NSRangeFromRange(range : Range<String.Index>) -> NSRange {
        let utf16view = self.utf16
        let from = String.UTF16View.Index(range.startIndex, within: utf16view) 
        let to = String.UTF16View.Index(range.endIndex, within: utf16view)
        return NSMakeRange(from - utf16view.startIndex, to - from)
    }
}

簡単なテスト:

let str = "a👿b🇩🇪c"
let r1 = str.rangeOfString("🇩🇪")!

// String range to NSRange:
let n1 = str.NSRangeFromRange(r1)
println((str as NSString).substringWithRange(n1)) // 🇩🇪

// NSRange back to String range:
let r2 = str.rangeFromNSRange(n1)!
println(str.substringWithRange(r2)) // 🇩🇪

Swift 2の更新:

のSwift 2バージョンrangeFromNSRange()はすでにSerhii Yakovenkoによってこの回答で提供されていました。完全を期すためにここに含めます。

extension String {
    func rangeFromNSRange(nsRange : NSRange) -> Range<String.Index>? {
        let from16 = utf16.startIndex.advancedBy(nsRange.location, limit: utf16.endIndex)
        let to16 = from16.advancedBy(nsRange.length, limit: utf16.endIndex)
        if let from = String.Index(from16, within: self),
            let to = String.Index(to16, within: self) {
                return from ..< to
        }
        return nil
    }
}

のSwift 2バージョンNSRangeFromRange()

extension String {
    func NSRangeFromRange(range : Range<String.Index>) -> NSRange {
        let utf16view = self.utf16
        let from = String.UTF16View.Index(range.startIndex, within: utf16view)
        let to = String.UTF16View.Index(range.endIndex, within: utf16view)
        return NSMakeRange(utf16view.startIndex.distanceTo(from), from.distanceTo(to))
    }
}

Swift 3(Xcode 8)のアップデート:

extension String {
    func nsRange(from range: Range<String.Index>) -> NSRange {
        let from = range.lowerBound.samePosition(in: utf16)
        let to = range.upperBound.samePosition(in: utf16)
        return NSRange(location: utf16.distance(from: utf16.startIndex, to: from),
                       length: utf16.distance(from: from, to: to))
    }
}

extension String {
    func range(from nsRange: NSRange) -> Range<String.Index>? {
        guard
            let from16 = utf16.index(utf16.startIndex, offsetBy: nsRange.location, limitedBy: utf16.endIndex),
            let to16 = utf16.index(utf16.startIndex, offsetBy: nsRange.location + nsRange.length, limitedBy: utf16.endIndex),
            let from = from16.samePosition(in: self),
            let to = to16.samePosition(in: self)
            else { return nil }
        return from ..< to
    }
}

例:

let str = "a👿b🇩🇪c"
let r1 = str.range(of: "🇩🇪")!

// String range to NSRange:
let n1 = str.nsRange(from: r1)
print((str as NSString).substring(with: n1)) // 🇩🇪

// NSRange back to String range:
let r2 = str.range(from: n1)!
print(str.substring(with: r2)) // 🇩🇪

7
そのコードは、絵文字など、複数のUTF-16コードポイントで構成される文字に対しては正しく機能しません。-テキストフィールドが「👿」のテキストが含まれており、その文字を削除した場合の例として、そしてrangeある(0,2)ためNSRangeでUTF-16文字を指しNSString。ただし、Swift文字列では1つの Unicode文字としてカウントされます。– let end = advance(start, range.length)この場合、参照されている回答からエラーメッセージ「致命的なエラー:endIndexをインクリメントできません」でクラッシュします。
マーティンR

8
@MattDiPasquale:もちろんです。私の意図は、Unicodeセーフな方法で逐語的質問「NSRangeをSwiftでRange <String.Index>に変換するにはどうすればよいですに答えることでした(誰かが役立つと思うので、これまではそうではありません:(
Martin R

5
もう一度、Appleの仕事をしてくれてありがとう。あなたのような人たちとStack Overflowがなければ、iOS / OS X開発を行う方法はありません。人生短し。
Womble

1
@ jiminybob99:コマンドStringリファレンスをクリックしてAPIリファレンスにジャンプし、すべてのメソッドとコメントを読んでから、機能するまでさまざまなことを試してください:)
Martin R

1
以下のためにlet from16 = utf16.index(utf16.startIndex, offsetBy: nsRange.location, limitedBy: utf16.endIndex)、私はあなたが実際でそれを制限したいと思いますutf16.endIndex - 1。それ以外の場合は、文字列の最後から始めることができます。
Michael Tsai

18

Range<String.Index>クラシックの代わりに使用する必要がありますNSRange。私がそれを行う方法(おそらくより良い方法があるかもしれません)は、で文字列をString.Index移動することadvanceです。

置換しようとしている範囲はわかりませんが、最初の2文字を置換したいとしましょう。

var start = textField.text.startIndex // Start at the string's start index
var end = advance(textField.text.startIndex, 2) // Take start index and advance 2 characters forward
var range: Range<String.Index> = Range<String.Index>(start: start,end: end)

textField.text.stringByReplacingCharactersInRange(range, withString: string)

18

Martin Rによるこの回答は、Unicodeを考慮しているため、正しいようです。

ただし、ポスト(Swift 1)の時点では、彼のコードはSwift 2.0(Xcode 7)ではコンパイルされませんadvance()。これは、関数が削除されたためです。更新されたバージョンは以下のとおりです。

スウィフト2

extension String {
    func rangeFromNSRange(nsRange : NSRange) -> Range<String.Index>? {
        let from16 = utf16.startIndex.advancedBy(nsRange.location, limit: utf16.endIndex)
        let to16 = from16.advancedBy(nsRange.length, limit: utf16.endIndex)
        if let from = String.Index(from16, within: self),
            let to = String.Index(to16, within: self) {
                return from ..< to
        }
        return nil
    }
}

スウィフト3

extension String {
    func rangeFromNSRange(nsRange : NSRange) -> Range<String.Index>? {
        if let from16 = utf16.index(utf16.startIndex, offsetBy: nsRange.location, limitedBy: utf16.endIndex),
            let to16 = utf16.index(from16, offsetBy: nsRange.length, limitedBy: utf16.endIndex),
            let from = String.Index(from16, within: self),
            let to = String.Index(to16, within: self) {
                return from ..< to
        }
        return nil
    }
}

スウィフト4

extension String {
    func rangeFromNSRange(nsRange : NSRange) -> Range<String.Index>? {
        return Range(nsRange, in: self)
    }
}

7

あなたが変換する方法を具体的に尋ねたので、これはしかし、エミリーの回答に似ているNSRangeRange<String.Index>あなたはこのような何かをするだろう。

func textField(textField: UITextField, shouldChangeCharactersInRange range: NSRange, replacementString string: String) -> Bool {

     let start = advance(textField.text.startIndex, range.location) 
     let end = advance(start, range.length) 
     let swiftRange = Range<String.Index>(start: start, end: end) 
     ...

}

2
文字列の文字ビューとUTF16ビューは、長さが異なる場合があります。この関数はNSRange、文字列の文字ビューに対してUTF16インデックス(構成要素は整数ですが)を使用しています。これは、複数のUTF16単位を表す「文字」を含む文字列で使用すると失敗する可能性があります。
Nate Cook

6

@Emilieによる素晴らしい答えのリフ。置換/競合する答えではありません。
(Xcode6-Beta5)

var original    = "🇪🇸😂This is a test"
var replacement = "!"

var startIndex = advance(original.startIndex, 1) // Start at the second character
var endIndex   = advance(startIndex, 2) // point ahead two characters
var range      = Range(start:startIndex, end:endIndex)
var final = original.stringByReplacingCharactersInRange(range, withString:replacement)

println("start index: \(startIndex)")
println("end index:   \(endIndex)")
println("range:       \(range)")
println("original:    \(original)")
println("final:       \(final)")

出力:

start index: 4
end index:   7
range:       4..<7
original:    🇪🇸😂This is a test
final:       🇪🇸!his is a test

インデックスが複数のコード単位を占めていることに注意してください。フラグ(REGIONAL INDICATOR SYMBOL LETTERS ES)は8バイト、(FACE WITH TEARS OF JOY)は4バイトです。(この特定のケースでは、UTF-8、UTF-16、およびUTF-32表現のバイト数は同じであることがわかります。)

それをfuncでラップする:

func replaceString(#string:String, #with:String, #start:Int, #length:Int) ->String {
    var startIndex = advance(original.startIndex, start) // Start at the second character
    var endIndex   = advance(startIndex, length) // point ahead two characters
    var range      = Range(start:startIndex, end:endIndex)
    var final = original.stringByReplacingCharactersInRange(range, withString: replacement)
    return final
}

var newString = replaceString(string:original, with:replacement, start:1, length:2)
println("newString:\(newString)")

出力:

newString: !his is a test

4
func textField(textField: UITextField, shouldChangeCharactersInRange range: NSRange, replacementString string: String) -> Bool {

       let strString = ((textField.text)! as NSString).stringByReplacingCharactersInRange(range, withString: string)

 }

2

スイフト2.0の仮定func textField(textField: UITextField, shouldChangeCharactersInRange range: NSRange, replacementString string: String) -> Bool {

var oldString = textfield.text!
let newRange = oldString.startIndex.advancedBy(range.location)..<oldString.startIndex.advancedBy(range.location + range.length)
let newString = oldString.stringByReplacingCharactersInRange(newRange, withString: string)

1

これが私の最善の努力です。しかし、これは間違った入力引数をチェックまたは検出できません。

extension String {
    /// :r: Must correctly select proper UTF-16 code-unit range. Wrong range will produce wrong result.
    public func convertRangeFromNSRange(r:NSRange) -> Range<String.Index> {
        let a   =   (self as NSString).substringToIndex(r.location)
        let b   =   (self as NSString).substringWithRange(r)

        let n1  =   distance(a.startIndex, a.endIndex)
        let n2  =   distance(b.startIndex, b.endIndex)

        let i1  =   advance(startIndex, n1)
        let i2  =   advance(i1, n2)

        return  Range<String.Index>(start: i1, end: i2)
    }
}

let s   =   "🇪🇸😂"
println(s[s.convertRangeFromNSRange(NSRange(location: 4, length: 2))])      //  Proper range. Produces correct result.
println(s[s.convertRangeFromNSRange(NSRange(location: 0, length: 4))])      //  Proper range. Produces correct result.
println(s[s.convertRangeFromNSRange(NSRange(location: 0, length: 2))])      //  Improper range. Produces wrong result.
println(s[s.convertRangeFromNSRange(NSRange(location: 0, length: 1))])      //  Improper range. Produces wrong result.

結果。

😂
🇪🇸
🇪🇸
🇪🇸

細部

NSRangefrom NSStringカウントUTF-16 コード単位 s。そしてRange<String.Index>スウィフトからString不透明です等価とナビゲーション操作のみを提供相対型があります。これは意図的に隠されたデザインです。

Range<String.Index>UTF-16コード単位のオフセットにマップされているようですが、これは実装の詳細にすぎず、保証についての言及はありませんでした。つまり、実装の詳細はいつでも変更できます。Swiftの内部表現はString明確に定義されておらず、それに頼ることはできません。

NSRange値は直接String.UTF16Viewインデックスにマッピングできます。しかし、それを変換する方法はありませんString.Index

Swift String.IndexCharacterUnicode書記素クラスターであるSwiftを反復するためのインデックスです。次に、NSRange正しい書記素クラスターを選択する適切なものを提供する必要があります。上記の例のように間違った範囲を指定すると、適切な書記素クラスターの範囲を把握できなかったため、誤った結果が生成されます。

String.Index UTF-16コード単位オフセットであること保証されている場合、問題は単純になります。しかし、それは起こりそうにありません。

逆変換

とにかく、逆変換を正確に行うことができます。

extension String {
    /// O(1) if `self` is optimised to use UTF-16.
    /// O(n) otherwise.
    public func convertRangeToNSRange(r:Range<String.Index>) -> NSRange {
        let a   =   substringToIndex(r.startIndex)
        let b   =   substringWithRange(r)

        return  NSRange(location: a.utf16Count, length: b.utf16Count)
    }
}
println(convertRangeToNSRange(s.startIndex..<s.endIndex))
println(convertRangeToNSRange(s.startIndex.successor()..<s.endIndex))

結果。

(0,6)
(4,2)

Swift 2では、次のreturn NSRange(location: a.utf16Count, length: b.utf16Count)ように変更する必要がありますreturn NSRange(location: a.utf16.count, length: b.utf16.count)
Elliot

1

私が見つけた最もきれいなswift2のみのソリューションは、NSRangeにカテゴリを作成することです:

extension NSRange {
    func stringRangeForText(string: String) -> Range<String.Index> {
        let start = string.startIndex.advancedBy(self.location)
        let end = start.advancedBy(self.length)
        return Range<String.Index>(start: start, end: end)
    }
}

そして、それをテキストフィールドデリゲート関数から呼び出します:

func textField(textField: UITextField, shouldChangeCharactersInRange range: NSRange, replacementString string: String) -> Bool {
    let range = range.stringRangeForText(textField.text)
    let output = textField.text.stringByReplacingCharactersInRange(range, withString: string)

    // your code goes here....

    return true
}

Swift2の最新のアップデート後、コードが機能しなくなったことに気付きました。Swift2で動作するように回答を更新しました。
ダニーブラボー

0

スウィフト3.0ベータ版公式ドキュメントには、タイトルの下にこのような状況のためにその標準的なソリューションを提供してきましたString.UTF16ViewセクションでUTF16View要素マッチNSStringの文字のタイトル


回答にリンクを提供すると役立つでしょう。
m5khan 2016年

0

受け入れられた答えで、私はオプションが面倒だと思います。これはSwift 3で動作し、絵文字には問題がないようです。

func textField(_ textField: UITextField, 
      shouldChangeCharactersIn range: NSRange, 
      replacementString string: String) -> Bool {

  guard let value = textField.text else {return false} // there may be a reason for returning true in this case but I can't think of it
  // now value is a String, not an optional String

  let valueAfterChange = (value as NSString).replacingCharacters(in: range, with: string)
  // valueAfterChange is a String, not an optional String

  // now do whatever processing is required

  return true  // or false, as required
}

0
extension StringProtocol where Index == String.Index {

    func nsRange(of string: String) -> NSRange? {
        guard let range = self.range(of: string) else {  return nil }
        return NSRange(range, in: self)
    }
}
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.