How to Find Multiple Nsrange for a String from Full String iOS Swift

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.

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

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

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:

  • Swift 5.7 and later:
    • WWDC 2022 video Meet Swift Regex
    • WWDC 2022 video Swift Regex: Beyond the basics
    • Hacking With Swift: Regular Expressions
  • Swift before 5.7:
    • Hacking With Swift: How to use regular expressions in Swift
    • NSHipster: Regular Expressions in Swift

Find NSRanges of all substrings splitted by new lines

In the closure create an NSRange from Range<String.Index>

let string = "hello\nhello"
let rangesSplittedByLines: [NSRange] = string.split(separator: "\n").map {
return NSRange(string.range(of: $0)!, in: string)
}

But there is another problem. The code returns [{0, 5}, {0, 5}] because range(of: returns always the range of the first match.

This fixes the problem, it slices the string by moving the start index to the position after the most recent match

let string = "hello\nhello"
var startIndex = string.startIndex
let rangesSplittedByLines: [NSRange] = string.split(separator: "\n").map {
let subString = string[startIndex...]
let swiftRange = subString.range(of: $0)!
let nsRange = NSRange(swiftRange, in: string)
startIndex = swiftRange.upperBound
return nsRange
}

And there is no reason to bridge String to NSString explicitly.

But as you want nsranges anyway Regular Expression like in Martin’s answer is much more efficient.

Swift: How to match ranged values (NSRange) array with full names Strings array?

This credit goes to @bewithyou for sure:

  for (mentionName, mentionRange) in zip(getMentionContactsFromComment(), setRangedMentionContactValues()) {

// print("(mentionName) : (mentionRange)")

    if gesture.didTapAttributedTextInLabel(label: parent.label, inRange: mentionRange) {
if mentionRange == NSRange(location: mentionRange.location, length: mentionName.count) {
let newMentionName = String(mentionName.dropFirst())
print(newMentionName)
}
}

}

How to detect multiple occurrences of a word within a string

This should get you an array of NSRanges with "playground"

let regex = try! NSRegularExpression(pattern: "playground", options: [.caseInsensitive])
let items = regex.matches(in: text, options: [], range: NSRange(location: 0, length: (text as NSString).length))
let ranges: [NSRange] = items.map{$0.range}

Then to get the strings:

let occurrences: [String] = ranges.map{String((text as NSString).substring(with:$0))}

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

Use NSAttributedString multiple times in one string

I found a way to do it:

I first check for the brackets, never bind what the content is. Once I've done that, I identify more precisely the stringiest. For example:

func regexMatchesToString(_ pattern: String, string: String) throws -> [String]? {
let regex = try NSRegularExpression(pattern: pattern)
let range = NSRange(string.startIndex..<string.endIndex, in: string)
let results = regex.matches(in: string, options: [], range: range)
return results.map {
String(string[Range($0.range, in: string)!])
}
}
func regexMatches(_ pattern: String, string: String) throws -> [NSTextCheckingResult]? {
let regex = try NSRegularExpression(pattern: pattern)
let range = NSRange(string.startIndex..<string.endIndex, in: string)
return regex.matches(in: string, options: [], range: range)
}

string = algAtCell //The text displayed in my UITableView
atStr = NSMutableAttributedString(string: string)

if let stringMatches1 = try? regexMatchesToString("\\((.*?)\\)", string: string) {
for triggerString in stringMatches1 {
print("String \(string)")
if triggerString == ("(R U R' U')") {
let matches = try? regexMatches("\\((R U R' U')\\)", string: string)
for match in matches! {
atStr.addAttribute(.foregroundColor, value: UIColor.init(displayP3Red: 255/255, green: 10/255, blue: 10/255, alpha: 1.0), range: match.range)
}

Hope this helps if anyone needs it.

iOS How to find multiple strings between 2 strings in a huge string?

I have overlooked the closing quote requirement. Well, in this case, the code with or without regex will be similar in length.

Here is a regex suggestion that basically extracts all substrings after "my_id": " and up to the next " or the end of string:

NSError *error = nil;
NSString *pattern = @"\"my_id\": \"([^\"]+)";
NSString *string = @"dsadasdasd\"my_id\": \"qwerr\"dcdscsdcds\"my_id\": \"oytuj\"adasddasddsddaS\"my_id\": \"sjghjgjg\"ddsfsdfsdf";
NSRange range = NSMakeRange(0, string.length);
NSRegularExpression *regex = [NSRegularExpression regularExpressionWithPattern:pattern options:0 error:&error];
NSArray *matches = [regex matchesInString:string options:0 range:range];
for (NSTextCheckingResult* match in matches) {
NSRange group1 = [match rangeAtIndex:1];
NSLog(@"group1: %@", [string substringWithRange:group1]);
}

See IDEONE demo



Related Topics



Leave a reply



Submit