简体   繁体   English

NSR 范围到范围<String.Index>

[英]NSRange to Range<String.Index>

How can I convert NSRange to Range<String.Index> in Swift?如何在 Swift NSRange转换为Range<String.Index>

I want to use the following UITextFieldDelegate method:我想使用以下UITextFieldDelegate方法:

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

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

在此处输入图片说明

As of Swift 4 (Xcode 9), the Swift standard library provides methods to convert between Swift string ranges ( Range<String.Index> ) and NSString ranges ( NSRange ).Swift 4 (Xcode 9) 开始,Swift 标准库提供了在 Swift 字符串范围 ( Range<String.Index> ) 和NSString范围 ( NSRange ) 之间进行转换的方法。 Example:例子:

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

Therefore the text replacement in the text field delegate method can now be done as因此,文本字段委托方法中的文本替换现在可以作为

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

(Older answers for Swift 3 and earlier:) (Swift 3 及更早版本的旧答案:)

As of Swift 1.2, String.Index has an initializer从 Swift 1.2 开始, String.Index有一个初始值设定项

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

which can be used to convert NSRange to Range<String.Index> correctly (including all cases of Emojis, Regional Indicators or other extended grapheme clusters) without intermediate conversion to an NSString :可用于将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
    }
}

This method returns an optional string range because not all NSRange s are valid for a given Swift string.此方法返回一个可选的字符串范围,因为并非所有NSRange都对给定的 Swift 字符串有效。

The UITextFieldDelegate delegate method can then be written as 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
}

The inverse conversion is逆转换是

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

A simple test:一个简单的测试:

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

Update for Swift 2: Swift 2 更新:

The Swift 2 version of rangeFromNSRange() was already given by Serhii Yakovenko in this answer , I am including it here for completeness: 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
    }
}

The Swift 2 version of NSRangeFromRange() is 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))
    }
}

Update for Swift 3 (Xcode 8): 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
    }
}

Example:例子:

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

The NSString version (as opposed to Swift String) of replacingCharacters(in: NSRange, with: NSString) accepts an NSRange , so one simple solution is to convert String to NSString first . NSString版本(与 Swift String 相对)的replacingCharacters(in: NSRange, with: NSString)接受NSRange ,因此一个简单的解决方案是先将String转换为NSString The delegate and replacement method names are slightly different in Swift 3 and 2, so depending on which Swift you're using: Swift 3 和 2 中的委托和替换方法名称略有不同,因此取决于您使用的 Swift:

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

This answer by Martin R seems to be correct because it accounts for Unicode. Martin R 的这个答案似乎是正确的,因为它考虑了 Unicode。

However at the time of the post (Swift 1) his code doesn't compile in Swift 2.0 (Xcode 7), because they removed advance() function.然而,在发布时(Swift 1),他的代码无法在 Swift 2.0(Xcode 7)中编译,因为他们删除了advance()函数。 Updated version is below:更新版本如下:

Swift 2斯威夫特 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 3斯威夫特 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
    }
}

Swift 4斯威夫特 4

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

You need to use Range<String.Index> instead of the classic NSRange .您需要使用Range<String.Index>而不是经典的NSRange The way I do it (maybe there is a better way) is by taking the string's String.Index a moving it with advance .我做的方式(也许有更好的方法)是通过利用字符串的String.Index一个与移动它advance

I don't know what range you are trying to replace, but let's pretend you want to replace the first 2 characters.我不知道您要替换的范围是什么,但让我们假设您要替换前 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)

This is similar to Emilie's answer however since you asked specifically how to convert the NSRange to Range<String.Index> you would do something like this:这与 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) 
     ...

}

A riff on the great answer by @Emilie, not a replacement/competing answer.对@Emilie 的精彩答案的即兴演奏,而不是替代/竞争答案。
(Xcode6-Beta5) (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)")

Output:输出:

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

Notice the indexes account for multiple code units.请注意索引占多个代码单元。 The flag (REGIONAL INDICATOR SYMBOL LETTERS ES) is 8 bytes and the (FACE WITH TEARS OF JOY) is 4 bytes.标志(区域指示符符号字母 ES)是 8 个字节,(FACE WITH TEARS OF JOY)是 4 个字节。 (In this particular case it turns out that the number of bytes is the same for UTF-8, UTF-16 and UTF-32 representations.) (在这种特殊情况下,UTF-8、UTF-16 和 UTF-32 表示的字节数是相同的。)

Wrapping it in a 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)")

Output:输出:

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)

 }

In Swift 2.0 assuming func textField(textField: UITextField, shouldChangeCharactersInRange range: NSRange, replacementString string: String) -> Bool { :在 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)

Here's my best effort.这是我最大的努力。 But this cannot check or detect wrong input argument.但这不能检查或检测错误的输入参数。

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.

Result.结果。

😂
🇪🇸
🇪🇸
🇪🇸

Details细节

NSRange from NSString counts UTF-16 code-unit s. NSString NSRange计算 UTF-16代码单元 And Range<String.Index> from Swift String is an opaque relative type which provides only equality and navigation operations.而 Swift String中的Range<String.Index>是一个不透明的相对类型,它只提供相等和导航操作。 This is intentionally hidden design.这是故意隐藏的设计。

Though the Range<String.Index> seem to be mapped to UTF-16 code-unit offset, that is just an implementation detail, and I couldn't find any mention about any guarantee.尽管Range<String.Index>似乎映射到 UTF-16 代码单元偏移量,但这只是一个实现细节,我找不到任何关于任何保证的提及。 That means the implementation details can be changed at any time.这意味着可以随时更改实施细节。 Internal representation of Swift String is not pretty defined, and I cannot rely on it. Swift String内部表示不是很明确,我不能依赖它。

NSRange values can be directly mapped to String.UTF16View indexes. NSRange值可以直接映射到String.UTF16View索引。 But there's no method to convert it into String.Index .但是没有将其转换为String.Index

Swift String.Index is index to iterate Swift Character which is an Unicode grapheme cluster . Swift String.Index是迭代 Swift Character索引,Swift Character是一个Unicode 字素簇 Then, you must provide proper NSRange which selects correct grapheme clusters.然后,您必须提供合适的NSRange来选择正确的字素簇。 If you provide wrong range like the above example, it will produce wrong result because proper grapheme cluster range couldn't be figured out.如果像上面的例子一样提供错误的范围,它会产生错误的结果,因为无法计算出正确的字素簇范围。

If there's a guarantee that the String.Index is UTF-16 code-unit offset, then problem becomes simple.如果保证String.IndexUTF-16 代码单元偏移量,那么问题就变得简单了。 But it is unlikely to happen.但这不太可能发生。

Inverse conversion逆转换

Anyway the inverse conversion can be done precisely.无论如何,可以精确地完成逆转换。

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

Result.结果。

(0,6)
(4,2)

I've found the cleanest swift2 only solution is to create a category on NSRange:我发现最干净的 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)
    }
}

And then call it from for text field delegate function:然后从 for 文本字段委托函数调用它:

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
}
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 3.0 beta 官方文档在UTF16View Elements Match NSString Characters 标题中的标题String.UTF16View下提供了针对这种情况的标准解决方案

In the accepted answer I find the optionals cumbersome.在接受的答案中,我发现选项很麻烦。 This works with Swift 3 and seems to have no problem with emojis.这适用于 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
}

Because NSRange , when used in NSString operations, represents positions of the UTF-16 units.因为NSRangeNSString操作中使用时,表示 UTF-16 单元的位置。 Then the shortest way to convert to String.Index is to initialise via String.Index(utf16Offset: Int, in: StringProtocol) initialiser.然后转换为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)

Example:例子:

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])")

Output:输出:

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

Swift 5 Solution Swift 5 解决方案

Short answer with main extension带有主要扩展名的简短回答

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

For detailed answer check here有关详细答案,请查看此处

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM