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)
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 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 create range in Swift?
Updated for Swift 4
Swift ranges are more complex than NSRange
, and they didn't get any easier in Swift 3. If you want to try to understand the reasoning behind some of this complexity, read this and this. I'll just show you how to create them and when you might use them.
Closed Ranges: a...b
This range operator creates a Swift range which includes both element a
and element b
, even if b
is the maximum possible value for a type (like Int.max
). There are two different types of closed ranges: ClosedRange
and CountableClosedRange
.
1. ClosedRange
The elements of all ranges in Swift are comparable (ie, they conform to the Comparable protocol). That allows you to access the elements in the range from a collection. Here is an example:
let myRange: ClosedRange = 1...3
let myArray = ["a", "b", "c", "d", "e"]
myArray[myRange] // ["b", "c", "d"]
However, a ClosedRange
is not countable (ie, it does not conform to the Sequence protocol). That means you can't iterate over the elements with a for
loop. For that you need the CountableClosedRange
.
2. CountableClosedRange
This is similar to the last one except now the range can also be iterated over.
let myRange: CountableClosedRange = 1...3
let myArray = ["a", "b", "c", "d", "e"]
myArray[myRange] // ["b", "c", "d"]
for index in myRange {
print(myArray[index])
}
Half-Open Ranges: a..<b
This range operator includes element a
but not element b
. Like above, there are two different types of half-open ranges: Range
and CountableRange
.
1. Range
As with ClosedRange
, you can access the elements of a collection with a Range
. Example:
let myRange: Range = 1..<3
let myArray = ["a", "b", "c", "d", "e"]
myArray[myRange] // ["b", "c"]
Again, though, you cannot iterate over a Range
because it is only comparable, not stridable.
2. CountableRange
A CountableRange
allows iteration.
let myRange: CountableRange = 1..<3
let myArray = ["a", "b", "c", "d", "e"]
myArray[myRange] // ["b", "c"]
for index in myRange {
print(myArray[index])
}
NSRange
You can (must) still use NSRange
at times in Swift (when making attributed strings, for example), so it is helpful to know how to make one.
let myNSRange = NSRange(location: 3, length: 2)
Note that this is location and length, not start index and end index. The example here is similar in meaning to the Swift range 3..<5
. However, since the types are different, they are not interchangeable.
Ranges with Strings
The ...
and ..<
range operators are a shorthand way of creating ranges. For example:
let myRange = 1..<3
The long hand way to create the same range would be
let myRange = CountableRange<Int>(uncheckedBounds: (lower: 1, upper: 3)) // 1..<3
You can see that the index type here is Int
. That doesn't work for String
, though, because Strings are made of Characters and not all characters are the same size. (Read this for more info.) An emoji like , for example, takes more space than the letter "b".
Problem with NSRange
Try experimenting with NSRange
and an NSString
with emoji and you'll see what I mean. Headache.
let myNSRange = NSRange(location: 1, length: 3)
let myNSString: NSString = "abcde"
myNSString.substring(with: myNSRange) // "bcd"
let myNSString2: NSString = "acde"
myNSString2.substring(with: myNSRange) // "c" Where is the "d"!?
The smiley face takes two UTF-16 code units to store, so it gives the unexpected result of not including the "d".
Swift Solution
Because of this, with Swift Strings you use Range<String.Index>
, not Range<Int>
. The String Index is calculated based on a particular string so that it knows if there are any emoji or extended grapheme clusters.
Example
var myString = "abcde"
let start = myString.index(myString.startIndex, offsetBy: 1)
let end = myString.index(myString.startIndex, offsetBy: 4)
let myRange = start..<end
myString[myRange] // "bcd"
myString = "acde"
let start2 = myString.index(myString.startIndex, offsetBy: 1)
let end2 = myString.index(myString.startIndex, offsetBy: 4)
let myRange2 = start2..<end2
myString[myRange2] // "cd"
One-sided Ranges: a...
and ...b
and ..<b
In Swift 4 things were simplified a bit. Whenever the starting or ending point of a range can be inferred, you can leave it off.
Int
You can use one-sided integer ranges to iterate over collections. Here are some examples from the documentation.
// iterate from index 2 to the end of the array
for name in names[2...] {
print(name)
}
// iterate from the beginning of the array to index 2
for name in names[...2] {
print(name)
}
// iterate from the beginning of the array up to but not including index 2
for name in names[..<2] {
print(name)
}
// the range from negative infinity to 5. You can't iterate forward
// over this because the starting point in unknown.
let range = ...5
range.contains(7) // false
range.contains(4) // true
range.contains(-1) // true
// You can iterate over this but it will be an infinate loop
// so you have to break out at some point.
let range = 5...
String
This also works with String ranges. If you are making a range with str.startIndex
or str.endIndex
at one end, you can leave it off. The compiler will infer it.
Given
var str = "Hello, playground"
let index = str.index(str.startIndex, offsetBy: 5)
let myRange = ..<index // Hello
You can go from the index to str.endIndex by using ...
var str = "Hello, playground"
let index = str.index(str.endIndex, offsetBy: -10)
let myRange = index... // playground
See also:
- How does String.Index work in Swift
- How does String substring work in Swift
Notes
- You can't use a range you created with one string on a different string.
- As you can see, String ranges are a pain in Swift, but they do make it possibly to deal better with emoji and other Unicode scalars.
Further Study
- String Range examples
- Range Operator documentation
- Strings and Characters documentation
String, substring, Range, NSRange in Swift 4
Use Range(_, in:) to convert an NSRange to a Range in Swift 4.
extension String {
func substring(with nsrange: NSRange) -> Substring? {
guard let range = Range(nsrange, in: self) else { return nil }
return self[range]
}
}
How to find Multiple NSRange for a string from full string iOS swift
You say that you want to iterate through NSRange
matches in a string so that you can apply a bold attribute to the relevant substrings. You could use NSRegularExpression
to do that:
let range = NSRange(location: 0, length: string.count)
try! NSRegularExpression(pattern: "[0-9]+").enumerateMatches(in: string, range: range) { result, _, _ in
guard let range = result?.range else { return }
attributedString.setAttributes(boldAttributes, range: range)
}
Personally, I find it useful to have a method to return an array of Swift ranges, i.e. [Range<String.Index>]
:
extension StringProtocol {
func ranges<T: StringProtocol>(of string: T, options: String.CompareOptions = []) -> [Range<Index>] {
var ranges: [Range<Index>] = []
var start: Index = startIndex
while let range = range(of: string, options: options, range: start ..< endIndex) {
ranges.append(range)
if !range.isEmpty {
start = range.upperBound // if not empty, resume search at upper bound
} else if range.lowerBound < endIndex {
start = index(after: range.lowerBound) // if empty and not at end, resume search at next character
} else {
break // if empty and at end, then quit
}
}
return ranges
}
}
Then you can use it like so:
let string = "Hello world, there are 09 continents and 195 countries."
let ranges = string.ranges(of: "[0-9]+", options: .regularExpression)
And then you can map
the Range
to NSRange
. Going back to the original example, if you wanted to make these numbers bold in some attributed string:
string.ranges(of: "[0-9]+", options: .regularExpression)
.map { NSRange($0, in: string) }
.forEach { attributedString.setAttributes(boldAttributes, range: $0) }
Return substring from attributed string using range in Swift
The thing is you can not subscript a String
using a NSRange
, you have to use a Range
. Try the following out:
let newRange = Range(range, in: text)
let mySubstring = text[newRange]
let myString = String(mySubstring)
Related Topics
How to Use Facebook iOS Sdk on iOS 10
Xcode 6: Keyboard Does Not Show Up in Simulator
Remove or Uninstall Library Previously Added: Cocoapods
Send Post Request Using Nsurlsession
Run Code Only After Asynchronous Function Finishes Executing
iPhone Sdk: Differencebetween Loadview and Viewdidload
Unique Values of Array in Swift
How to Scroll the Uiscrollview When the Keyboard Appears
How to Resize the Iphone/iPad Simulator
How to Create Local Notifications
Difference Between Uiviewcontentmodescaleaspectfit and Uiviewcontentmodescaletofill
Programmatically Align a Toolbar on Top of the Iphone Keyboard
Uinavigationbar Custom Back Button Without Title
Swrevealviewcontroller Without Using Navigationcontroller