Replace regex match with attributed string and text - ios

Our app Api returns a field with custom format for user mentions just like:
"this is a text with mention for #(steve|user_id)".
So before display it on UITextView, need to process the text, find the pattern and replace with something more user friendly.
Final result would be "this is a text with mention for #steve" where #steve should have a link attribute with user_id. Basically the same functionality as Facebook.
First I've created an UITextView extension, with a match function for the regex pattern.
extension UITextView {
func processText(pattern: String) {
let inString = self.text
let regex = try? NSRegularExpression(pattern: pattern, options: [])
let range = NSMakeRange(0, inString.characters.count)
let matches = (regex?.matchesInString(inString, options: [], range: range))! as [NSTextCheckingResult]
let attrString = NSMutableAttributedString(string: inString, attributes:attrs)
//Iterate over regex matches
for match in matches {
//Properly print match range
print(match.range)
//A basic idea to add a link attribute on regex match range
attrString.addAttribute(NSLinkAttributeName, value: "\(schemeMap["#"]):\(must_be_user_id)", range: match.range)
//Still text it's in format #(steve|user_id) how could replace it by #steve keeping the link attribute ?
}
}
}
//To use it
let regex = ""\\#\\(([\\w\\s?]*)\\|([a-zA-Z0-9]{24})\\)""
myTextView.processText(regex)
This is what I have right now, but I'm stucked trying to get final result
Thanks a lot !

I changed your regex a bit, but got a pretty good result. Modified the code a little as well, so you can test it directly in Playgrounds.
func processText() -> NSAttributedString {
let pattern = "(#\\(([^|]*)([^#]*)\\))"
let inString = "this is a text with mention for #(steve|user_id1) and #(alan|user_id2)."
let regex = try? NSRegularExpression(pattern: pattern, options: [])
let range = NSMakeRange(0, inString.characters.count)
let matches = (regex?.matchesInString(inString, options: [], range: range))!
let attrString = NSMutableAttributedString(string: inString, attributes:nil)
print(matches.count)
//Iterate over regex matches
for match in matches.reverse() {
//Properly print match range
print(match.range)
//Get username and userid
let userName = attrString.attributedSubstringFromRange(match.rangeAtIndex(2)).string
let userId = attrString.attributedSubstringFromRange(match.rangeAtIndex(3)).string
//A basic idea to add a link attribute on regex match range
attrString.addAttribute(NSLinkAttributeName, value: "\(userId)", range: match.rangeAtIndex(1))
//Still text it's in format #(steve|user_id) how could replace it by #steve keeping the link attribute ?
attrString.replaceCharactersInRange(match.rangeAtIndex(1), withString: "#\(userName)")
}
return attrString
}

Related

Swift regex for searching format specifiers in a string

I need help to write the swift regex to find any format specifier in a string.
Eg.
"I am %#. My age is %d and my height is %.02f."
I need to find all sub-strings in bold and replace them with 'MATCH'
Below is my code
var description = "I am %#. My age is %d and my height is %.02f. (%#)"
let pattern = "(%[#df])"
let regex = try NSRegularExpression(pattern: pattern, options: [])
let nsrange = NSRange(description.startIndex..<description.endIndex, in: description)
while let match = regex.firstMatch(in: description, options: [], range: nsrange) {
description = (description as NSString).replacingCharacters(in: match.range, with: "MATCH")
}
print(description)
and output
I am MATCH. My age is MATCH and my height is %.02f. (%#)
It did not find %.02f and last %# with paranthesis.
Thanks in advance!
First of all you have to replace the matches reversed otherwise you will run into index trouble.
A possible pattern is
%([.0-9]+)?[#df]
it considers also the (optional) decimal places specifier.
var description = "I am %#. My age is %d and my height is %.02f. (%#)"
let pattern = "%([.0-9]+)?[#df]"
let regex = try NSRegularExpression(pattern: pattern)
let nsrange = NSRange(description.startIndex..., in: description)
for match in regex.matches(in: description, range: nsrange).reversed() {
let range = Range(match.range, in: description)!
description.replaceSubrange(range, with: "MATCH")
}
print(description)

Manually formatting url starting with http or https in string for UITextView of iOS/Xcode

I would like to select a bunch of urls only starting with http or https from a string. In UITextView, .dataDetectorTypes can be set to .link for making all urls into blue underlined text.
For example, from "www.google.com and https://www.gogole.com and http://www.google.com as well as "google.com" I would like to make only url starting with https or http into blue underlined text and keep them in the same original sentence, if not in a new sentence with modified selected urls.
Is it possible for such approach? Or which way i could implement that.?
A way to do so:
Use a NSMutableAttributedString.
Use NSDataDetector to find all the links.
Enumerate (enumerateMatches(in:options:range:using:)) and edit according to your rule if you want to add or not the NSAttributedStringKey.link.
let initialString = "www.google.com and https://www.gogole.com and http://www.google.com as well as \"google.com\""
let linkDetector = try! NSDataDetector(types: NSTextCheckingResult.CheckingType.link.rawValue)
let attributedString = NSMutableAttributedString(string: initialString)
linkDetector.enumerateMatches(in: attributedString.string,
options: [],
range: NSRange(location: 0, length: attributedString.string.utf16.count)) { (match, flags, stop) in
if let match = match, match.resultType == NSTextCheckingResult.CheckingType.link, let url = match.url {
if let range = Range(match.range, in: attributedString.string) {
let substring = attributedString.string[range]
if substring.hasPrefix("http") {
attributedString.addAttribute(.link, value: url, range: match.range)
}
}
}
}
I used the test substring.hasPrefix("http"), but you can use the one you want.
Output:
attributedString:
www.google.com and {
}https://www.gogole.com{
NSLink = "https://www.gogole.com";
} and {
}http://www.google.com{
NSLink = "http://www.google.com";
} as well as "google.com"{
}

Use regex to match emojis as well as text in string

I am trying to find the range of specific substrings of a string. Each substring begins with a hashtag and can have any character it likes within it (including emojis). Duplicate hashtags should be detected at distinct ranges. A kind user from here suggested this code:
var str = "The range of #hashtag should be different to this #hashtag"
let regex = try NSRegularExpression(pattern: "(#[A-Za-z0-9]*)", options: [])
let matches = regex.matchesInString(str, options:[], range:NSMakeRange(0, str.characters.count))
for match in matches {
print("match = \(match.range)")
}
However, this code does not work for emojis. What would be the regex expression to include emojis? Is there a way to detect a #, followed by any character up until a space/line break?
Similarly as in Swift extract regex matches,
you have to pass an NSRange to the match functions, and the
returned ranges are NSRanges as well. This can be achieved
by converting the given text to an NSString.
The #\S+ pattern matches a # followed by one or more
non-whitespace characters.
let text = "The 😀range of #hashtag🐶 should 👺 be 🇩🇪 different to this #hashtag🐮"
let nsText = text as NSString
let regex = try NSRegularExpression(pattern: "#\\S+", options: [])
for match in regex.matchesInString(text, options: [], range: NSRange(location: 0, length: nsText.length)) {
print(match.range)
print(nsText.substringWithRange(match.range))
}
Output:
(15,10)
#hashtag🐶
(62,10)
#hashtag🐮
You can also convert between NSRange and Range<String.Index>
using the methods from NSRange to Range<String.Index>.
Remark: As #WiktorStribiżew correctly noticed, the above pattern
will include trailing punctuation (commas, periods, etc). If
that is not desired then
let regex = try NSRegularExpression(pattern: "#[^[:punct:][:space:]]+", options: [])
would be an alternative.

Add a string at the end of a string for a particular string inside a String

I have a big string that contains several strings like the following:
http://website.com/image.jpg and I want to add ?w=100 to each of them.
So far I thought about creating a regular expression where every time I would encounter that string, it would replace it by the same string with the ?w=100 added to it.
Here is an extension of String to replace the string:
public func stringByReplacingMatches(matchRegEx: String, withMatch: String) -> String {
do {
let regEx = try NSRegularExpression(pattern: matchRegEx, options: NSRegularExpressionOptions())
return regEx.stringByReplacingMatchesInString(self, options: NSMatchingOptions(), range: NSMakeRange(0, self.characters.count), withTemplate: withMatch)
} catch {
fatalError("Error in the regular expression: \"\(matchRegEx)\"")
}
}
And this is how I use it:
myString.stringByReplacingMatches("https:\\/\\/website\\.com\\/[\\w\\D]*.jpg", withMatch: "https:\\/\\/website\\.com\\/[\\w\\D]*.jpg?w=100"))
Problem, this is what I get after replacing the string:
https://website.com/[wD]*.jpg?w=100
How can I solve my problem?
You need to learn about capture groups:
myString.stringByReplacingMatches("(https:\\/\\/website\\.com\\/[\\w\\D]*.jpg)", withMatch: "$1?w=100")
Oh and don't forget to save the return value
let fixedString = myString.stringByReplacingMatches("(https:\\/\\/website\\.com\\/[\\w\\D]*.jpg)", withMatch: "$1?w=100")
myString will not change, but fixedString will have all the updates.
UPDATE
[\\w\\D]* matches any character. You need to craft a more specific regular expression.
For the input "https://website.com/x.jpg" "https://website.com/x.jpg", the regex \w works.
var myString = "\"https://website.com/x.jpg\" \"https://website.com/x.jpg\""
myString = myString.stringByReplacingMatches("(https:\\/\\/website\\.com\\/\\w*.jpg)", withMatch: "$1?w=100")
But this will not work for every possible URL.
You should call it as
myString.stringByReplacingMatches("https:\\/\\/website\\.com\\/[\\w\\D]*.jpg", withMatch: "$0?w=100"))
The template can use $0 to show the matching string. You should not repeat the matching regular expression there.
do {
let regex = try NSRegularExpression(pattern: "http:\\/\\/[a-z]*.[a-z]*\\/[a-z]*.[a-z]*", options: .CaseInsensitive)
var newString = regex.stringByReplacingMatchesInString(a, options: NSMatchingOptions(rawValue: 0), range: NSMakeRange(0, a.characters.count), withTemplate: "$0?w=100")
print(newString)
}

Return range with first and last character in string

I have a string: "Hey #username that's funny". For a given string, how can I search the string to return all ranges of string with first character # and last character to get the username?
I suppose I can get all indexes of # and for each, get the substringToIndex of the next space character, but wondering if there's an easier way.
If your username can contain only letters and numbers, you can use regular expression for that:
let s = "Hey #username123 that's funny"
if let r = s.rangeOfString("#\\w+", options: NSStringCompareOptions.RegularExpressionSearch) {
let name = s.substringWithRange(r) // #username123"
}
#Vladimir's answer is correct, but if you're trying to find multiple occurrences of "username", this should also work:
let s = "Hey #username123 that's funny"
let ranges: [NSRange]
do {
// Create the regular expression.
let regex = try NSRegularExpression(pattern: "#\\w+", options: [])
// Use the regular expression to get an array of NSTextCheckingResult.
// Use map to extract the range from each result.
ranges = regex.matchesInString(s, options: [], range: NSMakeRange(0, s.characters.count)).map {$0.range}
}
catch {
// There was a problem creating the regular expression
ranges = []
}
for range in ranges {
print((s as NSString).substringWithRange(range))
}

Resources