【问题标题】:NSRange to Range<String.Index>NSRange 到 Range<String.Index>
【发布时间】:2014-09-28 02:05:37
【问题描述】:

如何在 Swift 中将 NSRange 转换为 Range&lt;String.Index&gt;

我想使用下面的UITextFieldDelegate方法:

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

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

【问题讨论】:

  • 感谢 Apple,为我们提供了新的 Range 类,但没有更新任何字符串实用程序类,例如 NSRegularExpression,以使用它们!

标签: nsstring swift ios8 nsrange


【解决方案1】:

Swift 4 (Xcode 9) 开始,Swift 标准 库提供了在 Swift 字符串范围之间进行转换的方法 (Range&lt;String.Index&gt;) 和 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&lt;String.Index&gt; (包括表情符号、区域指标或其他扩展的所有情况 字素簇),无需中间转换为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
    }
}

此方法返回一个 可选 字符串范围,因为并非所有 NSRanges 对给定的 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 更新:

rangeFromNSRange() 的 Swift 2 版本已经给出 通过 Serhii Yakovenko 在this answer,我包括它 这里是为了完整性:

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
    }
}

NSRangeFromRange() 的 Swift 2 版本是

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)) // ??

【讨论】:

  • 对于包含多个 UTF-16 代码点的字符,该代码无法正常工作,例如表情符号。 – 例如,如果文本字段包含文本“?”并且您删除了该字符,则 range(0,2),因为 NSRange 指的是 NSString 中的 UTF-16 字符。但它算作 Swift 字符串中的 一个 Unicode 字符。 – 引用答案中的let end = advance(start, range.length) 在这种情况下确实会崩溃,并显示错误消息“致命错误:无法增加 endIndex”。
  • @MattDiPasquale:当然。我的意图是以 Unicode 安全的方式回答逐字的问题 "How can I convert NSRange to Range in Swift" (希望有人会发现它有用,这不是案例到现在:(
  • 再次感谢您为他们完成 Apple 的工作。如果没有像你这样的人和 Stack Overflow,我就无法进行 iOS/OS X 开发。生命太短暂了。
  • @jiminybob99:Command-click on String 跳转到 API 参考,阅读所有方法和 cmets,然后尝试不同的方法直到它起作用 :)
  • 对于let from16 = utf16.index(utf16.startIndex, offsetBy: nsRange.location, limitedBy: utf16.endIndex),我认为您实际上想将其限制为utf16.endIndex - 1。否则,您可以从字符串的末尾开始。
【解决方案2】:

replacingCharacters(in: NSRange, with: NSString)NSString 版本(相对于 Swift 字符串)接受NSRange,因此一个简单的解决方案是首先将String 转换为NSString。 Swift 3 和 2 中的委托和替换方法名称略有不同,因此取决于您使用的 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)
}

斯威夫特 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)
}

【讨论】:

  • 如果 textField 包含多代码单元 unicode 字符(如表情符号),则此操作无效。由于它是一个输入字段,因此用户可以输入表情符号(?),其他第 1 页字符的多代码单元字符,例如标志(??)。
  • @Zaph:由于范围来自 UITextField,它是用 Obj-C 针对 NSString 编写的,我怀疑委托回调只会提供基于字符串中 unicode 字符的有效范围,因此使用起来很安全,但我没有亲自测试过。
  • @Zaph,没错,我的意思是UITextFieldDelegatetextField:shouldChangeCharactersInRange:replacementString: 方法不会提供在Unicode 字符内开始或结束的范围,因为回调是基于用户的从键盘输入字符,并且不能只输入表情符号 unicode 字符的一部分。请注意,如果委托是用 Obj-C 编写的,也会有同样的问题。
  • 不是最佳答案。最容易阅读的代码,但@martin-r 有正确的答案。
  • 其实你们应该这样做(textField.text as NSString?)?.stringByReplacingCharactersInRange(range, withString: string)
【解决方案3】:

Martin R 的This answer 似乎是正确的,因为它考虑了 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)
    }
}

【讨论】:

    【解决方案4】:

    您需要使用Range&lt;String.Index&gt; 而不是经典的NSRange。我这样做的方法(也许有更好的方法)是将字符串的String.Indexadvance 一起移动。

    我不知道您要替换的范围,但我们假设您要替换前 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)
    

    【讨论】:

      【解决方案5】:

      这与 Emilie 的回答类似,但是因为您特别询问如何将 NSRange 转换为 Range&lt;String.Index&gt; 您会这样做:

      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) 
           ...
      
      }
      

      【讨论】:

      • 字符串的字符视图和 UTF16 视图的长度可能不同。此函数针对字符串的字符视图使用 UTF16 索引(这就是 NSRange 所说的,尽管它的组件是整数),当用于具有多个 UTF16 单位来表示的“字符”的字符串时,它可能会失败。跨度>
      【解决方案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
      

      请注意索引占多个代码单元。标志(区域指示符符号字母 ES)为 8 个字节,而(FACE WITH TEARS OF JOY)为 4 个字节。 (在这种特殊情况下,UTF-8、UTF-16 和 UTF-32 表示的字节数是相同的。)

      将其包装在一个函数中:

      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
      

      【讨论】:

        【解决方案7】:
        func textField(textField: UITextField, shouldChangeCharactersInRange range: NSRange, replacementString string: String) -> Bool {
        
               let strString = ((textField.text)! as NSString).stringByReplacingCharactersInRange(range, withString: string)
        
         }
        

        【讨论】:

          【解决方案8】:

          在 Swift 2.0 中假设 func textField(textField: UITextField, shouldChangeCharactersInRange range: NSRange, replacementString string: String) -&gt; 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)
          

          【讨论】:

            【解决方案9】:

            这是我最大的努力。但这不能检查或检测错误的输入参数。

            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.
            

            结果。

            ?
            ??
            ??
            ??
            

            详情

            NSRange 来自NSString 计数 UTF-16 code-units。而来自 Swift 的 StringRange&lt;String.Index&gt; 是一个 opaque 相对类型,它只提供相等和导航操作。这是故意隐藏的设计。

            虽然Range&lt;String.Index&gt; 似乎映射到 UTF-16 代码单元偏移量,但这只是一个实现细节,我找不到任何关于任何保证的提及。这意味着可以随时更改实施细节。 Swift String 的内部表示不是很明确,我不能依赖它。

            NSRange 值可以直接映射到String.UTF16View 索引。但是没有办法将其转换为String.Index

            Swift String.Index 是迭代 Swift Character 的索引,这是一个 Unicode 字形集群。然后,您必须提供正确的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)
            【解决方案10】:

            我发现最干净的 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 的答案。
            【解决方案11】:
            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)
                }
            }
            

            【讨论】:

              【解决方案12】:
              func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
                  
                  guard let current = textField.text, let r = Range(range, in: current) else {
                      return false
                  }
                  
                  let text = current.replacingCharacters(in: r, with: string)
                  // ...
                  return true
              }
              

              【讨论】:

                【解决方案13】:

                Swift 3.0 beta 官方文档在 UTF16View Elements Match NSString Characters title

                部分的标题 String.UTF16View 下提供了针对这种情况的标准解决方案

                【讨论】:

                【解决方案14】:

                在接受的答案中,我发现选项很麻烦。这适用于 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
                }
                

                【讨论】:

                  【解决方案15】:

                  因为NSRangeNSString 操作中使用时,表示UTF-16 单元的位置。那么转换为String.Index 的最短方法是通过String.Index(utf16Offset: Int, in: StringProtocol) 初始化器进行初始化。

                  let string = "...."
                  let nsRange = NSRange(....) // This NSRange belongs to `string` variable.
                  let range = String.Index(utf16Offset: nsRange.lowerBound, in: string)
                          ..< String.Index(utf16Offset: nsRange.upperBound, in: string)
                  

                  例子:

                  let string = "a-\u{1112}\u{1161}\u{11AB}-?-\u{E9}\u{20DD}-‼-?-(العلاجية)-f"
                  let rangeOfLeftParenthesis = (string as NSString).range(of: "(")
                  let rangeOfRightParenthesis = (string as NSString).range(of: ")")
                  print("string: \(string)")
                  let lowerBound = String.Index.init(utf16Offset: rangeOfLeftParenthesis.upperBound, in: string)
                  let upperBound = String.Index.init(utf16Offset: rangeOfRightParenthesis.lowerBound, in: string)
                  let arabicSentenceRange = lowerBound ..< upperBound // Instance of `Range<String.Index>`
                  print("arabicSentenceRange: \(string[arabicSentenceRange])")
                  

                  输出:

                  string: a-한-?-é⃝-‼-?-(العلاجية)-f
                  arabicSentenceRange: العلاجية
                  

                  【讨论】:

                    【解决方案16】:

                    Swift 5 解决方案

                    主扩展的简短回答

                    extension NSRange {
                    
                        public init(range: Range<String.Index>, 
                                    originalText: String) {
                    
                            self.init(location: range.lowerBound.utf16Offset(in: originalText),
                                      length: range.upperBound.utf16Offset(in: originalText) - range.lowerBound.utf16Offset(in: originalText))
                        }
                    }
                    

                    对于detailed answer check here

                    【讨论】:

                    • 如果您使用的是utf16Offset,那么值得使用Range&lt;String.UTF16View.Index&gt; 而不是Range&lt;String.Index&gt;
                    猜你喜欢
                    • 1970-01-01
                    • 1970-01-01
                    • 1970-01-01
                    • 2015-01-18
                    • 1970-01-01
                    • 1970-01-01
                    • 1970-01-01
                    • 2017-01-24
                    • 2018-11-15
                    相关资源
                    最近更新 更多