String.Index>

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

我想使用下面的UITextFieldDelegate方法:

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


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

enter image description here

137365 次浏览

你需要使用Range<String.Index>而不是经典的NSRange。我这样做的方式(也许有更好的方式)是通过获取字符串的String.Index,用advance移动它。

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

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)
@Emilie的精彩回答的重复,而不是替代/竞争的回答 (Xcode6-Beta5) < / p >
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

注意,索引包含多个代码单元。flag(区域指示符号字母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

replacingCharacters(in: NSRange, with: NSString)NSString版本(相对于Swift String)接受NSRange,所以一个简单的解决方案是首先将__ABC3转换为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.倍

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


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

这与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)
...


}

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

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 __abc4。Swift String中的Range<String.Index>是一个不透明的相对类型,只提供相等和导航操作。这是有意隐藏的设计。

虽然Range<String.Index>似乎映射到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)

作为斯威夫特4 (Xcode 9), Swift标准 库提供了在Swift字符串范围之间转换的方法 (Range<String.Index>)和NSString范围(NSRange)。 例子:< / p >

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
}
}
这个方法返回一个可选字符串范围,因为不是所有的__abc0

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在这个答案,我包括它 这里为完整性:

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)) // 🇩🇪

我发现最干净的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)
}
}

然后调用它from for textfield delegate函数:

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
}

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

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

Swift 3.0 beta版官方文档在UTF16View Elements匹配NSString字符标题节的标题字符串。UTF16View下为这种情况提供了标准解决方案

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


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


}

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

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

对于详细答案请点击这里

因为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: العلاجية