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
Related
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)
I am having a string in that I have getting a tag </p> and I want to append </b> ahead of </p>
let str = "<p>A new week-long event is kicking off soon in Pokemon Go. Niantic is holding another Adventure Week beginning Tuesday, June 4, and it'll give players a chance to earn extra rewards, catch some rare Rock-types, and even find a couple of new Shiny Pokemon.</p><p>During Adventure Week, Geodude, Rhyhorn, Omanyte, Aron, Lileep, Anorith, and other Rock Pokemon will appear in the wild much more frequently than normal.</p>"
I want this string as:
let newStr = #"<b><p>A new week-long event is kicking off soon in Pokemon Go. Niantic is holding another Adventure Week beginning Tuesday, June 4, and it'll give players a chance to earn extra rewards, catch some rare Rock-types, and even find a couple of new Shiny Pokemon.</p></b><p>During Adventure Week, Geodude, Rhyhorn, Omanyte, Aron, Lileep, Anorith, and other Rock Pokemon will appear in the wild much more frequently than normal.</p>"#
If you want to replace all occurrences of a string with another string you can use replacingOccurrences(of:with:). If you want to replace the first occurrence find range of the substring and replace with new substring
if let range = str.range(of: "<p>") {
str.replaceSubrange(range, with: "<b><p>")
}
if let range = str.range(of: "</p>") {
str.replaceSubrange(range, with: "</b></p>")
}
Updated due comment (just first tag) -> See history if you want simpler edition
extension String {
enum TagRange {
case first
case last
case any
}
func addingHTMLTag(_ wrapperTag: String, around tagToWrap: String, witchIs tagRange: TagRange = .any) -> String {
let range: NSRange?
let regxPattern = "<\(tagToWrap)>(.+?)</\(tagToWrap)>"
let regx = try! NSRegularExpression(pattern: regxPattern, options: [])
let allRange = NSRange(location: 0, length: self.utf8.count)
switch tagRange {
case .first: range = regx.rangeOfFirstMatch(in: self, options: [], range: allRange)
case .last: range = regx.matches(in: self, options: [], range: allRange).last?.range
case .any: range = nil
}
if let range = range {
let openTagged = (self as NSString).replacingOccurrences(of: "<\(tagToWrap)>", with: "<\(wrapperTag)><\(tagToWrap)>", range: range)
let offsetRange = NSRange(location: range.location + (wrapperTag.count+2) , length: range.length)
return (openTagged as NSString).replacingOccurrences(of: "</\(tagToWrap)>", with: "</\(tagToWrap)></\(wrapperTag)>", range: offsetRange)
} else {
let openTagged = replacingOccurrences(of: "<\(tagToWrap)>", with: "<\(wrapperTag)><\(tagToWrap)>")
return openTagged.replacingOccurrences(of: "</\(tagToWrap)>", with: "</\(tagToWrap)></\(wrapperTag)>")
}
}
}
usage:
let newStr = str.addingHTMLTag("b", around: "p", witchIs: .first)
This method also checks if the tag you want to wrap around is valid. Maybe there is no closing tag or no opening tag or invalid opening and closing tags order. or maybe there is a<p> in the text itself! that you don't want to wrap.
let's say I have a string
var a = "#bb #cccc #ddddd\u{ef}"
and i am setting it to textview like this
let text = a.trimmingCharacters(in: .whitespacesAndNewlines)
let textRemoved = text?.replacingOccurrences(of: "\u{ef}", with: "", options: NSString.CompareOptions.literal, range:nil)
textView.text = textRemove
I am trying to remove the \u{ef} character here. But in textRemoved it is not happening. Please help me how to do it.
I am using Xcode 10. Looks like below Xcode version than 10 is working
fine. is it a bug of Xcode 10?
This is a late answer but I struggled to replace "\u{ef}" in string as well. During debugging when hovered over string it showed presence of \u{ef} but when print in description it only showed space.
let str = "\u{ef} Some Title"
print(str) //" Some Title"
I tried replacingOccurrences(of: "\u{ef}", with: "", options: NSString.CompareOptions.literal, range: nil).trimmingCharacters(in: .whitespaces) but it failed as well.
So I used below snippet and it worked like wonder.
let modifiedStr = str.replacingOccurrences(of: "\u{fffc}", with: "", options: NSString.CompareOptions.literal, range: nil).trimmingCharacters(in: .whitespaces)
print(modifiedStr) //"Some Title"
Hope this helps someone!!
i also faced same issue for "\u{e2}". i have searched a lot but unable to find any answer. then i have tried below code , which works for me.
var newString = ""
for char in strMainString.unicodeScalars{
if char.isASCII{
newString += String(char)
}
}
Hope that will also work for you too.
In Xcode 10 Playground, string replaces for \u{00EF} is working.
var a = "#bb #cccc #ddddd\u{ef}"
a = a.replacingOccurrences(of: "\u{00EF}", with: "")
I hope that will work for you.
I tried the following and it worked like a charm:
replacingOccurrences(of: "�", with: " ", options: NSString.CompareOptions.literal, range: nil)
e.g. 1
let text = "\u{ef}\u{ef}\u{ef}\u{ef}😇哦哦哦"
let text1 = text.replacingOccurrences(of: "\u{fffc}", with: "", options: String.CompareOptions.literal, range: nil)
let text2 = text.replacingOccurrences(of: "\u{ef}", with: "", options: String.CompareOptions.literal, range: nil).trimmingCharacters(in: .whitespaces)
runnable
<img src="https://i.stack.imgur.com/styVo.png"/>
e.g. 2
let strBefore = textDocumentProxy.documentContextBeforeInput
let strAfter = textDocumentProxy.documentContextAfterInput
var textInput = strBefore + strAfter
let textInput2 = textInput.replacingOccurrences(of: "\u{ef}", with: "", options: String.CompareOptions.literal, range: nil)
let textInput1 = textInput.replacingOccurrences(of: "\u{fffc}", with: "", options: String.CompareOptions.literal, range: nil).trimmingCharacters(in: .whitespaces)
runnable
<img src="https://i.stack.imgur.com/xGHtW.png"/>
Similar to question but with \u{e2} symbol (fix is the same):
\u{e2} is not a character rather subset of UTF8 plane which starts with 0xE2 byte.
So look here, E2 are general punctuation symbols.
There many symbols actually which started with \u{e2} but not limited to it and full char can be represented f.e. with e2 80 a8 bytes (line separator).
That explains why shown in Xcode \u{e2} can't be replaced with replacingOccurrences... function. In order to filter out correct symbol you have to know what exact symbol it is, f.e. by using the snippet below:
"\u{2028}&😲".forEach { (char) in
print(Data(char.utf8).map { String(format: "%02x", $0) }.joined(separator: " "))
}
it prints to console:
e2 80 a8
26
f0 9f 98 b2
which are byte representation for each symbol.
Next step is to filter your string, go here and search in 3d column your bytes and unicode code point value is what you need (first column) and write it in swift code like "\u{2028}\u{206A}..." (depending on your sorting).
The final function may look like:
func removingE2Symbols() -> String {
let specialChars = "\u{202A}\u{202C}"
return filter { !specialChars.contains($0) }
}
Try this
extension String {
var asciiString: String {
return String(self.unicodeScalars.filter{ $0.isASCII })
}
}
It,s working Please check again:
let a = "#bb #cccc #ddddd\u{ef}"
let text = a.trimmingCharacters(in: .whitespacesAndNewlines)
let textRemoved = text.replacingOccurrences(of: "\u{ef}", with: "", options: NSString.CompareOptions.literal, range:nil)
print(textRemoved)
I'm working on a macOS application. I need to syntax-highlight text that is placed over TextView (NSTextView) with a list of selected words. For simplicity, I'm actually testing the same feature on the iPhone Simulator. Anyway, a list of words to highlight comes as a form of an array. The following is what I have.
func HighlightText {
let tagArray = ["let","var","case"]
let style = NSParagraphStyle.defaultParagraphStyle().mutableCopy() as! NSMutableParagraphStyle
style.alignment = NSTextAlignment.Left
let words = textView.string!.componentsSeparatedByString(" ") // textView.text (UITextView) or textView.string (NSTextView)
let attStr = NSMutableAttributedString()
for i in 0..<words.count {
let word = words[i]
if HasElements.containsElements(tagArray,text: word,ignore: true) {
let attr = [
NSForegroundColorAttributeName: syntaxcolor,
NSParagraphStyleAttributeName: style,
]
let str = (i != words.count-1) ? NSAttributedString(string: word.stringByAppendingString(" "), attributes: attr) : NSAttributedString(string: word, attributes: attr)
attStr.appendAttributedString(str)
} else {
let attr = [
NSForegroundColorAttributeName: NSColor.blackColor(),
NSParagraphStyleAttributeName: style,
]
let str = (i != words.count-1) ? NSAttributedString(string: word.stringByAppendingString(" "), attributes: attr) : NSAttributedString(string: word, attributes: attr)
attStr.appendAttributedString(str)
}
}
textView.textStorage?.setAttributedString(attStr)
}
class HasElements {
static func containsElements(array:Array<String>,text:String,ignore:Bool) -> Bool {
var has = false
for str in array {
if str == text {
has = true
}
}
return has
}
}
The simple methodology here is to separate the entire string of text into words with a white space (" ") and puts each word in an array (words). The containsElements function simply tells whether or not the selected word contains one of the keywords in the array (tagArray). If it returns true, the word is put in an NSMutableAttributedString with a highlight color. Otherwise, it's put in the same attributed string with a plain color.
The problem with this simple methodology is that a separated word puts the last word and /n and the next word together. For example, if I have a string like
let base = 3
let power = 10
var answer = 1
, only the first 'let' will be highlighted as the code puts 3 and the next let together like '3\nlet.' If I separate any word containing \n with a fast enumeration, the code won't detect each new paragraph well. I appreciate any advice to make it better. Just FYI, I'm going to leave this topic open to both macOS and iOS.
Muchos thankos
Couple different options. String has a function called componentsSeparatedByCharactersInSet that allows you to separate by a character set you define. Unfortunately this won't work since you want to separate by \n which is more than one character.
You could split the words twice.
let firstSplit = textView.text!.componentsSeparatedByString(" ")
var words = [String]()
for word in firstSplit {
let secondSplit = word.componentsSeparatedByString("\n")
words.appendContentsOf(secondSplit)
}
But then you wouldn't have any sense of the line breaks.. You'd need to re add them back in.
Finally, the easiest hack is simply:
let newString = textView.text!.stringByReplacingOccurrencesOfString("\n", withString: "\n ")
let words = newString.componentsSeparatedByString(" ")
So basically you add your own spaces.
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
}