NSRange to Range <String.Index>


258

Nasıl dönüştürebilirsiniz NSRangeiçin Range<String.Index>Swift?

Aşağıdaki UITextFieldDelegateyöntemi kullanmak istiyorum :

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

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

resim açıklamasını buraya girin


71
Apple, bize yeni Range sınıfını verdiğiniz için teşekkür eder, ancak bunları kullanmak için NSRegularExpression gibi dize yardımcı programı sınıflarının hiçbirini güncellemezsiniz!
puzzl

Yanıtlar:


269

NSStringVersiyonu (Swift String tersine) replacingCharacters(in: NSRange, with: NSString)bir kabul eden NSRange, çok basit bir çözüm için dönüştürmek Stringüzere NSString, ilk . Temsilci ve değiştirme yöntemi adları Swift 3 ve 2'de biraz farklıdır, bu nedenle hangi Swift'i kullandığınıza bağlı olarak:

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

14
textFieldEmoji gibi çok kodlu unicode karakterler içeriyorsa bu çalışmaz . Bir giriş alanı olduğundan, kullanıcı emoji (😂), bayraklar (🇪🇸) gibi çoklu kod birimi karakterlerinin diğer sayfa 1 karakterlerini girebilir.
zaph

2
@Zaph: Aralık, Obj-C'de NSString'e göre yazılan UITextField'dan geldiğinden, yalnızca dizedeki unicode karakterlere dayanan geçerli aralıkların delege geri araması tarafından sağlanacağından şüpheleniyorum ve bu nedenle bu kullanım güvenli , ama ben şahsen test etmedim.
Alex Pretzlav

2
@Zaph, doğru olduğunu, benim açımdan olmasıdır UITextFieldDelegate'ın textField:shouldChangeCharactersInRange:replacementString:geri arama klavyeden karakterleri girerken bir kullanıcı dayalı olduğu yöntem, unicode karakteri içeride başlar veya biter bir dizi sağlamayacaktır, ve yazmanın sadece bir parçası mümkün değildir bir emoji unicode karakter. Delege Obj-C'de yazılmış olsaydı, aynı sorunu yaşayacağını unutmayın.
Alex Pretzlav

4
En iyi cevap değil. Kodu okumak en kolay ama @ martin-r doğru cevabı veriyor.
Rog

3
Aslında siz bunu yapmalısınız(textField.text as NSString?)?.stringByReplacingCharactersInRange(range, withString: string)
Wanbok Choi

361

İtibariyle Swift 4 (Xcode 9), Swift standart kitaplığı Swift dize aralıkları (arasında dönüştürmek için yöntemler sağlar Range<String.Index>) ve NSStringaralıklar ( NSRange). Misal:

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

Bu nedenle, metin alanı delege yöntemindeki metin değiştirme işlemi şu şekilde yapılabilir:

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 ve öncesi için eski cevaplar :)

Swift 1.2 itibariyle String.Index, bir başlatıcısı var

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

per se için kullanılabilir NSRangeiçin Range<String.Index>doğru bir şekilde, ara dönüşüm olmadan (emojiler Bölgesel Göstergeler veya diğer genişletilmiş sesletim kümelerin her durumda da dahil olmak üzere) 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
    }
}

Bu yöntem isteğe bağlı bir dize aralığı döndürür, çünkü NSRangebelirli bir Swift dizesi için tüm s'ler geçerli değildir .

UITextFieldDelegateTemsilci Daha sonra, yöntem şu şekilde yazılabilir

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
}

Ters dönüşüm

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

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

Swift 2 için güncelleme:

Swift 2 versiyonu rangeFromNSRange()zaten bu cevapta Serhii Yakovenko tarafından verildi, tamlık için buraya ekliyorum :

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 2 versiyonu NSRangeFromRange()DİR

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 Güncellemesi (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
    }
}

Misal:

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

7
Bu kod, birden fazla UTF-16 kod noktasından (Emojis) oluşan karakterler için düzgün çalışmaz. - Örnek olarak, metin alanı "👿" metnini içeriyorsa ve bu karakteri silerseniz, rangebunun (0,2)nedeni NSRange, bir NSString. Ancak Swift dizesinde bir Unicode karakteri olarak sayılır . - let end = advance(start, range.length)başvurulan cevaptan "önemli hata: endIndex artış olamaz" hata mesajı ile bu durumda çöküyor.
Martin R

8
@MattDiPasquale: Elbette. Niyetim kelimesi kelimesine soruyu cevaplamak için oldu "Ben Swift in Range <String.Index> için NSRange dönüştürebilirsiniz nasıl" birisi :( şimdiye kadar böyle değildir ki, bu yarayabilecek umuduyla (Unicode güvenli bir şekilde
Martin R

5
Apple'ın onlar için çalıştığı için bir kez daha teşekkür ederim. Sizin gibi insanlar ve Stack Overflow olmasaydı, iOS / OS X geliştirme yapmam mümkün değil. Hayat çok kısa.
Womble

1
@ jiminybob99: StringAPI referansına atlamak, tüm yöntemleri ve yorumları okumak, ardından çalışana kadar farklı şeyler denemek için Command tuşuna basın :)
Martin R

1
Çünkü let from16 = utf16.index(utf16.startIndex, offsetBy: nsRange.location, limitedBy: utf16.endIndex)bence bunu gerçekten sınırlamak istiyorsunuz utf16.endIndex - 1. Aksi takdirde, dizenin sonundan başlayabilirsiniz.
Michael Tsai

18

Kullanmalısın Range<String.Index>Klasik yerineNSRange . Bunu yapmanın yolu (belki de daha iyi bir yol var) dize String.Indexile birlikte hareket etmektir advance.

Hangi aralığı değiştirmeye çalıştığınızı bilmiyorum, ancak ilk 2 karakteri değiştirmek istediğinizi varsayalım.

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)

18

Martin R'nin bu cevabı doğru görünüyor çünkü Unicode'u açıklıyor.

Ancak yazı (Swift 1) sırasında kodu Swift 2.0'da (Xcode 7) derlenmez, çünkü advance()işlevi kaldırmışlardır . Güncellenmiş sürüm aşağıdadır:

Hızlı 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
    }
}

Hızlı 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
    }
}

Hızlı 4

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

7

Dönüştürmek nasıl özellikle istedi beri Ancak bu Emilie cevabı benzer NSRangeüzere Range<String.Index>size böyle bir şey yapacağını:

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

}

2
Bir dizenin karakter görünümü ve UTF16 görünümü farklı uzunluklarda olabilir. Bu işlev, NSRangeifade için birden fazla UTF16 birimi alan bir "karakter" içeren bir dizede kullanıldığında başarısız olabilen, dizenin karakter görünümüne karşı UTF16 dizinlerini ( bileşenlerinin tamsayı olmasına rağmen konuşur) kullanır.
Nate Cook

6

@Emilie'nin büyük cevabı, bir yedek / rakip cevap değil.
(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)")

Çıktı:

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

Birden çok kod birimi için dizin hesabına dikkat edin. Bayrak (BÖLGESEL GÖSTERGE SEMBOL MEKTUPLARI ES) 8 bayt ve (JOY TEARS İLE YÜZ) 4 bayttır. (Bu özel durumda, bayt sayısının UTF-8, UTF-16 ve UTF-32 gösterimleri için aynı olduğu ortaya çıkar.)

Bir fonk içinde sarma:

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

Çıktı:

newString: !his is a test

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

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

 }

2

Swift 2.0'da 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)

1

İşte en iyi çabam. Ancak bu yanlış giriş argümanını kontrol edemez veya tespit edemez.

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.

Sonuç.

😂
🇪🇸
🇪🇸
🇪🇸

ayrıntılar

NSRangedan NSStringsayımları UTF-16 kod birimi s. Ve Range<String.Index>Swift adlı Stringbir olan opak tek eşitlik ve navigasyon işlemler sağlar göreceli türü. Bu kasıtlı olarak gizli bir tasarımdır.

Range<String.Index>UTF-16 kod birimi ofseti ile eşlenmiş gibi görünüyor olsa da , bu sadece bir uygulama detayı ve herhangi bir garanti hakkında herhangi bir söz bulamadık. Bu, uygulama ayrıntılarının herhangi bir zamanda değiştirilebileceği anlamına gelir. Swift'in iç temsili Stringoldukça tanımlanmamıştır ve buna güvenemem.

NSRangedeğerler doğrudan String.UTF16Viewindekslerle eşlenebilir . Ama onu dönüştürmek için bir yöntem yokString.Index .

Swift String.Index, Characterbir Unicode grafik kümesi kümesi olan Swift'i yinelemenin dizinidir . Ardından, NSRangedoğru grafik kümelerini seçen doğru olanı sağlamalısınız . Yukarıdaki örnek gibi yanlış bir aralık sağlarsanız, doğru grafik oluşturma küme aralığı belirlenemediğinden yanlış sonuç verir.

Bir garanti varsa o String.Index olduğunu UTF-16 kod birimi ofset, o zaman sorun basit hale gelir. Ancak gerçekleşmesi pek olası değildir.

Ters dönüşüm

Her neyse, ters dönüşüm tam olarak yapılabilir.

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

Sonuç.

(0,6)
(4,2)

Swift 2'dereturn NSRange(location: a.utf16Count, length: b.utf16Count)return NSRange(location: a.utf16.count, length: b.utf16.count)
Elliot

1

En temiz swift2 çözümü NSRange üzerinde bir kategori oluşturmak olduğunu buldum:

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

Ve sonra metin alanı temsilci işlevi için çağırın:

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
}

Kodumun en son Swift2 güncellemesinden sonra artık çalışmadığını fark ettim. Swift2 ile çalışmak için cevabımı güncelledim.
Danny Bravo

0

Swift 3.0 beta resmi belgeleri, UTF16View Elements Match NSString Karakterleri başlığı altındaki String.UTF16Görünüm başlığı altında bu durum için standart çözümünü sağlamıştır .


Cevabınızda bağlantılar sağlamanız yararlı olacaktır.
m5khan

0

Kabul edilen cevapta opsiyonelleri hantal buluyorum. Bu Swift 3 ile çalışıyor ve emoji ile hiçbir problemi yok gibi görünüyor.

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
}

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)
    }
}
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.