NSRange đến Phạm vi <String. Index>


258

Làm cách nào tôi có thể chuyển đổi NSRangesang Range<String.Index>trong Swift?

Tôi muốn sử dụng UITextFieldDelegatephương pháp sau :

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

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

nhập mô tả hình ảnh ở đây


71
Cảm ơn bạn, Apple, đã cho chúng tôi lớp Range mới, nhưng sau đó không cập nhật bất kỳ lớp tiện ích chuỗi nào, chẳng hạn như NSRegularExpression, để sử dụng chúng!
puzzl

Câu trả lời:


269

Các NSStringphiên bản (như trái ngược với Swift String) của replacingCharacters(in: NSRange, with: NSString)chấp nhận một NSRange, vì vậy một giải pháp đơn giản là để chuyển đổi Stringđể NSStringđầu tiên . Tên phương thức ủy nhiệm và thay thế hơi khác nhau trong Swift 3 và 2, do đó tùy thuộc vào Swift bạn đang sử dụng:

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
Điều này sẽ không hoạt động nếu textFieldchứa các ký tự unicode đơn vị nhiều mã như biểu tượng cảm xúc. Vì là trường nhập, người dùng cũng có thể nhập biểu tượng cảm xúc (), trang 1 ký tự khác của nhiều ký tự đơn vị mã như cờ (🇪🇸).
zaph

2
@Zaph: do phạm vi xuất phát từ UITextField, được viết bằng Obj-C chống lại NSString, tôi nghi ngờ rằng chỉ các phạm vi hợp lệ dựa trên các ký tự unicode trong chuỗi sẽ được cung cấp bởi cuộc gọi lại của đại biểu và vì vậy điều này an toàn khi sử dụng , nhưng cá nhân tôi đã không thử nó.
Alex Pretzlav

2
@Zaph, đó là sự thật, quan điểm của tôi là UITextFieldDelegate's textField:shouldChangeCharactersInRange:replacementString:phương pháp sẽ không cung cấp một phạm vi mà bắt đầu hoặc kết thúc bên trong một ký tự unicode, kể từ khi gọi lại được dựa trên một người dùng nhập các ký tự từ bàn phím, và nó không thể gõ chỉ phần của một nhân vật biểu tượng cảm xúc unicode. Lưu ý rằng nếu đại biểu được viết bằng Obj-C, nó sẽ có vấn đề tương tự.
Alex Pretzlav

4
Không phải là câu trả lời tốt nhất. Dễ nhất để đọc mã nhưng @ martin-r có câu trả lời đúng.
Rog

3
Thật ra các bạn nên làm điều này(textField.text as NSString?)?.stringByReplacingCharactersInRange(range, withString: string)
Wanbok Choi

361

Kể từ Swift 4 (Xcode 9), thư viện chuẩn Swift cung cấp các phương thức để chuyển đổi giữa phạm vi chuỗi Swift ( Range<String.Index>) và NSStringphạm vi ( NSRange). Thí dụ:

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

Do đó, việc thay thế văn bản trong phương thức ủy nhiệm trường văn bản hiện có thể được thực hiện như

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

(Câu trả lời cũ hơn cho Swift 3 trở về trước :)

Kể từ Swift 1.2, String.Indexcó trình khởi tạo

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

có thể được sử dụng để chuyển đổi NSRangethành Range<String.Index>chính xác (bao gồm tất cả các trường hợp Biểu tượng cảm xúc, Chỉ báo khu vực hoặc cụm biểu đồ mở rộng khác) mà không cần chuyển đổi trung gian thành 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
    }
}

Phương thức này trả về một phạm vi chuỗi tùy chọn vì không phải tất cả NSRanges đều hợp lệ cho một chuỗi Swift đã cho.

Các UITextFieldDelegatephương pháp đại biểu sau đó có thể được viết như sau

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
}

Chuyển đổi ngược lại là

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

Một bài kiểm tra đơn giản:

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

Cập nhật cho Swift 2:

Phiên bản Swift 2 rangeFromNSRange()đã được Serhii Yakovenko đưa ra trong câu trả lời này , tôi bao gồm nó ở đây để hoàn thiện:

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

Phiên bản 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))
    }
}

Cập nhật cho 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
    }
}

Thí dụ:

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
Mã đó không hoạt động chính xác cho các ký tự bao gồm nhiều hơn một điểm mã UTF-16, ví dụ: Biểu tượng cảm xúc. - Ví dụ: nếu trường văn bản chứa văn bản "" và bạn xóa ký tự đó, thì đó range(0,2)NSRangeđề cập đến các ký tự UTF-16 trong một NSString. Nhưng nó được tính là một ký tự Unicode trong chuỗi Swift. - let end = advance(start, range.length)từ câu trả lời được tham chiếu không gặp sự cố trong trường hợp này với thông báo lỗi "lỗi nghiêm trọng: không thể tăng end Index".
Martin R

8
@MattDiPasquale: Chắc chắn rồi. Ý định của tôi là trả lời câu hỏi nguyên văn "Làm cách nào tôi có thể chuyển đổi NSRange thành Phạm vi <String.Index> trong Swift" theo cách an toàn Unicode (hy vọng rằng ai đó có thể thấy nó hữu ích, không phải là trường hợp cho đến bây giờ :(
Martin R

5
Cảm ơn bạn, một lần nữa, vì đã làm việc cho Apple. Không có những người như bạn và Stack Overflow, không có cách nào tôi có thể thực hiện phát triển iOS / OS X. Cuộc đời quá ngắn ngủi.
Womble

1
@ jiminybob99: Nhấp chuột vào Stringlệnh để chuyển đến tham chiếu API, đọc tất cả các phương thức và nhận xét, sau đó thử những thứ khác nhau cho đến khi nó hoạt động :)
Martin R

1
Đối với let from16 = utf16.index(utf16.startIndex, offsetBy: nsRange.location, limitedBy: utf16.endIndex), tôi nghĩ rằng bạn thực sự muốn giới hạn nó bằng utf16.endIndex - 1. Nếu không, bạn có thể bắt đầu kết thúc chuỗi.
Michael Tsai

18

Bạn cần sử dụng Range<String.Index>thay vì cổ điển NSRange. Cách tôi làm điều đó (có thể có một cách tốt hơn) là bằng cách String.Indexdi chuyển chuỗi advance.

Tôi không biết bạn đang cố gắng thay thế phạm vi nào, nhưng hãy giả vờ bạn muốn thay thế 2 ký tự đầu tiên.

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

Câu trả lời này của Martin R có vẻ đúng vì nó chiếm Unicode.

Tuy nhiên, tại thời điểm đăng bài (Swift 1), mã của anh ấy không biên dịch trong Swift 2.0 (Xcode 7), vì chúng đã xóa advance()chức năng. Phiên bản cập nhật dưới đây:

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

Swift 4

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

7

Điều này tương tự với câu trả lời của Emilie tuy nhiên vì bạn đã hỏi cụ thể làm thế nào để chuyển đổi NSRangethành Range<String.Index>bạn sẽ làm một cái gì đó như thế này:

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
Chế độ xem ký tự và chế độ xem UTF16 của một chuỗi có thể có độ dài khác nhau. Hàm này đang sử dụng các chỉ mục UTF16 (đó là những gì NSRangenói, mặc dù các thành phần của nó là số nguyên) đối với chế độ xem ký tự của chuỗi, có thể thất bại khi được sử dụng trên một chuỗi có "ký tự" có nhiều hơn một đơn vị UTF16 để thể hiện.
Nate Cook

6

Một câu trả lời về câu trả lời tuyệt vời của @Emilie, không phải là câu trả lời thay thế / cạnh tranh.
(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)")

Đầu ra:

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

Lưu ý các chỉ mục chiếm nhiều đơn vị mã. Cờ (REGIONAL INDICATOR SYMBOL LETTERS ES) là 8 byte và (FACE With TEARS OF JOY) là 4 byte. (Trong trường hợp cụ thể này, hóa ra số lượng byte là giống nhau cho các đại diện UTF-8, UTF-16 và UTF-32.)

Gói nó trong một 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)")

Đầu ra:

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

Trong giả định 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

Đây là nỗ lực tốt nhất của tôi. Nhưng điều này không thể kiểm tra hoặc phát hiện đối số đầu vào sai.

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.

Kết quả.

😂
🇪🇸
🇪🇸
🇪🇸

Chi tiết

NSRangetừ NSStringsố đơn vị mã UTF-16 s. Và Range<String.Index>từ Swift Stringlà một loại tương đối mờ chỉ cung cấp các hoạt động điều hướng và bình đẳng. Đây là thiết kế ẩn cố ý.

Mặc dù Range<String.Index>dường như được ánh xạ tới phần bù đơn vị mã UTF-16, đó chỉ là một chi tiết triển khai và tôi không thể tìm thấy bất kỳ đề cập nào về bất kỳ đảm bảo nào. Điều đó có nghĩa là các chi tiết thực hiện có thể được thay đổi bất cứ lúc nào. Đại diện nội bộ của Swift Stringkhông được xác định rõ ràng và tôi không thể dựa vào nó.

NSRangecác giá trị có thể được ánh xạ trực tiếp đến String.UTF16Viewcác chỉ mục. Nhưng không có phương pháp để chuyển đổi nó thành String.Index.

Swift String.Indexlà chỉ mục để lặp lại Swift Characterlà một cụm đồ thị Unicode . Sau đó, bạn phải cung cấp đúng cách NSRangechọn cụm grapheme chính xác. Nếu bạn cung cấp phạm vi sai như ví dụ trên, nó sẽ tạo ra kết quả sai vì phạm vi cụm grapheme thích hợp không thể được tìm ra.

Nếu có một sự đảm bảo rằng các String.Index UTF-16 mã đơn vị bù đắp, sau đó vấn đề trở nên đơn giản. Nhưng nó không có khả năng xảy ra.

Chuyển đổi ngược

Dù sao việc chuyển đổi ngược có thể được thực hiện chính xác.

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

Kết quả.

(0,6)
(4,2)

Trong Swift 2, return NSRange(location: a.utf16Count, length: b.utf16Count)phải đổi thànhreturn NSRange(location: a.utf16.count, length: b.utf16.count)
Elliot

1

Tôi đã tìm thấy giải pháp duy nhất trong swift2 là tạo một danh mục trên 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)
    }
}

Và sau đó gọi nó từ cho chức năng ủy nhiệm trường văn bả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
}

Tôi nhận thấy mã của tôi không còn hoạt động sau bản cập nhật Swift2 mới nhất. Tôi đã cập nhật câu trả lời của mình để làm việc với Swift2.
Daniel Bravo

0

Tài liệu chính thức Swift 3.0 beta đã cung cấp giải pháp tiêu chuẩn cho tình huống này dưới tiêu đề String.UTF16View trong phần UTF16View Các yếu tố phù hợp với tiêu đề Nhân vật NSString


Sẽ rất hữu ích nếu bạn sẽ cung cấp các liên kết trong câu trả lời của bạn.
m5khan

0

Trong câu trả lời được chấp nhận tôi thấy các tùy chọn cồng kềnh. Điều này hoạt động với Swift 3 và dường như không có vấn đề gì với biểu tượng cảm xúc.

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)
    }
}
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.