NSRange到Range <String.Index>


258

如何在Swift中转换NSRangeRange<String.Index>

我想使用以下UITextFieldDelegate方法:

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

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

在此处输入图片说明


71
Apple,感谢您为我们提供了新的Range类,但没有更新任何字符串实用程序类(例如NSRegularExpression)来使用它们!
puzzl 2014年

Answers:


269

NSString版本(与Swift String相对)replacingCharacters(in: NSRange, with: NSString)接受an NSRange,因此一个简单的解决方案是将其转换StringNSStringfirst。在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)
}

14
如果textField包含emoji等多代码单元的unicode字符,则此功能将不起作用。由于它是一个输入字段,因此用户可以输入表情符号(😂),多代码单元字符的其他第1页字符,例如标志(🇪🇸)。
zaph 2014年

2
@Zaph:由于范围来自UITextField,它是用Obj-C针对NSString编写的,因此我怀疑委托回调仅提供基于字符串中unicode字符的有效范围,因此可以安全使用,但我还没有亲自测试过。
Alex Pretzlav 2015年

2
@Zaph,这是真的,我的观点是,UITextFieldDelegatetextField:shouldChangeCharactersInRange:replacementString:方法不会提供开始或结束Unicode字符内的范围,因为回调是基于从键盘输入字符的用户,而这是不可能的键入仅部分表情符号unicode字符的字符。请注意,如果委托是用Obj-C编写的,则将有同样的问题。
Alex Pretzlav 2015年

4
不是最好的答案。最容易阅读的代码,但是@ martin-r有正确的答案。
罗格

3
你们真的应该这样做(textField.text as NSString?)?.stringByReplacingCharactersInRange(range, withString: string)
Wanbok Choi

361

Swift 4(Xcode 9)开始,Swift标准库提供了在Swift字符串范围(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)

它可以用于转换NSRangeRange<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
    }
}

此方法返回一个可选的字符串范围,因为并非所有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()Serhii Yakovenko已在此答案中给出了Swift 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
    }
}

的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代码点组成的字符,例如Emojis。–例如,如果文本字段包含文本“👿”,而您删除了该字符,则range(0,2)因为在中NSRange引用了UTF-16字符NSString。但是它被视为Swift字符串中的一个 Unicode字符。– let end = advance(start, range.length)在这种情况下,引用的答案确实崩溃,并显示错误消息“严重错误:无法增加endIndex”。
马丁·R

8
@MattDiPasquale:好的。我的意图是用Unicode安全的方式回答逐字问题“如何在Swift中将NSRange转换为Range <String.Index>”(希望有人会发现它有用,但直到现在才如此:(
马丁R

5
再次感谢您为苹果所做的工作。没有像你这样的人和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。否则,您可以从字符串的结尾开始。
迈克尔·蔡

18

您需要使用Range<String.Index>而不是classic 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

这与Emilie的答案类似,但是由于您专门询问如何将转换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索引,当在带有“字符”且需要多个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

请注意,索引说明了多个代码单元。标志(区域指示符字母ES)为8个字节,(带有喜乐的脸)为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

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

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

 }

2

在Swift 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.

结果。

😂
🇪🇸
🇪🇸
🇪🇸

细节

NSRangeNSString计数UTF-16 代码单位开始。而Range<String.Index>Swift String不透明的相对类型,仅提供相等性和导航操作。这是故意隐藏的设计。

尽管Range<String.Index>似乎已映射到UTF-16代码单元偏移量,但这仅是实现细节,我找不到任何保证的内容。这意味着实现细节可以随时更改。Swift的内部表示形式String还没有很好的定义,我不能依靠它。

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

Swift String.Index是迭代Swift的索引,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)
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中使用。
Danny Bravo 2015年

0

Swift 3.0 beta官方文档在UTF16View Elements Match NSString Characters标题下的String.UTF16View标题下提供了这种情况的标准解决方案。


如果您在答案中提供链接,将很有帮助。
m5khan

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)
    }
}
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.