NSRange ke Range <String.Index>

258

Bagaimana saya bisa mengonversi NSRangeke Range<String.Index>dalam Swift?

Saya ingin menggunakan UITextFieldDelegatemetode berikut :

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

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

masukkan deskripsi gambar di sini

JΓ‘nos
sumber
71
Terima kasih, Apple, karena memberi kami kelas Range baru, tetapi kemudian tidak memperbarui salah satu dari kelas utilitas string, seperti NSRegularExpression, untuk menggunakannya!
puzzl

Jawaban:

269

The NSStringVersi (sebagai lawan Swift String) dari replacingCharacters(in: NSRange, with: NSString)menerima sebuah NSRange, jadi salah satu solusi sederhana adalah dengan mengkonversi Stringke NSStringpertama . Nama metode delegasi dan penggantian sedikit berbeda dalam Swift 3 dan 2, jadi tergantung pada Swift mana yang Anda gunakan:

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)
}
Alex Pretzlav
sumber
14
Ini tidak akan berfungsi jika textFieldberisi karakter unicode unit multi-kode unit seperti emoji. Karena ini adalah bidang entri, pengguna dapat memasukkan emoji (πŸ˜‚), halaman 1 karakter karakter multi-kode unit lainnya seperti flag (πŸ‡ͺπŸ‡Έ).
zaph
2
@Zaph: karena rentang berasal dari UITextField, yang ditulis dalam Obj-C terhadap NSString, saya menduga bahwa hanya rentang yang valid berdasarkan karakter unicode dalam string yang akan disediakan oleh delegasi panggilan balik, dan jadi ini aman untuk digunakan , tapi saya belum mengujinya secara pribadi.
Alex Pretzlav
2
@Zaph, itu benar, poin saya adalah bahwa UITextFieldDelegate's textField:shouldChangeCharactersInRange:replacementString:metode tidak akan menyediakan berbagai yang dimulai atau berakhir dalam karakter unicode, karena callback didasarkan pada pengguna memasukkan karakter dari keyboard, dan itu tidak mungkin untuk mengetik hanya sebagian dari karakter unicode emoji. Perhatikan bahwa jika delegasi ditulis dalam Obj-C, itu akan memiliki masalah yang sama.
Alex Pretzlav
4
Bukan jawaban terbaik. Paling mudah membaca kode tetapi @ martin-r memiliki jawaban yang benar.
Rog
3
Sebenarnya kalian harus melakukan ini(textField.text as NSString?)?.stringByReplacingCharactersInRange(range, withString: string)
Wanbok Choi
361

Pada Swift 4 (Xcode 9), perpustakaan standar Swift menyediakan metode untuk mengkonversi antara rentang string Swift ( Range<String.Index>) dan NSStringrentang ( NSRange). Contoh:

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]) // πŸ‡©πŸ‡ͺ

Oleh karena itu penggantian teks dalam metode delegasi bidang teks sekarang dapat dilakukan sebagai

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

(Jawaban lama untuk Swift 3 dan sebelumnya :)

Pada Swift 1.2, String.Indexmemiliki penginisialisasi

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

yang dapat digunakan untuk mengkonversi NSRangedengan Range<String.Index>benar (termasuk semua kasus Emoji, Indikator Regional atau cluster grapheme diperluas) tanpa konversi antara ke 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
    }
}

Metode ini mengembalikan rentang string opsional karena tidak semua NSRanges valid untuk string Swift yang diberikan.

The UITextFieldDelegatemetode delegasi kemudian dapat ditulis sebagai

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
}

Konversi terbalik adalah

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

Tes sederhana:

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)) // πŸ‡©πŸ‡ͺ

Pembaruan untuk Swift 2:

Versi Swift 2 rangeFromNSRange()sudah diberikan oleh Serhii Yakovenko dalam jawaban ini , saya memasukkannya di sini untuk kelengkapan:

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

Versi Swift 2 NSRangeFromRange()adalah

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

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

Contoh:

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)) // πŸ‡©πŸ‡ͺ
Martin R
sumber
7
Kode itu tidak berfungsi dengan benar untuk karakter yang terdiri dari lebih dari satu titik kode UTF-16, misalnya Emoji. - Sebagai contoh, jika bidang teks berisi teks "πŸ‘Ώ" dan Anda menghapus karakter itu, maka rangeitu (0,2)karena NSRangemerujuk ke karakter UTF-16 dalam sebuah NSString. Tapi itu dianggap sebagai satu karakter Unicode dalam string Swift. - let end = advance(start, range.length)dari jawaban yang direferensikan tidak crash dalam hal ini dengan pesan kesalahan "kesalahan fatal: tidak dapat meningkatkan endIndex".
Martin R
8
@MattDiPasquale: Tentu. Tujuan saya adalah untuk menjawab pertanyaan kata demi kata "Bagaimana saya bisa mengubah NSRange menjadi Range <String.Index> di Swift" dengan cara yang aman untuk Unicode (berharap bahwa seseorang mungkin menganggapnya berguna, yang tidak berlaku sampai sekarang :(
Martin R
5
Terima kasih, sekali lagi, untuk melakukan pekerjaan Apple untuk mereka. Tanpa orang-orang seperti Anda, dan Stack Overflow, tidak mungkin saya melakukan pengembangan iOS / OS X. Hidup ini terlalu singkat.
Womble
1
@ jiminybob99: Klik perintah Stringuntuk melompat ke referensi API, baca semua metode dan komentar, lalu coba hal-hal yang berbeda sampai berhasil :)
Martin R
1
Karena let from16 = utf16.index(utf16.startIndex, offsetBy: nsRange.location, limitedBy: utf16.endIndex), saya pikir Anda benar-benar ingin membatasi utf16.endIndex - 1. Jika tidak, Anda dapat memulai dari akhir string.
Michael Tsai
18

Anda harus menggunakan Range<String.Index>bukan yang klasik NSRange. Cara saya melakukannya (mungkin ada cara yang lebih baik) adalah dengan mengambil string yang String.Indexbergerak dengannya advance.

Saya tidak tahu kisaran apa yang ingin Anda ganti, tetapi mari kita berpura-pura ingin mengganti 2 karakter pertama.

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
sumber
18

Jawaban oleh Martin R ini tampaknya benar karena ini menjelaskan Unicode.

Namun pada saat posting (Swift 1) kodenya tidak dikompilasi di Swift 2.0 (Xcode 7), karena mereka menghapus advance()fungsinya. Versi terbaru di bawah ini:

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

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

Cepat 4

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

Ini mirip dengan jawaban Emilie namun karena Anda bertanya secara spesifik bagaimana mengonversinya NSRangemenjadi Range<String.Index>Anda akan melakukan sesuatu seperti ini:

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

}
James Jones
sumber
2
Tampilan karakter dan tampilan UTF16 string mungkin memiliki panjang yang berbeda. Fungsi ini menggunakan indeks UTF16 (itu yang NSRangeberbicara, meskipun komponennya adalah bilangan bulat) terhadap tampilan karakter string, yang mungkin gagal ketika digunakan pada string dengan "karakter" yang membutuhkan lebih dari satu unit UTF16 untuk mengekspresikan.
Nate Cook
6

Riff pada jawaban hebat oleh @Emilie, bukan jawaban pengganti / bersaing.
(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)")

Keluaran:

start index: 4
end index:   7
range:       4..<7
original:    πŸ‡ͺπŸ‡ΈπŸ˜‚This is a test
final:       πŸ‡ͺπŸ‡Έ!his is a test

Perhatikan akun indeks untuk beberapa unit kode. Bendera (REGIONAL INDICATOR SYMBOL LETTERS ES) adalah 8 byte dan (WAJAH DENGAN TEAR DARI SUKACITA) adalah 4 byte. (Dalam kasus khusus ini ternyata jumlah byte sama untuk representasi UTF-8, UTF-16 dan UTF-32.)

Membungkusnya dengan fungsi:

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

Keluaran:

newString: !his is a test
zaph
sumber
4
func textField(textField: UITextField, shouldChangeCharactersInRange range: NSRange, replacementString string: String) -> Bool {

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

 }
Darshan Panchal
sumber
2

Dalam Swift 2.0 dengan asumsi 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)
Brenden
sumber
1

Inilah upaya terbaik saya. Tetapi ini tidak dapat memeriksa atau mendeteksi argumen input yang salah.

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.

Hasil.

πŸ˜‚
πŸ‡ͺπŸ‡Έ
πŸ‡ͺπŸ‡Έ
πŸ‡ͺπŸ‡Έ

Detail

NSRangedari NSStringjumlah unit kode UTF-16 s. Dan Range<String.Index>dari Swift Stringadalah jenis relatif buram yang hanya menyediakan operasi kesetaraan dan navigasi. Ini adalah desain yang sengaja disembunyikan.

Meskipun Range<String.Index>tampaknya dipetakan ke offset unit kode UTF-16, itu hanya detail implementasi, dan saya tidak dapat menemukan menyebutkan tentang jaminan apa pun. Itu berarti detail implementasi dapat diubah kapan saja. Representasi internal Swift Stringtidak cukup didefinisikan, dan saya tidak dapat mengandalkannya.

NSRangenilai dapat langsung dipetakan ke String.UTF16Viewindeks. Tetapi tidak ada metode untuk mengubahnya menjadiString.Index .

Swift String.Indexadalah indeks untuk beralih Swift Characteryang merupakan cluster graphode Unicode . Kemudian, Anda harus menyediakan yang tepat NSRangeyang memilih cluster grapheme yang benar. Jika Anda memberikan rentang yang salah seperti contoh di atas, itu akan menghasilkan hasil yang salah karena rentang cluster grapheme yang tepat tidak dapat dipecahkan.

Jika ada jaminan bahwa String.Index adalah UTF-16 kode unit offset, maka masalah menjadi sederhana. Tapi itu tidak mungkin terjadi.

Konversi terbalik

Pokoknya konversi terbalik dapat dilakukan dengan tepat.

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

Hasil.

(0,6)
(4,2)
Eonil
sumber
Dalam Swift 2, return NSRange(location: a.utf16Count, length: b.utf16Count)harus diubah kereturn NSRange(location: a.utf16.count, length: b.utf16.count)
Elliot
1

Saya telah menemukan satu-satunya solusi swift2 terbersih adalah membuat kategori di 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)
    }
}

Dan kemudian memanggilnya dari untuk fungsi delegasi bidang teks:

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
}
Danny Bravo
sumber
Saya perhatikan kode saya tidak lagi berfungsi setelah pembaruan Swift2 terbaru. Saya telah memperbarui jawaban saya untuk bekerja dengan Swift2.
Danny Bravo
0

Dokumentasi resmi Swift 3.0 beta telah memberikan solusi standar untuk situasi ini di bawah judul String.UTF16View di bagian UTF16View Elements Match NSString Characters title

pengguna6511559
sumber
Akan sangat membantu jika Anda akan memberikan tautan dalam jawaban Anda.
m5khan
0

Dalam jawaban yang diterima saya menemukan opsional yang rumit. Ini bekerja dengan Swift 3 dan tampaknya tidak punya masalah dengan emoji.

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
}
Murray Sagal
sumber
0
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)
    }
}
zheng
sumber