简体   繁体   English

来自 Swift Range 的 NSRange?

[英]NSRange from Swift Range?

Problem: NSAttributedString takes an NSRange while I'm using a Swift String that uses Range问题:当我使用使用 Range 的 Swift String 时,NSAttributedString 使用 NSRange

let text = "Long paragraph saying something goes here!"
let textRange = text.startIndex..<text.endIndex
let attributedString = NSMutableAttributedString(string: text)

text.enumerateSubstringsInRange(textRange, options: NSStringEnumerationOptions.ByWords, { (substring, substringRange, enclosingRange, stop) -> () in

    if (substring == "saying") {
        attributedString.addAttribute(NSForegroundColorAttributeName, value: NSColor.redColor(), range: substringRange)
    }
})

Produces the following error:产生以下错误:

error: 'Range' is not convertible to 'NSRange' attributedString.addAttribute(NSForegroundColorAttributeName, value: NSColor.redColor(), range: substringRange)错误:'Range' 不能转换为 'NSRange'attributedString.addAttribute(NSForegroundColorAttributeName, value: NSColor.redColor(), range: substringRange)

Swift String ranges and NSString ranges are not "compatible". Swift String范围和NSString范围不“兼容”。 For example, an emoji like 😄 counts as one Swift character, but as two NSString characters (a so-called UTF-16 surrogate pair).例如,像 😄 这样的表情符号算作一个 Swift 字符,但算作两个NSString字符(所谓的 UTF-16 代理对)。

Therefore your suggested solution will produce unexpected results if the string contains such characters.因此,如果字符串包含此类字符,您建议的解决方案将产生意外结果。 Example:例子:

let text = "😄😄😄Long paragraph saying!"
let textRange = text.startIndex..<text.endIndex
let attributedString = NSMutableAttributedString(string: text)

text.enumerateSubstringsInRange(textRange, options: NSStringEnumerationOptions.ByWords, { (substring, substringRange, enclosingRange, stop) -> () in
    let start = distance(text.startIndex, substringRange.startIndex)
    let length = distance(substringRange.startIndex, substringRange.endIndex)
    let range = NSMakeRange(start, length)

    if (substring == "saying") {
        attributedString.addAttribute(NSForegroundColorAttributeName, value: NSColor.redColor(), range: range)
    }
})
println(attributedString)

Output:输出:

😄😄😄Long paragra{
}ph say{
    NSColor = "NSCalibratedRGBColorSpace 1 0 0 1";
}ing!{
}

As you see, "ph say" has been marked with the attribute, not "saying".如您所见,“ph say”已被标记为属性,而不是“saying”。

Since NS(Mutable)AttributedString ultimately requires an NSString and an NSRange , it is actually better to convert the given string to NSString first.由于NS(Mutable)AttributedString最终需要一个NSString和一个NSRange ,实际上最好先将给定的字符串转换为NSString Then the substringRange is an NSRange and you don't have to convert the ranges anymore:然后substringRangeNSRange并且您不必再转换范围:

let text = "😄😄😄Long paragraph saying!"
let nsText = text as NSString
let textRange = NSMakeRange(0, nsText.length)
let attributedString = NSMutableAttributedString(string: nsText)

nsText.enumerateSubstringsInRange(textRange, options: NSStringEnumerationOptions.ByWords, { (substring, substringRange, enclosingRange, stop) -> () in

    if (substring == "saying") {
        attributedString.addAttribute(NSForegroundColorAttributeName, value: NSColor.redColor(), range: substringRange)
    }
})
println(attributedString)

Output:输出:

😄😄😄Long paragraph {
}saying{
    NSColor = "NSCalibratedRGBColorSpace 1 0 0 1";
}!{
}

Update for Swift 2: Swift 2 更新:

let text = "😄😄😄Long paragraph saying!"
let nsText = text as NSString
let textRange = NSMakeRange(0, nsText.length)
let attributedString = NSMutableAttributedString(string: text)

nsText.enumerateSubstringsInRange(textRange, options: .ByWords, usingBlock: {
    (substring, substringRange, _, _) in

    if (substring == "saying") {
        attributedString.addAttribute(NSForegroundColorAttributeName, value: NSColor.redColor(), range: substringRange)
    }
})
print(attributedString)

Update for Swift 3: Swift 3 更新:

let text = "😄😄😄Long paragraph saying!"
let nsText = text as NSString
let textRange = NSMakeRange(0, nsText.length)
let attributedString = NSMutableAttributedString(string: text)

nsText.enumerateSubstrings(in: textRange, options: .byWords, using: {
    (substring, substringRange, _, _) in

    if (substring == "saying") {
        attributedString.addAttribute(NSForegroundColorAttributeName, value: NSColor.red, range: substringRange)
    }
})
print(attributedString)

Update for Swift 4: Swift 4 更新:

As of Swift 4 (Xcode 9), the Swift standard library provides method to convert between Range<String.Index> and NSRange .从 Swift 4 (Xcode 9) 开始,Swift 标准库提供了在Range<String.Index>NSRange之间进行转换的方法。 Converting to NSString is no longer necessary:不再需要转换为NSString

let text = "😄😄😄Long paragraph saying!"
let attributedString = NSMutableAttributedString(string: text)

text.enumerateSubstrings(in: text.startIndex..<text.endIndex, options: .byWords) {
    (substring, substringRange, _, _) in
    if substring == "saying" {
        attributedString.addAttribute(.foregroundColor, value: NSColor.red,
                                      range: NSRange(substringRange, in: text))
    }
}
print(attributedString)

Here substringRange is a Range<String.Index> , and that is converted to the corresponding NSRange with这里substringRange是一个Range<String.Index> ,它被转换为相应的NSRange

NSRange(substringRange, in: text)

For cases like the one you described, I found this to work.对于您所描述的情况,我发现这是可行的。 It's relatively short and sweet:它相对简短而甜蜜:

 let attributedString = NSMutableAttributedString(string: "follow the yellow brick road") //can essentially come from a textField.text as well (will need to unwrap though)
 let text = "follow the yellow brick road"
 let str = NSString(string: text) 
 let theRange = str.rangeOfString("yellow")
 attributedString.addAttribute(NSForegroundColorAttributeName, value: UIColor.yellowColor(), range: theRange)

The answers are fine, but with Swift 4 you could simplify your code a bit:答案很好,但是使用 Swift 4,您可以稍微简化一下代码:

let text = "Test string"
let substring = "string"

let substringRange = text.range(of: substring)!
let nsRange = NSRange(substringRange, in: text)

Be cautious, as the result of range function has to be unwrapped.小心,因为必须解开range函数的结果。

Possible Solution可能的解决方案

Swift provides distance() which measures the distance between start and end that can be used to create an NSRange: Swift 提供了 distance() 来测量起点和终点之间的距离,可用于创建 NSRange:

let text = "Long paragraph saying something goes here!"
let textRange = text.startIndex..<text.endIndex
let attributedString = NSMutableAttributedString(string: text)

text.enumerateSubstringsInRange(textRange, options: NSStringEnumerationOptions.ByWords, { (substring, substringRange, enclosingRange, stop) -> () in
    let start = distance(text.startIndex, substringRange.startIndex)
    let length = distance(substringRange.startIndex, substringRange.endIndex)
    let range = NSMakeRange(start, length)

//    println("word: \(substring) - \(d1) to \(d2)")

        if (substring == "saying") {
            attributedString.addAttribute(NSForegroundColorAttributeName, value: NSColor.redColor(), range: range)
        }
})

For me this works perfectly:对我来说,这非常有效:

let font = UIFont.systemFont(ofSize: 12, weight: .medium)
let text = "text"
let attString = NSMutableAttributedString(string: "exemple text :)")

attString.addAttributes([.font: font], range:(attString.string as NSString).range(of: text))

label.attributedText = attString

Swift 4:斯威夫特 4:

Sure, I know that Swift 4 has an extension for NSRange already当然,我知道 Swift 4 已经有了 NSRange 的扩展

public init<R, S>(_ region: R, in target: S) where R : RangeExpression,
    S : StringProtocol, 
    R.Bound == String.Index, S.Index == String.Index

I know in most cases this init is enough.我知道在大多数情况下这个 init 就足够了。 See its usage:查看它的用法:

let string = "Many animals here: 🐶🦇🐱 !!!"

if let range = string.range(of: "🐶🦇🐱"){
     print((string as NSString).substring(with: NSRange(range, in: string))) //  "🐶🦇🐱"
 }

But conversion can be done directly from Range< String.Index > to NSRange without Swift's String instance.但是可以直接从 Range<String.Index> 转换为 NSRange,无需 Swift 的 String 实例。

Instead of generic init usage which requires from you the target parameter as String and if you don't have target string at hand you can create conversion directly而不是通用init用法,它需要您将目标参数作为字符串,如果您手头没有目标字符串,您可以直接创建转换

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

or you can create the specialized extension for Range itself或者您可以为 Range 本身创建专门的扩展

extension Range where Bound == String.Index {
    var nsRange:NSRange {
    return NSRange(location: self.lowerBound.encodedOffset,
                     length: self.upperBound.encodedOffset -
                             self.lowerBound.encodedOffset)
    }
}

Usage:用法:

let string = "Many animals here: 🐶🦇🐱 !!!"
if let range = string.range(of: "🐶🦇🐱"){
    print((string as NSString).substring(with: NSRange(range))) //  "🐶🦇🐱"
}

or要么

if let nsrange = string.range(of: "🐶🦇🐱")?.nsRange{
    print((string as NSString).substring(with: nsrange)) //  "🐶🦇🐱"
}

Swift 5:斯威夫特 5:

Due to the migration of Swift strings to UTF-8 encoding by default, the usage of encodedOffset is considered as deprecated and Range cannot be converted to NSRange without an instance of String itself, because in order to calculate the offset we need the source string which is encoded in UTF-8 and it should be converted to UTF-16 before calculating offset.由于 Swift 字符串默认迁移到 UTF-8 编码,因此使用encodedOffset被认为是不推荐使用的,并且 Range 在没有 String 本身的实例的情况下无法转换为 NSRange,因为为了计算偏移量我们需要源字符串以 UTF-8 编码,在计算偏移量之前应将其转换为 UTF-16。 So best approach, for now, is to use generic init .所以目前最好的方法是使用通用的init

Swift 4斯威夫特 4

I think, there are two ways.我想,有两种方法。

1. NSRange(range, in: ) 1. NSRange(范围,在:)

2. NSRange(location:, length: ) 2. NSRange(位置:,长度:)

Sample code:示例代码:

let attributedString = NSMutableAttributedString(string: "Sample Text 12345", attributes: [.font : UIFont.systemFont(ofSize: 15.0)])

// NSRange(range, in: )
if let range = attributedString.string.range(of: "Sample")  {
    attributedString.addAttribute(.foregroundColor, value: UIColor.orange, range: NSRange(range, in: attributedString.string))
}

// NSRange(location: , length: )
if let range = attributedString.string.range(of: "12345") {
    attributedString.addAttribute(.foregroundColor, value: UIColor.green, range: NSRange(location: range.lowerBound.encodedOffset, length: range.upperBound.encodedOffset - range.lowerBound.encodedOffset))
}

Screen Shot:截屏: 在此处输入图片说明

Swift 3 Extension Variant that preserves existing attributes.保留现有属性的Swift 3 扩展变体

extension UILabel {
  func setLineHeight(lineHeight: CGFloat) {
    guard self.text != nil && self.attributedText != nil else { return }
    var attributedString = NSMutableAttributedString()

    if let attributedText = self.attributedText {
      attributedString = NSMutableAttributedString(attributedString: attributedText)
    } else if let text = self.text {
      attributedString = NSMutableAttributedString(string: text)
    }

    let style = NSMutableParagraphStyle()
    style.lineSpacing = lineHeight
    style.alignment = self.textAlignment
    let str = NSString(string: attributedString.string)

    attributedString.addAttribute(NSParagraphStyleAttributeName,
                                  value: style,
                                  range: str.range(of: str as String))
    self.attributedText = attributedString
  }
}
func formatAttributedStringWithHighlights(text: String, highlightedSubString: String?, formattingAttributes: [String: AnyObject]) -> NSAttributedString {
    let mutableString = NSMutableAttributedString(string: text)

    let text = text as NSString         // convert to NSString be we need NSRange
    if let highlightedSubString = highlightedSubString {
        let highlightedSubStringRange = text.rangeOfString(highlightedSubString) // find first occurence
        if highlightedSubStringRange.length > 0 {       // check for not found
            mutableString.setAttributes(formattingAttributes, range: highlightedSubStringRange)
        }
    }

    return mutableString
}

I love the Swift language, but using NSAttributedString with a Swift Range that is not compatible with NSRange has made my head hurt for too long.我喜欢 Swift 语言,但是将NSAttributedString与与NSRange不兼容的 Swift Range一起NSRange让我的NSRange了太久。 So to get around all that garbage I devised the following methods to return an NSMutableAttributedString with the highlighted words set with your color.因此,为了解决所有这些垃圾,我设计了以下方法来返回一个NSMutableAttributedString ,其中突出显示的单词设置为您的颜色。

This does not work for emojis.并不适用于表情符号工作。 Modify if you must.如果必须,请修改。

extension String {
    func getRanges(of string: String) -> [NSRange] {
        var ranges:[NSRange] = []
        if contains(string) {
            let words = self.components(separatedBy: " ")
            var position:Int = 0
            for word in words {
                if word.lowercased() == string.lowercased() {
                    let startIndex = position
                    let endIndex = word.characters.count
                    let range = NSMakeRange(startIndex, endIndex)
                    ranges.append(range)
                }
                position += (word.characters.count + 1) // +1 for space
            }
        }
        return ranges
    }
    func highlight(_ words: [String], this color: UIColor) -> NSMutableAttributedString {
        let attributedString = NSMutableAttributedString(string: self)
        for word in words {
            let ranges = getRanges(of: word)
            for range in ranges {
                attributedString.addAttributes([NSForegroundColorAttributeName: color], range: range)
            }
        }
        return attributedString
    }
}

Usage:用法:

// The strings you're interested in
let string = "The dog ran after the cat"
let words = ["the", "ran"]

// Highlight words and get back attributed string
let attributedString = string.highlight(words, this: .yellow)

// Set attributed string
label.attributedText = attributedString

My solution is a string extension that first gets the swift range then get's the distance from the start of the string to the start and end of the substring.我的解决方案是一个字符串扩展,它首先获取 swift 范围,然后获取从字符串开头到子字符串开头和结尾的距离。

These values are then used to calculate the start and length of the substring.然后使用这些值来计算子字符串的开始和长度。 We can then apply these values to the NSMakeRange constructor.然后我们可以将这些值应用到 NSMakeRange 构造函数。

This solution works with substrings that consist of multiple words, which a lot of the solutions here using enumerateSubstrings let me down on.此解决方案适用于由多个单词组成的子字符串,这里使用 enumerateSubstrings 的许多解决方案让我失望。

extension String {

    func NSRange(of substring: String) -> NSRange? {
        // Get the swift range 
        guard let range = range(of: substring) else { return nil }

        // Get the distance to the start of the substring
        let start = distance(from: startIndex, to: range.lowerBound) as Int
        //Get the distance to the end of the substring
        let end = distance(from: startIndex, to: range.upperBound) as Int

        //length = endOfSubstring - startOfSubstring
        //start = startOfSubstring
        return NSMakeRange(start, end - start)
    }

}

Swift 5 Solution Swift 5 解决方案

Converting Range into NSRange将范围转换为 NSRange

As the 'encodedOffset' is deprecated, so now in order to convert String.Index to Int we need the reference of original string from which Range<String.Index> was derived.由于不推荐使用'encodedOffset' ,所以现在为了将String.Index转换为Int,我们需要从其派生Range<String.Index>的原始字符串的引用。

A convenient detailed extension for NSRange could be as below: NSRange 的一个方便的详细扩展如下:

extension NSRange {

    public init(range: Range<String.Index>, 
                originalText: String) {

        let range_LowerBound_INDEX = range.lowerBound
        let range_UpperBound_INDEX = range.upperBound

        let range_LowerBound_INT = range_LowerBound_INDEX.utf16Offset(in: originalText)
        let range_UpperBound_INT = range_UpperBound_INDEX.utf16Offset(in: originalText)

        let locationTemp = range_LowerBound_INT
        let lengthTemp = range_UpperBound_INT - range_LowerBound_INT

        self.init(location: locationTemp,
                  length: lengthTemp)
    }
}

While the shorthand extension is as below虽然简写扩展如下

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

Now we can use any Range to convert it into NSRange as below, sharing my own requirement which led me to write above extensions现在我们可以使用任何 Range 将其转换为 NSRange,如下所示,分享我自己的需求,这导致我编写了上述扩展

I was using below String extension for finding all the ranges of specific word from the String我使用下面的字符串扩展来查找字符串中特定单词的所有范围

extension String {
        
    func ranges(of substring: String, options: CompareOptions = [], locale: Locale? = nil) -> [Range<Index>] {
        var ranges: [Range<Index>] = []
        while let range = range(of: substring, options: options, range: (ranges.last?.upperBound ?? self.startIndex)..<self.endIndex, locale: locale) {
            ranges.append(range)
        }
        return ranges
    }
}

My requirement was to change the colour of specific words in a String, so for that I wrote this extension which does the job我的要求是更改字符串中特定单词的颜色,因此我编写了这个扩展程序来完成这项工作

extension NSAttributedString {

    static func colored(originalText:String,
                        wordToColor:String,
                        currentColor:UIColor,
                        differentColor:UIColor) -> NSAttributedString {
        
        let attr = NSMutableAttributedString(string: originalText)
        
        attr.beginEditing()
        
        attr.addAttribute(NSAttributedString.Key.foregroundColor,
                          value: currentColor,
                          range: NSRange(location: 0, length: originalText.count))
        
        // FOR COVERING ALL THE OCCURENCES
        for eachRange in originalText.ranges(of: wordToColor) {
            attr.addAttribute(NSAttributedString.Key.foregroundColor,
                              value: differentColor,
                              range: NSRange(range: eachRange, originalText: originalText))
        }
        
        attr.endEditing()
        
        return attr
    }

}

Finally I was using it from my main code as below最后我从我的主要代码中使用它,如下所示

let text = "Collected".localized() + "  +  " + "Cancelled".localized() + "  +  " + "Pending".localized()
myLabel.attributedText = NSAttributedString.colored(originalText: text,
                                                    wordToColor: "+",
                                                    currentColor: UIColor.purple,
                                                    differentColor: UIColor.blue)

And the result is as below, having the colour of + sign changed as blue from the main text colour which is purple.结果如下,+号的颜色从紫色的正文颜色变为蓝色。

在此处输入图片说明

Hope this helps someone in need.希望这可以帮助有需要的人。 Thanks!谢谢!

let text:String = "Hello Friend"

let searchRange:NSRange = NSRange(location:0,length: text.characters.count)

let range:Range`<Int`> = Range`<Int`>.init(start: searchRange.location, end: searchRange.length)

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

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