NSRange dari Swift Range?

176

Masalah: NSAttributedString mengambil NSRange saat saya menggunakan Swift String yang menggunakan Range

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

Menghasilkan kesalahan berikut:

galat: 'Rentang' tidak dapat dikonversi menjadi 'NSRange' yang dikaitkanString.addAttribute (NSForegroundColorAttributeName, nilai: NSColor.redColor (), rentang: substringRange)

Jay
sumber
4
Kemungkinan duplikat NSRange ke Range <String.Index>
Suhaib
2
@ Suhaib yang terjadi sebaliknya.
geoff

Jawaban:

262

Swift Stringrentang dan NSStringrentang tidak "kompatibel". Misalnya, emoji seperti πŸ˜„ dihitung sebagai satu karakter Swift, tetapi sebagai dua NSString karakter (pasangan pengganti UTF-16).

Karena itu solusi yang Anda sarankan akan menghasilkan hasil yang tidak terduga jika string berisi karakter tersebut. Contoh:

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)

Keluaran:

Parag Paragraf panjang {
} ph katakan {
    NSColor = "NSCalibratedRGBColorSpace 1 0 0 1";
} ing! {
}

Seperti yang Anda lihat, "ph say" telah ditandai dengan atribut, bukan "mengatakan".

Karena NS(Mutable)AttributedStringpada akhirnya membutuhkan NSStringdan NSRange, sebenarnya lebih baik untuk mengubah string yang diberikan NSStringterlebih dahulu. Maka itu substringRange adalah NSRangedan Anda tidak perlu mengonversi rentang lagi:

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)

Keluaran:

Paragraph Paragraf panjang {
}pepatah{
    NSColor = "NSCalibratedRGBColorSpace 1 0 0 1";
}! {
}

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

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

Pembaruan untuk Swift 4:

Pada Swift 4 (Xcode 9), perpustakaan standar Swift menyediakan metode untuk mengkonversi antara Range<String.Index>dan NSRange. Konversi ke NSStringtidak lagi diperlukan:

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)

Berikut substringRangeini adalah Range<String.Index>, dan yang dikonversi ke sesuai NSRangedengan

NSRange(substringRange, in: text)
Martin R
sumber
74
Bagi siapa pun yang ingin mengetikkan karakter emoji di OSX - Control-Command-space bar menampilkan pemilih karakter
Jay
2
Ini tidak berfungsi jika saya mencocokkan lebih dari satu kata, dan saya tidak yakin apa keseluruhan string yang cocok. Katakanlah saya mendapatkan string kembali dari API dan menggunakannya dalam string lain, dan saya ingin string dari API digarisbawahi, saya tidak dapat menjamin bahwa substring tidak akan berada di string baik dari API dan yang lain tali! Ada ide?
simonthumper
NSMakeRange Berubah str.substringWithRange (Rentang <String.Index> (mulai: str.startIndex, end: str.endIndex)) // "Halo, bermain" ini perubahannya
HariKrishnan.P
(or) casting the string --- let substring = (string as NSString) .substringWithRange (NSMakeRange (start, length))
HariKrishnan.P
2
Anda menyebutkan itu Range<String.Index>dan NSStringtidak kompatibel. Apakah mitra mereka juga tidak kompatibel? Yaitu NSRangedan Stringtidak kompatibel? Karena salah satu API Apple secara khusus menggabungkan dua: kecocokan (dalam: opsi: kisaran :)
Senseful
57

Untuk kasus-kasus seperti yang Anda gambarkan, saya menemukan ini berfungsi. Ini relatif pendek dan manis:

 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)
royherma
sumber
11
attributedString.addAttribute tidak akan berfungsi dengan Range cepat
Paludis
7
@Paludis, Anda benar tetapi solusi ini tidak berusaha menggunakan rentang Swift. Itu menggunakan NSRange. stradalah NSStringdan karenanya str.RangeOfString()mengembalikan sebuah NSRange.
tjpaul
3
Anda juga dapat menghapus string duplikat di baris 2 dengan mengganti baris 2 dan 3 dengan:let str = attributedString.string as NSString
Jason Moore
2
Ini adalah mimpi buruk lokalisasi.
Sulthan
29

Jawabannya baik-baik saja, tetapi dengan Swift 4 Anda dapat menyederhanakan kode Anda sedikit:

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

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

Berhati-hatilah, karena rangefungsi harus dibuka.

George Maisuradze
sumber
10

Kemungkinan Solusi

Swift menyediakan jarak () yang mengukur jarak antara mulai dan akhir yang dapat digunakan untuk membuat 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)
        }
})
Jay
sumber
2
Catatan: Ini bisa pecah jika menggunakan karakter seperti emoji di string - Lihat respons Martin.
Jay
7

Bagi saya ini berfungsi dengan baik:

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
Breno VinΓ­cios
sumber
5

Swift 4:

Tentu, saya tahu Swift 4 sudah memiliki ekstensi untuk NSRange

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

Saya tahu dalam banyak kasus init ini sudah cukup. Lihat penggunaannya:

let string = "Many animals here: πŸΆπŸ¦‡πŸ± !!!"

if let range = string.range(of: "πŸΆπŸ¦‡πŸ±"){
     print((string as NSString).substring(with: NSRange(range, in: string))) //  "πŸΆπŸ¦‡πŸ±"
 }

Tetapi konversi dapat dilakukan langsung dari Range <String.Index> ke NSRange tanpa instance String Swift.

Alih-alih penggunaan init umum yang mengharuskan dari Anda parameter target sebagai String dan jika Anda tidak memiliki string target , Anda dapat membuat konversi secara langsung

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

atau Anda dapat membuat ekstensi khusus untuk Range itu sendiri

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

Pemakaian:

let string = "Many animals here: πŸΆπŸ¦‡πŸ± !!!"
if let range = string.range(of: "πŸΆπŸ¦‡πŸ±"){
    print((string as NSString).substring(with: NSRange(range))) //  "πŸΆπŸ¦‡πŸ±"
}

atau

if let nsrange = string.range(of: "πŸΆπŸ¦‡πŸ±")?.nsRange{
    print((string as NSString).substring(with: nsrange)) //  "πŸΆπŸ¦‡πŸ±"
}

Swift 5:

Karena migrasi string Swift ke pengkodean UTF-8 secara default, penggunaan encodedOffsetdianggap usang dan Rentang tidak dapat dikonversi ke NSRange tanpa instance dari String itu sendiri, karena untuk menghitung offset kita perlu string sumber yang dikodekan dalam UTF-8 dan harus dikonversi ke UTF-16 sebelum menghitung offset. Jadi pendekatan terbaik, untuk saat ini, adalah dengan menggunakan init generik .

Dmitry A.
sumber
Penggunaan encodedOffsetyang dianggap berbahaya dan akan ditinggalkan .
Martin R
3

Cepat 4

Saya pikir, ada dua cara.

1. NSRange (rentang, dalam:)

2. NSRange (lokasi :, panjang:)

Kode sampel:

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

Tangkapan Layar: masukkan deskripsi gambar di sini

Sarang
sumber
Penggunaan encodedOffsetyang dianggap berbahaya dan akan ditinggalkan .
Martin R
1

Swift 3 Extension Variant yang mempertahankan atribut yang ada.

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
  }
}
jriskin
sumber
0
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
}
orkoden
sumber
0

Saya suka bahasa Swift, tetapi menggunakan NSAttributedStringdengan Swift Rangeyang tidak kompatibel dengan NSRangetelah membuat kepala saya sakit terlalu lama. Jadi untuk mengatasi semua sampah itu saya merancang metode berikut untuk mengembalikan sebuah NSMutableAttributedStringdengan kata-kata yang disorot diatur dengan warna Anda.

Ini tidak berfungsi untuk emoji. Ubah jika Anda harus.

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

Pemakaian:

// 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
Brandon A
sumber
-3
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)
jonas
sumber
6
Bagaimana dengan menjelaskan jawaban Anda sedikit, dan lebih baik memformat kode dengan benar?
SamB