NSRange เป็น Range <String.Index>


258

ฉันNSRangeจะแปลงเป็นRange<String.Index>Swift ได้อย่างไร

ฉันต้องการใช้UITextFieldDelegateวิธีการต่อไปนี้:

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

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

ป้อนคำอธิบายรูปภาพที่นี่


71
ขอขอบคุณ Apple ที่ให้คลาส Range ใหม่แก่เรา แต่ไม่ได้อัปเดตคลาสสตริงอรรถประโยชน์ใด ๆ เช่น NSRegularExpression เพื่อใช้งาน!
puzzl

คำตอบ:


269

NSStringรุ่น (เมื่อเทียบกับสวิฟท์ String) ของreplacingCharacters(in: NSRange, with: NSString)ยอมรับNSRangeดังนั้นหนึ่งวิธีง่ายๆคือการแปลงStringไปNSStringเป็นครั้งแรก ชื่อผู้มอบหมายและวิธีการเปลี่ยนจะแตกต่างกันเล็กน้อยใน Swift 3 และ 2 ดังนั้นขึ้นอยู่กับ Swift ที่คุณใช้:

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

สวิฟท์ 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
สิ่งนี้จะไม่ทำงานหากtextFieldมีอักขระยูนิโค้ดหลายหน่วยเช่น emoji เนื่องจากเป็นฟิลด์ป้อนข้อมูลผู้ใช้อาจป้อน emoji (😂) ได้ดีหน้าอื่น ๆ 1 อักขระของอักขระหน่วยรหัสหลายหน่วยเช่นค่าสถานะ (🇪🇸)
zaph

2
@Zaph: เนื่องจากช่วงดังกล่าวมาจาก UITextField ซึ่งเขียนใน Obj-C กับ NSString ฉันจึงสงสัยว่าเฉพาะช่วงที่ถูกต้องซึ่งยึดตามอักขระ Unicode ในสตริงเท่านั้นที่จะได้รับจากการมอบหมายของการโทรกลับ แต่ฉันไม่ได้ทดสอบเป็นการส่วนตัว
Alex Pretzlav

2
@Zaph, ที่จริงจุดของฉันคือการที่UITextFieldDelegate's textField:shouldChangeCharactersInRange:replacementString:วิธีการจะไม่ให้ช่วงที่เริ่มต้นหรือสิ้นสุดภายในอักขระ Unicode เป็นตั้งแต่การเรียกกลับจะขึ้นอยู่กับผู้ใช้ป้อนตัวอักษรจากแป้นพิมพ์และมันเป็นไปไม่ได้ที่จะพิมพ์เพียงส่วนหนึ่ง ของอักขระอิโมจิยูนิโค้ด โปรดทราบว่าหากผู้รับมอบสิทธิ์ถูกเขียนใน Obj-C ก็จะมีปัญหาเดียวกันนี้
Alex Pretzlav

4
ไม่ใช่คำตอบที่ดีที่สุด อ่านรหัสได้ง่ายที่สุด แต่ @ martin-r มีคำตอบที่ถูกต้อง
Rog

3
อันที่จริงพวกคุณควรจะทำเช่นนี้(textField.text as NSString?)?.stringByReplacingCharactersInRange(range, withString: string)
บอยชอย

361

ตั้งแต่Swift 4 (Xcode 9) ไลบรารีมาตรฐาน Swift มีวิธีการแปลงระหว่างช่วงสตริงของ Swift ( Range<String.Index>) และNSStringช่วง ( NSRange) ตัวอย่าง:

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

ดังนั้นการแทนที่ข้อความในวิธีการมอบหมายเขตข้อมูลข้อความสามารถทำได้ดังนี้

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 และรุ่นก่อนหน้านี้ :)

ตั้งแต่ Swift 1.2 String.Indexมี initializer

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

ซึ่งสามารถใช้ในการแปลงNSRangeให้Range<String.Index>ถูกต้อง (รวมถึงทุกกรณีของ Emojis ตัวชี้วัดระดับภูมิภาคหรือกลุ่มกราฟขยายอื่น ๆ ) โดยไม่มีการแปลงระดับกลางเป็น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
    }
}

เมธอดนี้ส่งคืนช่วงสตริงที่เป็นทางเลือกเนื่องจากไม่ใช่NSRanges ทั้งหมดที่ใช้ได้สำหรับสตริง Swift ที่กำหนด

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
}

การแปลงผกผันคือ

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

การทดสอบอย่างง่าย:

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:

rangeFromNSRange()Serhii Yakovenko เวอร์ชั่น Swift 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 2 NSRangeFromRange()คือ

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

ตัวอย่าง:

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
รหัสนั้นทำงานไม่ถูกต้องสำหรับตัวละครที่ประกอบด้วยจุดโค้ด UTF-16 มากกว่าหนึ่งเช่น Emojis - ตัวอย่างเช่นถ้าฟิลด์ข้อความมีข้อความ "👿" และคุณลบตัวอักษรที่แล้วrangeเป็น(0,2)เพราะNSRangeหมายถึง UTF-16 NSStringตัวอักษรใน แต่จะนับเป็นอักขระ Unicode หนึ่งตัวในสตริง Swift - let end = advance(start, range.length)จากคำตอบที่อ้างอิงจะล้มเหลวในกรณีนี้พร้อมกับข้อความแสดงข้อผิดพลาด "ข้อผิดพลาดร้ายแรง: ไม่สามารถเพิ่ม endIndex"
Martin R

8
@MattDiPasquale: แน่นอน ความตั้งใจของฉันคือการตอบคำถามทุกข้อ"ฉันจะแปลง NSRange เป็น Range <String.Index> ใน Swift"ในวิธีที่ปลอดภัยแบบ Unicode ได้อย่างไร (หวังว่าบางคนอาจพบว่ามีประโยชน์ซึ่งไม่เป็นเช่นนั้นมาจนถึงตอนนี้ :(
Martin R

5
ขอบคุณอีกครั้งสำหรับการทำงานของ Apple สำหรับพวกเขา หากไม่มีคนอย่างคุณและ Stack Overflow ไม่มีทางที่ฉันจะพัฒนา iOS / OS X ชีวิตสั้นเกินไป
Womble

1
@ jiminybob99: คำสั่งคลิกStringเพื่อข้ามไปอ้างอิง API อ่านวิธีการทั้งหมดและแสดงความคิดเห็นแล้วลองสิ่งที่แตกต่างกันจนกว่าจะทำงาน :)
มาร์ติน R

1
สำหรับlet from16 = utf16.index(utf16.startIndex, offsetBy: nsRange.location, limitedBy: utf16.endIndex)ผมคิดว่าคุณต้องการจริงที่จะ จำกัด utf16.endIndex - 1โดย มิฉะนั้นคุณสามารถเริ่มจากจุดสิ้นสุดของสตริง
Michael Tsai

18

คุณจำเป็นต้องใช้แทนของคลาสสิกRange<String.Index> NSRangeวิธีที่ผมทำมัน (อาจจะมีวิธีที่ดีกว่า) คือโดยการสตริงที่เคลื่อนที่ด้วยString.Indexadvance

ฉันไม่รู้ว่าคุณพยายามจะเปลี่ยนช่วงอะไร แต่ลองทำเป็นว่าคุณต้องการแทนที่ตัวละคร 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)

18

คำตอบนี้ดูเหมือนว่าของ Martin R นี้จะถูกต้องเพราะมันเป็น Unicode

อย่างไรก็ตามในช่วงเวลาของการโพสต์ (Swift 1) รหัสของเขาไม่ได้รวบรวมใน Swift 2.0 (Xcode 7) เพราะพวกเขาลบ advance()ฟังก์ชั่น รุ่นที่ปรับปรุงอยู่ด้านล่าง:

สวิฟท์ 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
    }
}

สวิฟท์ 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
    }
}

สวิฟต์ 4

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

7

นี่คล้ายกับคำตอบของ 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) 
     ...

}

2
มุมมองอักขระและมุมมอง UTF16 ของสตริงอาจมีความยาวต่างกัน ฟังก์ชันนี้ใช้ดัชนี UTF16 (นั่นคือสิ่งที่NSRangeพูดแม้ว่าส่วนประกอบจะเป็นจำนวนเต็ม) กับมุมมองอักขระของสตริงซึ่งอาจล้มเหลวเมื่อใช้กับสตริงที่มี "อักขระ" ซึ่งใช้หน่วย UTF16 มากกว่าหนึ่งรายการ
Nate Cook

6

คำตอบที่ยอดเยี่ยมโดย @Emilie ไม่ใช่คำตอบที่ทดแทน / แข่งขัน
(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)")

เอาท์พุท:

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

สังเกตว่าบัญชีดัชนีมีหน่วยรหัสหลายหน่วย แฟล็ก (ตัวบ่งชี้สัญลักษณ์ส่วนย่อย ES) คือ 8 ไบต์และ (FACE with TEARS OF JOY) คือ 4 ไบต์ (ในกรณีนี้ปรากฎว่าจำนวนไบต์เหมือนกันสำหรับการแทน UTF-8, UTF-16 และ UTF-32)

ห่อไว้ใน 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)")

เอาท์พุท:

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 สมมติว่า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

นี่คือความพยายามที่ดีที่สุดของฉัน แต่สิ่งนี้ไม่สามารถตรวจสอบหรือตรวจสอบอาร์กิวเมนต์ที่ป้อนผิด

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.

ผลลัพธ์.

😂
🇪🇸
🇪🇸
🇪🇸

รายละเอียด

NSRangeจากNSStringข้อหา UTF-16 รหัสหน่วย s และRange<String.Index>จากสวิฟท์Stringเป็นประเภทสัมพัทธ์ทึบแสงซึ่งให้การดำเนินการที่เท่าเทียมกันและการนำทางเท่านั้น นี่คือการออกแบบที่ซ่อนความตั้งใจ

แม้ว่าRange<String.Index>ดูเหมือนจะถูกแมปไปยังออฟเซ็ตรหัสหน่วย UTF-16 นั่นเป็นเพียงรายละเอียดการใช้งานและฉันไม่สามารถพูดถึงการรับประกันใด ๆ ซึ่งหมายความว่ารายละเอียดการใช้งานสามารถเปลี่ยนแปลงได้ตลอดเวลา การเป็นตัวแทนภายในของ Swift Stringไม่ได้นิยามไว้สวยและฉันไม่สามารถพึ่งพาได้

NSRangeค่าสามารถถูกแมปโดยตรงกับString.UTF16Viewดัชนี แต่ไม่มีวิธีในการแปลงเป็นString.Indexแต่มีวิธีการในการแปลงเป็นไม่มี

รวดเร็ว String.Indexเป็นดัชนีเพื่อย้ำสวิฟท์Characterซึ่งเป็นคลัสเตอร์อักษร Unicode จากนั้นคุณจะต้องระบุให้ถูกต้องNSRangeซึ่งเลือกกลุ่มของกราฟที่ถูกต้อง หากคุณระบุช่วงที่ไม่ถูกต้องเช่นตัวอย่างด้านบนจะทำให้เกิดผลลัพธ์ที่ไม่ถูกต้องเนื่องจากไม่สามารถหาช่วงกลุ่มกราฟที่เหมาะสมได้

หากมีการรับประกันว่าString.Index เป็น UTF-16 รหัสหน่วยชดเชยปัญหาแล้วจะกลายเป็นง่าย แต่มันไม่น่าจะเกิดขึ้น

การแปลงผกผัน

อย่างไรก็ตามการแปลงผกผันสามารถทำได้อย่างแม่นยำ

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

ผลลัพธ์.

(0,6)
(4,2)

ใน Swift 2 return NSRange(location: a.utf16Count, length: b.utf16Count)จะต้องเปลี่ยนเป็นreturn NSRange(location: a.utf16.count, length: b.utf16.count)
Elliot

1

ฉันพบวิธีแก้ปัญหา 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)
    }
}

จากนั้นเรียกใช้จากสำหรับฟังก์ชั่นมอบหมายฟิลด์ข้อความ:

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
}

ฉันสังเกตเห็นว่ารหัสของฉันไม่ทำงานหลังจากอัปเดต Swift2 ล่าสุด ฉันได้อัปเดตคำตอบสำหรับการทำงานกับ Swift2 แล้ว
Danny Bravo

0

เอกสารประกอบอย่างเป็นทางการของ Swift 3.0 เบต้าได้จัดเตรียมโซลูชันมาตรฐานสำหรับสถานการณ์นี้ภายใต้ชื่อString.UTF16Viewในส่วนUTF16View องค์ประกอบที่ตรงกับชื่อตัวละคร NSString


มันจะมีประโยชน์ถ้าคุณจะให้ลิงค์ในคำตอบของคุณ
m5khan

0

ในคำตอบที่ยอมรับฉันพบว่าตัวเลือกยุ่งยาก ใช้งานได้กับ 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
}

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.