replace NSTextAttachment that include an image to String - swift

Before, I changed specific strings to NSTextAttachment that include image to display custom emoticon.
String to NSTextAttachment code
{
guard
let original = self.attributedText
else { return }
let pattern = "\\[img src=(\\w+)\\]"
do{
let regex = try NSRegularExpression(pattern: pattern, options: [])
let matches = regex.matches(in: original.string, options : [], range : NSMakeRange(0, original.string.characters.count))
let attributeString = NSMutableAttributedString(attributedString: original)
for match in matches.reversed(){
let emoticonString = attributeString.attributedSubstring(from: match.rangeAt(1)).string
if let emoticonAndroid = Emoticon(rawValue: emoticonString),
let image = UIImage(named : "\(emoticonAndroid.convertFromAndroid().rawValue)_000"){
image.accessibilityIdentifier = emoticonAndroid.rawValue
let attributedImage = NSTextAttachment()
attributedImage.image = image
attributedImage.bounds = CGRect(x: 0, y: -8, width: 25, height: 25)
attributeString.beginEditing()
attributeString.replaceCharacters(in: match.rangeAt(0), with: NSAttributedString(attachment: attributedImage))
attributeString.endEditing()
}
}
self.attributedText = attributeString
}catch{
return
}
}
but, I need to replace NSTextAttachment to string to send message.
I used NSMutableAttributedString.replaceCharacters(in:with:) method. but, It can work with only one emoticon image.
one emoticon
two emoticons or more
how can I fix it?
NSTextAttachment to String code
{
if let original = self.attributedText{
let attributeString = NSMutableAttributedString(attributedString: original)
original.enumerateAttribute(NSAttachmentAttributeName, in: NSMakeRange(0, original.length), options: [], using: { attribute, range, _ in
if let attachment = attribute as? NSTextAttachment,
let image = attachment.image{
let str = "[img src=\(image.accessibilityIdentifier!)]"
attributeString.beginEditing()
attributeString.(in: range, with: str)
attributeString.endEditing()
}
})
self.attributedText = attributeString
return attributeString.string
}else{
return nil
}
}

Umm.. I solved this problem.
First : Count number of NSTextAttachment
var count = 0
self.attributedText.enumerateAttribute(NSAttachmentAttributeName, in : NSMakeRange(0, self.attributedText.length), options: [], using: { attribute, range, _ in
if let attachment = attribute as? NSTextAttachment,
let image = attachment.image{
count = count + 1
}
})
return count
Second : Replace NSTextAttachment with String and calculate the changed range. <- Repeat
for i in 0..<self.countOfNSTextAttachment(){
let attributedString = NSMutableAttributedString(attributedString: self.attributedText)
var count = 0
attributedString.enumerateAttribute(NSAttachmentAttributeName, in : NSMakeRange(0, attributedString.length), options: [], using: { attribute, range, _ in
if let attachment = attribute as? NSTextAttachment,
let image = attachment.image{
let str = "[img src=\(image.accessibilityIdentifier!)]"
if count == 0{
attributedString.beginEditing()
attributedString.replaceCharacters(in: range, with: NSAttributedString(string : str))
attributedString.endEditing()
self.attributedText = attributedString
}else{
return
}
count = count + 1
}
})
}
return self.attributedText.string
Result : result
Perfect!!

Related

Applying NSMutableAttributedString to a range of text

I have some text:
New Content - Published Today | 10 min read
I'd like to apply styles to everything after and including the pipe, so | 10 min read
I have tried the below but it has only styles the pipe itself.
func makeAttributedText(using baseString: String?) -> NSMutableAttributedString? {
guard let baseString = baseString else { return nil }
let attributedString = NSMutableAttributedString(string: baseString, attributes: nil)
let timeToReadRange = (attributedString.string as NSString).range(of: "|")
attributedString.setAttributes([NSAttributedString.Key.font: UIFont.boldSystemFont(ofSize: 18)], range: timeToReadRange)
return attributedString
}
Rather than getting the range of a single character get the index of the character and create a range from that index to the end of the string.
func makeAttributedText(using baseString: String?) -> NSMutableAttributedString? {
guard let baseString = baseString else { return nil }
let attributedString = NSMutableAttributedString(string: baseString, attributes: nil)
guard let timeToReadIndex = baseString.firstIndex(of: "|") else { return attributedString }
let timeToReadRange = NSRange(timeToReadIndex..., in: baseString)
attributedString.setAttributes([NSAttributedString.Key.font: UIFont.boldSystemFont(ofSize: 18)], range: timeToReadRange)
return attributedString
}
Note:
Swift has dedicated methods to convert Range<String.Index> to NSRange. There's no reason for the bridge cast to NSString

I need help removing underscore after formatting a string

When a string starts and ends with an underscore, I am making that string italic. After that, I am removing the underscores. This works fine if the string is like this "_hello_ world"
However, this doesn't work => "_hello_ world _happy_"
This is my regex => "\\_(.*?)\\_"
func applyItalicFormat(string: String) {
let matches = RegexPattern.italicRegex.matches(string)
for match in matches {
let mRange = match.range
self.addAttributes([NSAttributedStringKey.font : UIFont.latoMediumItalic(size: 15)],
range: mRange)
if let rangeObj = Range(NSMakeRange(mRange.location, mRange.upperBound), in: string) {
var sub = string.substring(with: rangeObj)
sub = sub.replacingOccurrences(of: "_", with: "")
print("sub is \(sub)")
replaceCharacters(in: mRange, with: sub)
} else {
}
}
}
Another Regex format, \\_(?:(?!_).)+\\_ and using map
var mySentence = "This is from _mcdonal_ mac _system_ which says that _below_ answer is one of the _easiest_ way"
var wholeText = NSMutableAttributedString()
override func viewDidLoad() {
super.viewDidLoad()
wholeText = NSMutableAttributedString(string: mySentence)
italicLbl.attributedText = matches(for: "\\_(?:(?!_).)+\\_", in: mySentence)
}
func matches(for regex: String, in text: String) -> NSAttributedString {
do {
let regex = try NSRegularExpression(pattern: regex)
let results = regex.matches(in: text, range: NSRange(text.startIndex..., in: text))
let _ = results.map { (val) in
wholeText.addAttributes([NSAttributedString.Key.font : UIFont.italicSystemFont(ofSize: 17)],
range: val.range)
var sub = String(text[Range(val.range, in: text)!])
sub = sub.replacingOccurrences(of: "_", with: " ")
wholeText.replaceCharacters(in: val.range, with: sub)
}
return wholeText
} catch let error {
print("invalid regex: \(error.localizedDescription)")
return wholeText
}
}
Screenshot
With small modifications, and use of range(at:) of the matches:
extension NSMutableAttributedString {
func applyItalicFormat(pattern: String) {
let regex = try! NSRegularExpression(pattern: pattern, options: [])
let matches = regex.matches(in: string, options: [], range: NSRange(location: 0, length: string.utf16.count))
let italicAttributes = [NSAttributedString.Key.font: UIFont.italicSystemFont(ofSize: 15)]
for match in matches.reversed() {
let textRange = match.range(at: 1)
let attributedTextToChange = NSMutableAttributedString(attributedString: self.attributedSubstring(from: textRange))
attributedTextToChange.addAttributes(italicAttributes, range: NSRange(location: 0, length: attributedTextToChange.length))
replaceCharacters(in: match.range, with: attributedTextToChange)
}
}
}
You don't need to replace the _, you have already the good range of the text alone without the underscores.
I use the matches.reversed(), because when you apply the first one, then the range of the second already found is not correct anymore (you remove twice _).
I prefer to extract the attributedString part to modify, modify it, and then replace it with the modified it.
I simplified some rest of the code.
Sample Test (usable in Playground):
let initialTexts = ["_hello_ world", "\n\n", "_hello_ world _happy_"]
let label = UILabel.init(frame: CGRect(x: 0, y: 0, width: 500, height: 500))
label.backgroundColor = .orange
label.numberOfLines = 0
let attr = NSMutableAttributedString()
for anInitialText in initialTexts {
let attributedStr = NSMutableAttributedString(string: anInitialText)
attributedStr.applyItalicFormat(pattern: "\\_(.*?)\\_")
attr.append(attributedStr)
}
label.attributedText = attr

How I can change text color for every 5 first words in label?

I get different text from API and I want change text color for every 5 first word. I try use range and attributes string, but I do something wrong and this not good work for me. How can i do it?
this is my code:
private func setMessageText(text: String) {
let components = text.components(separatedBy: .whitespacesAndNewlines)
let words = components.filter { !$0.isEmpty }
if words.count >= 5 {
let attribute = NSMutableAttributedString.init(string: text)
var index = 0
for word in words where index < 5 {
let range = (text as NSString).range(of: word, options: .caseInsensitive)
attribute.addAttribute(NSAttributedString.Key.foregroundColor, value: Colors.TitleColor, range: range)
attribute.addAttribute(NSAttributedString.Key.font, value: Fonts.robotoBold14, range: range)
index += 1
}
label.attributedText = attribute
} else {
label.text = text
}
}
enter image description here
It's more efficient to get the index of the end of the 5th word and add color and font once for the entire range.
And you are strongly discouraged from bridging String to NSString to get a subrange from a string. Don't do that. Use native Swift Range<String.Index>, there is a convenience API to convert Range<String.Index> to NSRange reliably.
private func setMessageText(text: String) {
let components = text.components(separatedBy: .whitespacesAndNewlines)
let words = components.filter { !$0.isEmpty }
if words.count >= 5 {
let endOf5thWordIndex = text.range(of: words[4])!.upperBound
let nsRange = NSRange(text.startIndex..<endOf5thWordIndex, in: text)
let attributedString = NSMutableAttributedString(string: text)
attributedString.addAttributes([.foregroundColor : Colors.TitleColor, .font : Fonts.robotoBold14], range: nsRange)
label.attributedText = attributedString
} else {
label.text = text
}
}
An alternative – more sophisticated – way is to use the dedicated API enumerateSubstrings(in:options: with option byWords
func setMessageText(text: String) {
var wordIndex = 0
var attributedString : NSMutableAttributedString?
text.enumerateSubstrings(in: text.startIndex..., options: .byWords) { (substring, substringRange, enclosingRange, stop) in
if wordIndex == 4 {
let endIndex = substringRange.upperBound
let nsRange = NSRange(text.startIndex..<endIndex, in: text)
attributedString = NSMutableAttributedString(string: text)
attributedString!.addAttributes([.foregroundColor : Colors.TitleColor, .font : Fonts.robotoBold14], range: nsRange)
stop = true
}
wordIndex += 1
}
if let attributedText = attributedString {
label.attributedText = attributedText
} else {
label.text = text
}
}

NSAttributedString get images and string in parts

I have an NSAttributedString with a mixture of String and NSTextAttachment with images in there. How would I extract an [AnyObject] array of the parts?
This worked for me in Swift 4:
extension UITextView {
func getParts() -> [AnyObject] {
var parts = [AnyObject]()
let attributedString = self.attributedText
let range = NSMakeRange(0, attributedString.length)
attributedString.enumerateAttributes(in: range, options: NSAttributedString.EnumerationOptions(rawValue: 0)) { (object, range, stop) in
if object.keys.contains(NSAttributedStringKey.attachment) {
if let attachment = object[NSAttributedStringKey.attachment] as? NSTextAttachment {
if let image = attachment.image {
parts.append(image)
} else if let image = attachment.image(forBounds: attachment.bounds, textContainer: nil, characterIndex: range.location) {
parts.append(image)
}
}
} else {
let stringValue : String = attributedString.attributedSubstring(from: range).string
if (!stringValue.trimmingCharacters(in: .whitespaces).isEmpty) {
parts.append(stringValue as AnyObject)
}
}
}
return parts
}
I figured it out you can iterate over all the attributedString and read if the object has an NSTextAttachmentAttributeName property. If not, assume it's a string.
extension UITextView {
func getParts() -> [AnyObject] {
var parts = [AnyObject]()
let attributedString = self.attributedText
let range = NSMakeRange(0, attributedString.length)
attributedString.enumerateAttributesInRange(range, options: NSAttributedStringEnumerationOptions(rawValue: 0)) { (object, range, stop) in
if object.keys.contains(NSAttachmentAttributeName) {
if let attachment = object[NSAttachmentAttributeName] as? NSTextAttachment {
if let image = attachment.image {
parts.append(image)
}else if let image = attachment.imageForBounds(attachment.bounds, textContainer: nil, characterIndex: range.location) {
parts.append(image)
}
}
}else {
let stringValue : String = attributedString.attributedSubstringFromRange(range).string
if !stringValue.isEmptyOrWhitespace() {
parts.append(stringValue)
}
}
}
return parts
}
}

Swift: fill AutoreleasingUnsafeMutablePointer<NSDictionary?>

I am DTCoreText to transform HTML to attributed text. Because I want to set the font up front, not afterwards as that would override all bold, italic etcetera tags I want to set the document attributes in the constructor. This constructors wants me to give a AutoreleasingUnsafeMutablePointer that more or less seems to be a NSDictionary? with & up front. Sort of. Only it doesn't let me set it in any way. I've tried .memory, tried to cast the dictionary in any possible way and it just doesn't accept any data.
let font = UIFont.systemFontOfSize(12)
let data = info.desc?.dataUsingEncoding(NSUTF8StringEncoding)
let attributes: NSMutableDictionary? = NSMutableDictionary()
attributes!.setObject(font, forKey: NSFontAttributeName)
var attributeRef: AutoreleasingUnsafeMutablePointer<NSDictionary?> = AutoreleasingUnsafeMutablePointer.null()
NSMutableAttributedString(HTMLData: data, documentAttributes: nil)
//attributeRef = *attributeDict
let attributedString = NSMutableAttributedString(HTMLData: data, documentAttributes:attributeRef)
let paragraphStyle: NSMutableParagraphStyle = NSMutableParagraphStyle()
paragraphStyle.lineBreakMode = NSLineBreakMode.ByWordWrapping;
let range = NSMakeRange(0, attributedString.length)
attributedString.addAttribute(NSParagraphStyleAttributeName, value: paragraphStyle, range: range)
lblMessage.attributedText = attributedString
You should not be using DTCoreText at this point; iOS now has native calls for this. Just say var dict = NSDictionary?() and pass &dict. Here's example code:
let s = "<html><body><h1>Howdy</h1><p>Hello</p></body></html>"
let d = s.dataUsingEncoding(NSUTF16StringEncoding, allowLossyConversion: false)
var dict = NSDictionary?()
let att = NSAttributedString(data: d!, options: [
NSDocumentTypeDocumentAttribute:NSHTMLTextDocumentType
], documentAttributes: &dict, error: nil)
println(att!)
println(dict!)
You'll see that this works perfectly well. Here is dict:
BottomMargin = 72;
Converted = "-1";
DocumentType = NSHTML;
LeftMargin = 90;
PaperMargin = "UIEdgeInsets: {72, 90, 72, 90}";
PaperSize = "NSSize: {612, 792}";
RightMargin = 90;
TopMargin = 72;
UTI = "public.html";
However, I usually pass nil because nothing is coming back in the second dictionary that I really care about.
To update fonts in generated HTML use the code similar to the following:
Swift 5
extension String {
/// Convert HTML to NSAttributedString
func convertHtml() -> NSAttributedString {
guard let data = data(using: .utf8) else { return NSAttributedString() }
if let attributedString = try? NSAttributedString(data: data, options: [.documentType: NSAttributedString.DocumentType.html], documentAttributes: nil) {
let string = NSMutableAttributedString(attributedString: attributedString)
// Apply text color
string.addAttributes([.foregroundColor: UIColor.text], range: NSRange(location: 0, length: attributedString.length))
// Update fonts
let regularFont = UIFont(name: Fonts.Regular, size: 13)! // DEFAULT FONT (REGUALR)
let boldFont = UIFont(name: Fonts.Bold, size: 13)! // BOLD FONT
/// add other fonts if you have them
string.enumerateAttribute(.font, in: NSMakeRange(0, attributedString.length), options: NSAttributedString.EnumerationOptions(rawValue: 0), using: { (value, range, stop) -> Void in
/// Update to our font
// Bold font
if let oldFont = value as? UIFont, oldFont.fontName.lowercased().contains("bold") {
string.removeAttribute(.font, range: range)
string.addAttribute(.font, value: boldFont, range: range)
}
// Default font
else {
string.addAttribute(.font, value: regularFont, range: range)
}
})
return string
}
return NSAttributedString()
}
}