let fullString = "Hello world, there are \(string(07)) continents and \(string(195)) countries."
let range = [NSMakeRange(24,2), NSMakeRange(40,3)]
Need to find the NSRange for numbers in the entire full string and there is a possibility that both numbers can be same. Currently hard coding like shown above, the message can be dynamic where hard coding values will be problematic.
I have split the strings and try to fetch NSRange
since there is a possibility of same value. like stringOne and stringTwo.
func findNSMakeRange(initialString:String, fromString: String) {
let fullStringRange = fromString.startIndex..<fromString.endIndex
fromString.enumerateSubstrings(in: fullStringRange, options: NSString.EnumerationOptions.byWords) { (substring, substringRange, enclosingRange, stop) -> () in
let start = distance(fromString.startIndex, substringRange.startIndex)
let length = distance(substringRange.startIndex, substringRange.endIndex)
let range = NSMakeRange(start, length)
if (substring == initialString) {
print(substring, range)
}
})
}
Receiving errors like Cannot invoke distance with an argument list of type (String.Index, String.Index)
Anyone have any better solution ?
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.
In Swift 5.7 and later, you can use the new Regex
:
string.ranges(of: /\d+/)
.map { NSRange($0, in: string) }
.forEach {
attributedString.setAttributes(attributes, range: $0)
}
Or if you find the traditional regular expressions too cryptic, you can import RegexBuilder
, and you can use the new regex DSL:
string.ranges(of: Regex { OneOrMore(.digit) })
.map { NSRange($0, in: string) }
.forEach {
attributedString.setAttributes(attributes, range: $0)
}
Now, this pattern of mapping the Range<String.Index>
to a NSRange
is only necessary if you really need an NSRange
, which is rarely needed nowadays. E.g., if you just wanted to print the subranges, you might just do:
string.ranges(of: /\d+/)
.forEach { print(string[$0]) }
Or, if you needed to update a mutable AttributedString
on the basis of finding ranges within the original String
, you might map to a Range<AttributedString.Index>
:
string.ranges(of: /\d+/)
.compactMap { Range($0, in: attributedString) }
.forEach { attributedString[$0].foregroundColor = .blue }
But the key takeaway is that we can now use native Regex
literals surrounded by /
characters, like shown above.
In Swift versions prior to 5.7, one would use NSRegularExpression
. E.g.:
let range = NSRange(location: 0, length: string.count)
try! NSRegularExpression(pattern: "\\d+").enumerateMatches(in: string, range: range) { result, _, _ in
guard let range = result?.range else { return }
attributedString.setAttributes(attributes, range: range)
}
Because the regex backslash was inside a string, that has to be escaped by yet another backslash (which is why this has two backslashes in it). Or you can use extended string delimiters, replacing "\\d+"
with #"\d+"#
.
Personally, before Swift 5.7, I found 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) }
Resources: