NSRange范围<String.Index>

如何将NSRange转换为NSRange中的Range<String.Index>

我想要使​​用下面的UITextFieldDelegate方法:

  func textField(textField: UITextField!, shouldChangeCharactersInRange range: NSRange, replacementString string: String!) -> Bool { textField.text.stringByReplacingCharactersInRange(???, withString: string) 

在这里输入图像描述

replacingCharacters(in: NSRange, with: NSString)字符的NSString版本(与Swiftstring相反) replacingCharacters(in: NSRange, with: NSString)接受一个NSRange ,所以一个简单的解决scheme是先将String转换为NSString 。 在Swift 3和2中,委托和replace方法名称略有不同,所以取决于你正在使用的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) } 

Swift 4 (Xcode 9)开始,Swift标准库提供了在Swiftstring范围( 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: text)! print(str.substring(with: r2)) // 🇩🇪 

因此,文本字段委托方法中的文本replace现在可以完成

 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> (包括Emojis,Regional Indicators或其他扩展字形集群的所有情况),无需中间转换为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 } } 

这个方法返回一个可选的string范围,因为并不是所有的NSRange都对一个给定的Swiftstring有效。

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

一个简单的testing:

 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更新:

这个答案已经由Serhii Yakovenko给出了范围从rangeFromNSRange()的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)) // 🇩🇪 

您需要使用Range<String.Index>而不是传统的NSRange 。 我这样做的方式(也许有更好的方法)是通过string的String.Index advance移动它。

我不知道你要replace的是什么范围,但是假设你想要replace前两个字符。

 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) 

马丁R的这个答案似乎是正确的,因为它占了Unicode。

但是目前他的代码不能在Swift 2.0(Xcode 7)中编译,因为他们删除了advance()函数。 更新版本如下:

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

这与Emilie的答案类似,但是因为您特别询问如何将NSRange转换为Range<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) ... } 

@Emilie的一个很好的答案,而不是一个替代/竞争的答案。
(Xcode6-β5)

 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个字节,并且(与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 
 func textField(textField: UITextField, shouldChangeCharactersInRange range: NSRange, replacementString string: String) -> Bool { let strString = ((textField.text)! as NSString).stringByReplacingCharactersInRange(range, withString: string) } 

在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) 

这是我最好的努力。 但是这不能检查或检测错误的input参数。

 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. 

结果。

 😂 🇪🇸 🇪🇸 🇪🇸 

细节

来自NSString NSRange计算UTF-16 代码单元 。 而Swift String中的Range<String.Index>是一个不透明的相对types,它只提供相等和导航操作。 这是故意隐藏的devise。

尽pipeRange<String.Index>似乎被映射到UTF-16的代码单元偏移量,但这只是一个实现细节,我没有提到任何保证。 这意味着实施细节可以随时更改。 Swift String内部表示forms不是很明确,我不能依赖它。

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

Swift String.Index是迭代Swift Character索引,它是一个Unicode字形集群 。 然后,你必须提供适当的select正确的字形NSRange 。 如果你提供的错误范围像上面的例子那样会产生错误的结果,因为没有find适当的string聚类范围。

如果有保证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) 

我发现最清洁swift2唯一的解决办法是在NSRange上创build一个类别:

 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 } 

Swift 3.0 beta官方文档已经为这种情况提供了标准的解决scheme,标题为String.UTF16View ,部分为UTF16View元素匹配NSString字符标题

在接受的答案,我觉得可选的麻烦。 这与Swift 3一起工作,似乎没有与emojis的问题。

 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 }