Last active
May 22, 2021 07:43
-
-
Save mediter/bd00f1ad04ace6f546e7cba0b89ab3a0 to your computer and use it in GitHub Desktop.
[NSRange v.s. Range<String.Index>] #swift #nsrange #range
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
https://stackoverflow.com/a/30404532 | |
As of Swift 4 (Xcode 9), the Swift standard library provides methods to convert between Swift string ranges (Range<String.Index>) and NSString ranges (NSRange). Example: | |
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: text)! | |
print(str.substring(with: r2)) // π©πͺ | |
Therefore the text replacement in the text field delegate method can now be done as | |
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) | |
// ... | |
} | |
// ... | |
} | |
(Older answers for Swift 3 and earlier:) | |
As of Swift 1.2, String.Index has an initializer | |
init?(_ utf16Index: UTF16Index, within characters: String) | |
which can be used to convert NSRange to Range<String.Index> correctly (including all cases of Emojis, Regional Indicators or other extended grapheme clusters) without intermediate conversion to an 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 | |
} | |
} | |
This method returns an optional string range because not all NSRanges are valid for a given Swift string. | |
The UITextFieldDelegate delegate method can then be written as | |
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 | |
} | |
The inverse conversion is | |
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) | |
} | |
} | |
A simple 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)) // π©πͺ | |
Update for Swift 2: | |
The Swift 2 version of rangeFromNSRange() was already given by Serhii Yakovenko in this answer, I am including it here for completeness: | |
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 | |
} | |
} | |
The Swift 2 version of NSRangeFromRange() is | |
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)) | |
} | |
} | |
Update for 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 | |
} | |
} | |
Example: | |
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)) // π©πͺ |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment