How to find urls in NSAttributedString? [duplicate] - ios

This question already has answers here:
Detect UIWebView links without click
(2 answers)
Closed 7 days ago.
I have simple attributed string:
let string = NSAttributedString(string: "hello https://www.stackoverflow.com")
When I display that string I would like to see:
hello URL
where URL is clickable link and opens https://www.stackoverflow.com.
The link is not hardcoded, at the time when I replace it, I don't know how much, (if any) links exist there.
EDIT:
Look at the question and compare to the one marked as duplicated. It is NOT DUPLICATED. Please review it wisely and smart.

This should do the trick:
let linkDetector = try! NSDataDetector(types: NSTextCheckingResult.CheckingType.link.rawValue)
let str = "hello at https://www.stackoverflow.com or http://google.com ?"
let attrStr = NSMutableAttributedString(string: str)
let matches = linkDetector.matches(in: attrStr.string, range: NSRange(location: 0, length: attrStr.string.utf16.count))
matches.reversed().forEach { aMatch in //Use `reversed()` to avoid range issues
let linkRange = aMatch.range
let link = (attrStr.string as NSString).substring(with: linkRange) //Or use Range
//Here, you could modify the "link", and compute if needed myURLTitle, like URL(string: link)?.host ?? "myURLTitle"
let replacement = NSAttributedString(string: "myURLTitle", attributes: [.link: link])
attrStr.replaceCharacters(in: linkRange, with: replacement)
}
print(attrStr)

Related

Swift: Getting range of text that includes emojis [duplicate]

This question already has an answer here:
Swift Regex doesn't work
(1 answer)
Closed 5 years ago.
I'm trying to parse out "#mentions" from a user provided string. The regular expression itself seems to find them, but the range it provides is incorrect when emoji are present.
let text = "๐Ÿ˜‚๐Ÿ˜˜๐Ÿ™‚ #joe "
let tagExpr = try? NSRegularExpression(pattern: "#\\S+")
tagExpr?.enumerateMatches(in: text, range: NSRange(location: 0, length: text.characters.count)) { tag, flags, pointer in
guard let tag = tag?.range else { return }
if let newRange = Range(tag, in: text) {
let replaced = text.replacingCharacters(in: newRange, with: "[email]")
print(replaced)
}
}
When running this
tag = (location: 7, length: 2)
And prints out
๐Ÿ˜‚๐Ÿ˜˜๐Ÿ™‚ [email]oe
The expected result is
๐Ÿ˜‚๐Ÿ˜˜๐Ÿ™‚ [email]
NSRegularExpression (and anything involving NSRange) operates on UTF16 counts / indexes. For that matter, NSString.count is the UTF16 count as well.
But in your code, you're telling NSRegularExpression to use a length of text.characters.count. This is the number of composed characters, not the UTF16 count. Your string "๐Ÿ˜‚๐Ÿ˜˜๐Ÿ™‚ #joe " has 9 composed characters, but 12 UTF16 code units. So you're actually telling NSRegularExpression to only look at the first 9 UTF16 code units, which means it's ignoring the trailing "oe ".
The fix is to pass length: text.utf16.count.
let text = "๐Ÿ˜‚๐Ÿ˜˜๐Ÿ™‚ #joe "
let tagExpr = try? NSRegularExpression(pattern: "#\\S+")
tagExpr?.enumerateMatches(in: text, range: NSRange(location: 0, length: text.utf16.count)) { tag, flags, pointer in
guard let tag = tag?.range else { return }
if let newRange = Range(tag, in: text) {
let replaced = text.replacingCharacters(in: newRange, with: "[email]")
print(replaced)
}
}

Replace just bold word in string, Swift

I have a UILabel with text "hello world, hello". There are 2 hello words.
And I want to replace the only 'bold hello' to 'thanks' without bold.
I use this code:
uiLabel1.text = "hello world, hello"
let target = "hello"
let replace = "thanks"
uiLabel1.text.replacingOccurrences(of: target, with: replace, options:
NSString.CompareOptions.literal, range: nil)
And the result is: "thanks world, thanks"
The result I want: "hello world, thanks"
Okay, so there's probably an easier way to do this...
So, I went through the API (like super quick) and looked for something like lastIndexOf, which lead me on a little trail to String#range(of:options), which allows you to search backwards, hmmm, interesting.
This returns a Range<String.Index> ... okay, so how can I use that?! Hmm, maybe String#replacingOccurrences(of:with:options:range:) ๐Ÿค”
So, crack open a play ground and...
var str = "hello world, hello"
let lastIndexOf = str.range(of: "hello", options: .backwards)
str = str.replacingOccurrences(of: "hello", with: "thanks", options: .caseInsensitive, range: lastIndexOf)
str now equals "hello world, thanks"
Hi #MadProgrammer, your code is to replace the last hello word to thanks, right? But my question is to replace hello with the bold attribute, it may in the first, middle or at the end of a string.
Okay, so clearly we're missing some context...
Assuming, now, you're using a NSAttributedString, it becomes slightly more complicated
Building the string itself is not hard, figuring out how to find string components by attribute, a little more difficult.
Lucky for us, we have the Internet. So, the following is based on ideas I got from:
NSAttributedString by example
Detect whether a font is bold/italic on iOS?
One of the important things to remember when trying to solve an issue, you'll be lucky to find a single answer which does it all, instead, you need to break your issue down and focus on solving individual elements, and be prepared to go back to the start ๐Ÿ˜‰
So, once again, unto the play ground...
import UIKit
var str = "hello world, "
//let lastIndexOf = str.range(of: "hello", options: .backwards)
//str = str.replacingOccurrences(of: "hello", with: "thanks", options: .caseInsensitive, range: lastIndexOf)
extension UIFont {
var isBold: Bool {
return fontDescriptor.symbolicTraits.contains(.traitBold)
}
var isItalic: Bool {
return fontDescriptor.symbolicTraits.contains(.traitItalic)
}
}
// Just so I can see that the style ;)
let fontSize = CGFloat(24.0)
let boldAttrs = [
NSAttributedString.Key.font: UIFont.boldSystemFont(ofSize: fontSize),
NSAttributedString.Key.foregroundColor: UIColor.white // Playground
]
// Playground only
let plainAttrs = [
NSAttributedString.Key.foregroundColor: UIColor.white // Playground
]
let boldText = NSMutableAttributedString(string: "hello", attributes: boldAttrs)
let styledText = NSMutableAttributedString(string: str, attributes: plainAttrs)
let someMoreBoldText = NSMutableAttributedString(string: "not to be replaced", attributes: boldAttrs)
// Attributes can be combined with their appear together ;)
styledText.append(boldText)
styledText.append(NSMutableAttributedString(string: " ", attributes: plainAttrs))
styledText.append(someMoreBoldText)
styledText.append(NSMutableAttributedString(string: " ", attributes: plainAttrs))
styledText.append(boldText)
styledText.enumerateAttribute(NSAttributedString.Key.font, in: NSRange(0..<styledText.length)) { (value, range, stop) in
guard let font = value as? UIFont, font.isBold else {
return;
}
let subText = styledText.attributedSubstring(from: range)
guard subText.string == "hello" else {
return
}
styledText.replaceCharacters(in: range, with: "thanks")
}
styledText
Which outputs...
The important things for me are:
The style has not be changed
Only the individual "hello" values, which are bolded, have been changed
Here is the code. But actually this is hardcoded. If the target enclosed in between <b></b>, it will work.
var text = "hello world, <b>hello</b>"
let target = "hello"
let replace = "thanks"
text = text.replacingOccurrences(of: "<b>\(target)</b>", with: replace, options: .literal, range: nil) //hello world, thanks

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"{
}

In Swift, how can I detect an "emoji tag"? [duplicate]

This question already has answers here:
Find out if Character in String is emoji?
(17 answers)
Closed 6 years ago.
func getEmojiTags(text: String) -> [String] {
}
An emoji tag is a combination of two parts, with no spaces between them. If there is a space between them, then it is not an emojiTag.
a character which is an emoji
a string which is not an emoji
For example:
Hello, my name is Jason ๐Ÿ˜€ ๐Ÿธ how are you ?-> []
Hello, my name is ๐Ÿ˜€Jason -> [๐Ÿ˜€Jason]
Hello, my name is ๐Ÿ˜€ Jason -> []
I am going to the โ›ฑbeach with some ๐Ÿ™‰monkeys ๐Ÿ‘ -> [โ›ฑbeach, ๐Ÿ™‰monkeys]
Try using NSRegularExpression with emoji code ranges.
func emojiTags(str: String) -> [String] {
//A little bit simplified, you may need to define what are your "emoji".
//This is a subset defined in http://stackoverflow.com/a/36258684/6541007 .
let emojiCharacters = "\\U0001F600-\\U0001F64F\\U0001F300-\\U0001F5FF\\U0001F680-\\U0001F6FF\\u2600-\\u26FF"
let pattern = "[\(emojiCharacters)][^\(emojiCharacters)\\s]+"
let regex = try! NSRegularExpression(pattern: pattern, options: [])
let matches = regex.matchesInString(str, options: [], range: NSRange(0..<str.utf16.count))
return matches.map{(str as NSString).substringWithRange($0.range)}
}
let str1 = "Hello, my name is Jason ๐Ÿ˜€ ๐Ÿธ how are you ?"
print(emojiTags(str1)) //->[]
let str2 = "Hello, my name is ๐Ÿ˜€Jason"
print(emojiTags(str2)) //->["๐Ÿ˜€Jason"]
let str3 = "Hello, my name is ๐Ÿ˜€ Jason"
print(emojiTags(str3)) //->[]
let str4 = "I am going to the โ›ฑbeach with some ๐Ÿ™‰monkeys ๐Ÿ‘"
print(emojiTags(str4)) //->["โ›ฑbeach", "๐Ÿ™‰monkeys"]

Replace regex match with attributed string and text

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
}

Resources