How to convert Range in NSRange?
Xcode 11 • Swift 5.1
import Foundation
extension RangeExpression where Bound == String.Index {
func nsRange<S: StringProtocol>(in string: S) -> NSRange { .init(self, in: string) }
}
let string = "Hello USA !!! Hello World !!!"
if let nsRange = string.range(of: "Hello World")?.nsRange(in: string) {
(string as NSString).substring(with: nsRange) // "Hello World"
}
You can also create the corresponding nsRange(of:)
method extending StringProtocol
:
extension StringProtocol {
func nsRange<S: StringProtocol>(of string: S, options: String.CompareOptions = [], range: Range<Index>? = nil, locale: Locale? = nil) -> NSRange? {
self.range(of: string,
options: options,
range: range ?? startIndex..<endIndex,
locale: locale ?? .current)?
.nsRange(in: self)
}
func nsRanges<S: StringProtocol>(of string: S, options: String.CompareOptions = [], range: Range<Index>? = nil, locale: Locale? = nil) -> [NSRange] {
var start = range?.lowerBound ?? startIndex
let end = range?.upperBound ?? endIndex
var ranges: [NSRange] = []
while start < end,
let range = self.range(of: string,
options: options,
range: start..<end,
locale: locale ?? .current) {
ranges.append(range.nsRange(in: self))
start = range.lowerBound < range.upperBound ? range.upperBound :
index(range.lowerBound, offsetBy: 1, limitedBy: endIndex) ?? endIndex
}
return ranges
}
}
let string = "Hello USA !!! Hello World !!!"
if let nsRange = string.nsRange(of: "Hello World") {
(string as NSString).substring(with: nsRange) // "Hello World"
}
let nsRanges = string.nsRanges(of: "Hello")
print(nsRanges) // "[{0, 5}, {19, 5}]\n"
NSRange from Swift Range?
Swift String
ranges and NSString
ranges are not "compatible".
For example, an emoji like counts as one Swift character, but as two NSString
characters (a so-called UTF-16 surrogate pair).
Therefore your suggested solution will produce unexpected results if the string
contains such characters. Example:
let text = "Long paragraph saying!"
let textRange = text.startIndex..<text.endIndex
let attributedString = NSMutableAttributedString(string: text)
text.enumerateSubstringsInRange(textRange, options: NSStringEnumerationOptions.ByWords, { (substring, substringRange, enclosingRange, stop) -> () in
let start = distance(text.startIndex, substringRange.startIndex)
let length = distance(substringRange.startIndex, substringRange.endIndex)
let range = NSMakeRange(start, length)
if (substring == "saying") {
attributedString.addAttribute(NSForegroundColorAttributeName, value: NSColor.redColor(), range: range)
}
})
println(attributedString)
Output:
Long paragra{
}ph say{
NSColor = "NSCalibratedRGBColorSpace 1 0 0 1";
}ing!{
}
As you see, "ph say" has been marked with the attribute, not "saying".
Since NS(Mutable)AttributedString
ultimately requires an NSString
and an NSRange
, it is actually
better to convert the given string to NSString
first. Then the substringRange
is an NSRange
and you don't have to convert the ranges anymore:
let text = "Long paragraph saying!"
let nsText = text as NSString
let textRange = NSMakeRange(0, nsText.length)
let attributedString = NSMutableAttributedString(string: nsText)
nsText.enumerateSubstringsInRange(textRange, options: NSStringEnumerationOptions.ByWords, { (substring, substringRange, enclosingRange, stop) -> () in
if (substring == "saying") {
attributedString.addAttribute(NSForegroundColorAttributeName, value: NSColor.redColor(), range: substringRange)
}
})
println(attributedString)
Output:
Long paragraph {
}saying{
NSColor = "NSCalibratedRGBColorSpace 1 0 0 1";
}!{
}
Update for Swift 2:
let text = "Long paragraph saying!"
let nsText = text as NSString
let textRange = NSMakeRange(0, nsText.length)
let attributedString = NSMutableAttributedString(string: text)
nsText.enumerateSubstringsInRange(textRange, options: .ByWords, usingBlock: {
(substring, substringRange, _, _) in
if (substring == "saying") {
attributedString.addAttribute(NSForegroundColorAttributeName, value: NSColor.redColor(), range: substringRange)
}
})
print(attributedString)
Update for Swift 3:
let text = "Long paragraph saying!"
let nsText = text as NSString
let textRange = NSMakeRange(0, nsText.length)
let attributedString = NSMutableAttributedString(string: text)
nsText.enumerateSubstrings(in: textRange, options: .byWords, using: {
(substring, substringRange, _, _) in
if (substring == "saying") {
attributedString.addAttribute(NSForegroundColorAttributeName, value: NSColor.red, range: substringRange)
}
})
print(attributedString)
Update for Swift 4:
As of Swift 4 (Xcode 9), the Swift standard library
provides method to convert between Range<String.Index>
and NSRange
.
Converting to NSString
is no longer necessary:
let text = "Long paragraph saying!"
let attributedString = NSMutableAttributedString(string: text)
text.enumerateSubstrings(in: text.startIndex..<text.endIndex, options: .byWords) {
(substring, substringRange, _, _) in
if substring == "saying" {
attributedString.addAttribute(.foregroundColor, value: NSColor.red,
range: NSRange(substringRange, in: text))
}
}
print(attributedString)
Here substringRange
is a Range<String.Index>
, and that is converted to the
corresponding NSRange
with
NSRange(substringRange, in: text)
NSRange to Range String.Index
The NSString
version (as opposed to Swift String) of replacingCharacters(in: NSRange, with: NSString)
accepts an NSRange
, so one simple solution is to convert String
to NSString
first. The delegate and replacement method names are slightly different in Swift 3 and 2, so depending on which Swift you're using:
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)
}
How to generate NSRange correctly for a given String to represent end of String?
There are many ways. I recommend making a Swift Range<String.Index>
, and convert it to an NSRange
:
NSRange(string.endIndex..., in: string)
See also: NSRange from Swift Range?
The reason why the incorrect behaviour happens, is because the location
property in NSRange
counts a different thing (UTF-16 code units) than what the Swift String.count
property counts (the number of Swift Character
s, which are extended grapheme clusters, in the string). Therefore, to fix this, we can also just pass the correct number to NSRange(location:length:)
:
NSRange(location: string.utf16.count, length: 0)
// or
NSRange(location: (string as NSString).length, length: 0)
But I find these less intuitive than just converting the Swift range to NSRange
.
Convert String.Index to Int or Range String.Index to NSRange
If you look into the definition of String.Index
you find:
struct Index : BidirectionalIndexType, Comparable, Reflectable {
/// Returns the next consecutive value after `self`.
///
/// Requires: the next value is representable.
func successor() -> String.Index
/// Returns the previous consecutive value before `self`.
///
/// Requires: the previous value is representable.
func predecessor() -> String.Index
/// Returns a mirror that reflects `self`.
func getMirror() -> MirrorType
}
So actually there is no way to convert it to Int
and that for good reason. Depending on the encoding of the string the single characters occupy a different number of bytes. The only way would be to count how many successor
operations are needed to reach the desired String.Index
.
Edit The definition of String
has changed over the various Swift versions but it's basically the same answer. To see the very current definition just CMD-click on a String
definition in XCode to get to the root (works for other types as well).
The distanceTo
is an extension which goes to a variety of protocols. Just look for it in the String
source after the CMD-click.
Convert Range Int to Range String.Index
The Swift String
method rangeOfString()
returns an optional Range?
which
does not have a location
property but can be checked with conditional binding (if let
).
And if you replace the NSString
method stringByReplacingCharactersInRange()
by the Swift String
method replaceRange()
(or in this case
simply by removeRange()
) then you can work purely with Range<Swift.Index>
without converting it to NSRange
or Range<Int>
.
func removeHTMLTags(source : String) -> String {
var sourceString = source
let HTMLTags = "<[^>]*>"
while let range = sourceString.rangeOfString(HTMLTags, options: .RegularExpressionSearch) {
sourceString.removeRange(range)
}
return sourceString;
}
Related Topics
How to Use Alamofire with Custom Headers
Add Subtitle Under the Title in Navigation Bar Controller in Xcode
Why Does Filter(_:)'s Predicate Get Called So Many Times When Evaluating It Lazily
Error When Trying to Save Image in Nsuserdefaults Using Swift
Pull Down to Refresh Data in Swiftui
Uiapplication.Shared.Delegate Equivalent for Scenedelegate Xcode11
Why Is There No Universal Base Class in Swift
Access Input from Uialertcontroller
Swiftui - Navigation Bar Button Not Clickable After Sheet Has Been Presented
Swift Write/Save/Move a Document File to Icloud Drive
How to Check If a Property Value Exists in Array of Objects in Swift
Using Function Parameter Names in Swift
How to Add an Event in the Device Calendar Using Swift
How to Set the Blurradius of Uiblureffectstyle.Light
Change Color Searchbar Result Icon Swift
Replacing Calayer and Cabasicanimation with Skscene and Skactions