How to convert AttributedString to NSMutableString Swift? - swift

I have HTML and was converted to AttributedString. Now, I need to change the generated Attributed string's font but I'm having a hard time retaining the style(Bold, Italic or Regular).
I found a solution but the problem is I don't know how to use it. They using NSMutableAttributedString as extension. I pasted my code how did I convert and the supposed solution at the bottom.
Thank you.
extension String {
var htmlToAttributedString: NSAttributedString? {
guard let data = data(using: .utf8) else { return NSAttributedString() }
do {
return try NSAttributedString(data: data, options: [.documentType: NSAttributedString.DocumentType.html, .characterEncoding:String.Encoding.utf8.rawValue], documentAttributes: nil)
} catch {
return NSAttributedString()
}
}
}
import Foundation
struct Service: Codable {
var id: Int
var name: String?
var price: String?
var description: String?
var subtitle: String?
var bodyPreview: String?
var featuredImage: String? // For FindAll
var imageList: [String]? // For FindByID
private enum CodingKeys: String, CodingKey {
case id
case name
case price
case subtitle
case description
case bodyPreview = "body_preview"
case featuredImage = "featured_image_url"
case imageList = "images_url"
}
}
class ServiceDetailViewController: UIViewController {
private var service: Service?
private func showServiceDetails() {
detailLabel.attributedText = service?.description?.htmlToAttributedString
collectionView.reloadData()
startCollectionViewTimer()
}
}
Manmal's solution:
extension NSMutableAttributedString {
func setFontFace(font: UIFont, color: UIColor? = nil) {
beginEditing()
self.enumerateAttribute(
.font,
in: NSRange(location: 0, length: self.length)
) { (value, range, stop) in
if let f = value as? UIFont,
let newFontDescriptor = f.fontDescriptor
.withFamily(font.familyName)
.withSymbolicTraits(f.fontDescriptor.symbolicTraits) {
let newFont = UIFont(
descriptor: newFontDescriptor,
size: font.pointSize
)
removeAttribute(.font, range: range)
addAttribute(.font, value: newFont, range: range)
if let color = color {
removeAttribute(
.foregroundColor,
range: range
)
addAttribute(
.foregroundColor,
value: color,
range: range
)
}
}
}
endEditing()
}
}

let attriString = NSAttributedString(string:"attriString", attributes:
[NSAttributedString.Key.foregroundColor: UIColor.lightGray,
NSAttributedString.Key.font: AttriFont])

You can simply create a similar extension to return a mutable attributed string:
extension String {
var htmlToMutableAttributedString: NSMutableAttributedString? {
do {
return try .init(data: Data(utf8), options: [.documentType: NSAttributedString.DocumentType.html, .characterEncoding: String.Encoding.utf8.rawValue], documentAttributes: nil)
} catch {
print(error)
return nil
}
}
}

Related

Add multiple Structs to one UserDefaults entity

I have my UserDefaults like this
fileprivate enum userdefaultKeys: String {
userSearchHistory = "userSearchHistory",
}
extension UserDefaults {
static func getUserSearchHistory() -> SearchHistory? {
let data = self.standard.data(forKey: userdefaultKeys.userSearchHistory.rawValue)
return SearchHistory.decode(json: data)
}
static func setUserSearchHistory(userSearchHistory: SearchHistory?) {
guard let json: Any = userSearchHistory?.json else { return }
self.standard.set(json, forKey: userdefaultKeys.userSearchHistory.rawValue)
}
}
And I'm saving this data to the UserDefaults
struct SearchHistory: Codable {
let type: SearchHistoryEnum
let name: String
let corpNo: String
let storeNo: String
let long: Double
let lat: Double
}
enum SearchHistoryEnum: Codable {
case storeSearch
case jsonSearch
}
let historySearch = SearchHistory(type: SearchHistoryEnum.storeSearch, name: store?.storename ?? "", corpNo: store?.corpno ?? "", storeNo: store?.storeno ?? "", long: longtitude, lat: latitude)
UserDefaults.setUserSearchHistory(userSearchHistory: historySearch)
This is okay, but it saves only one instance of SearchHistory in the time. I would like to have max 5. When 6th instance comes, I would like to delete the most old one
First of all your enum doesn't compile, you probably mean
fileprivate enum UserdefaultKeys: String {
case userSearchHistory
}
It's not necessary to specify the raw value.
Second of all I highly recommend not to fight the framework and to conform to the UserDefaults pattern to overload the getter and setter. I don't know your special methods for decoding and encoding, this is a simple implementation with standard JSONDecoder and JSONEncoder
extension UserDefaults {
func searchHistory(forKey key: String = UserdefaultKeys.userSearchHistory.rawValue) -> SearchHistory? {
guard let data = data(forKey: key) else { return nil }
return try? JSONDecoder().decode(SearchHistory.self, from: data)
}
func searchHistory(forKey key: String = UserdefaultKeys.userSearchHistory.rawValue) -> [SearchHistory]? {
guard let data = data(forKey: key) else { return nil }
return try? JSONDecoder().decode([SearchHistory].self, from: data)
}
func set(_ value: SearchHistory, forKey key: String = UserdefaultKeys.userSearchHistory.rawValue) {
guard let data = try? JSONEncoder().encode(value) else { return }
set(data, forKey: key)
}
func set(_ value: [SearchHistory], forKey key: String = UserdefaultKeys.userSearchHistory.rawValue) {
guard let data = try? JSONEncoder().encode(value) else { return }
set(data, forKey: key)
}
}
The benefit is the syntax is always the same for a single item or for an array
let historySearch = SearchHistory(type: SearchHistoryEnum.storeSearch, name: store?.storename ?? "", corpNo: store?.corpno ?? "", storeNo: store?.storeno ?? "", long: longtitude, lat: latitude)
UserDefaults.standard.set(historySearch)
or
UserDefaults.standard.set([historySearch])
The code to delete the oldest put in the method to write the data.

NSAttribute Doesn't Make Bold Swift

I am trying to make fullname bold with boldFullName func. But obviously, it does not make any change on it. I believe that casting to string is deleting mutableString features. How can I avoid it without returning NSAttributedString
class NewNotificationModel: Serializable {
var fromUser: NewNotificationFromUserModel!
}
class NewNotificationFromUserModel: Serializable {
var name: String!
var lastName: String!
}
final class NewNotificationViewModel {
// MARK: Properties
var notificationModel: NewNotificationModel!
private(set) var fullName: String!
init(notificationModel: NewNotificationModel) {
self.notificationModel = notificationModel
self.fullName = boldFullName(getFullName())
private func getFullName() -> String {
guard let fromUser = notificationModel.fromUser, let name = fromUser.name, let lastname = fromUser.lastName else { return "" }
return name + " " + lastname
}
func boldFullName(_ fullname: String) -> String {
let range = NSMakeRange(0, getFullName().count)
let nonBoldFontAttribute = [NSAttributedString.Key.font:UIFont.sfProTextSemibold(size: 16)]
let boldFontAttribute = [NSAttributedString.Key.font:UIFont.catamaranBold(size: 20)]
let boldString = NSMutableAttributedString(string: getFullName() as String, attributes:nonBoldFontAttribute)
boldString.addAttributes(boldFontAttribute, range: range)
return boldString.mutableString as String
}
}
And I am using this fullname in table cell as below
class NewNotificationTableViewCell: UITableViewCell, Reusable, NibLoadable {
#IBOutlet weak var messageLbl: UILabel!
messageLbl.text = NewNotificationTableViewCell.configureText(model: model)
My configureText func is
private static func configureText(model: NewNotificationViewModel) -> String {
guard let type = model.type else { return "" }
switch NotificationType.init(rawValue: type) {
String(format:"new_notification.group.request.want.join.text_%#_%#".localized, model.fullName, model.groupName ?? "")
case .mention?: return String(format:"new_notification.post.mention_%#".localized, model.fullName)
But those .fullName does not do anything about bolding fullName.
Edited as NSAttributedString but this gives error
case .internalCommunicationMessage?: return NSAttributedString(format:("new_notification.announcement.text_%#".localized), args: NSAttributedString(string: model.groupName ?? ""))
with this extension.
public extension NSAttributedString {
convenience init(format: NSAttributedString, args: NSAttributedString...) {
let mutableNSAttributedString = NSMutableAttributedString(attributedString: format)
args.forEach { (attributedString) in
let range = NSString(string: mutableNSAttributedString.string).range(of: "%#")
mutableNSAttributedString.replaceCharacters(in: range, with: attributedString)
}
self.init(attributedString: mutableNSAttributedString)
}
}
Cannot convert value of type 'String' to expected argument type 'NSAttributedString'
String doesn't contain attributes, you do need to return a NSAttributedString from your function.
What you can do instead is assigning the attributed string to the attributedText property of your UILabel. Documentation here
Example (after updating your function to return a NSAttributedString):
messageLbl.attributedText = NewNotificationTableViewCell.configureText(model: model)
You need to assign messageLbl.attributedText
func boldFullName(_ fullname: String) -> NSAttributedString {
let range = NSMakeRange(0, getFullName().count)
let nonBoldFontAttribute = [NSAttributedString.Key.font:UIFont.sfProTextSemibold(size: 16)]
let boldFontAttribute = [NSAttributedString.Key.font:UIFont.catamaranBold(size: 20)]
let boldString = NSMutableAttributedString(string: getFullName() as String, attributes:nonBoldFontAttribute)
boldString.addAttributes(boldFontAttribute, range: range)
return boldString.mutableString
}
The main problem is you are trying to give attributedString to text property which is not gonna effect on the UILabel . You must change some part of your code like :
private(set) var fullName: String!
to :
private(set) var fullName: NSMutableAttributedString!
And
func boldFullName(_ fullname: String) -> String {
let range = NSMakeRange(0, getFullName().count)
let nonBoldFontAttribute = [NSAttributedString.Key.font:UIFont.sfProTextSemibold(size: 16)]
let boldFontAttribute = [NSAttributedString.Key.font:UIFont.catamaranBold(size: 20)]
let boldString = NSMutableAttributedString(string: getFullName() as String, attributes:nonBoldFontAttribute)
boldString.addAttributes(boldFontAttribute, range: range)
return boldString.mutableString as String
}
to:
func boldFullName(_ fullname: String) -> NSMutableAttributedString {
let range = NSMakeRange(0, getFullName().count)
let nonBoldFontAttribute = [NSAttributedString.Key.font:UIFont.systemFont(ofSize: 10)]
let boldFontAttribute = [NSAttributedString.Key.font:UIFont.systemFont(ofSize: 30)]
let boldString = NSMutableAttributedString(string: getFullName() as String, attributes:nonBoldFontAttribute)
boldString.addAttributes(boldFontAttribute, range: range)
return boldString
}
And last when you call use attributedText instead of string
messageLbl.attributedText = ...
with this extension
convenience init(format: NSAttributedString, args: NSAttributedString...) {
let mutableNSAttributedString = NSMutableAttributedString(attributedString: format)
args.forEach { (attributedString) in
let range = NSString(string: mutableNSAttributedString.string).range(of: "%#")
mutableNSAttributedString.replaceCharacters(in: range, with: attributedString)
}
self.init(attributedString: mutableNSAttributedString)
}
I modified my switch cases as below
case .internalCommunicationMessage?:
let content = NSAttributedString(string:"new_notification.announcement.text_%#".localized)
let gn = NSAttributedString(string: model.groupName ?? "", attributes: [.font: UIFont.sfProTextMedium(size: size),
.kern: -0.26])
return NSAttributedString(format: content, args: gn)
the changed return type of configureText
configureText(model: NewNotificationViewModel) -> NSAttributedString
deleted boldFullName function and changed fullName type back to String
and finally inserted as below to label.
messageLbl.attributedText = NewNotificationTableViewCell.configureText(model: model)

How to extract Hashtags from text using SwiftUI?

Is there any way to find Hashtags from a text with SwiftUI?
This is how my try looks like:
calling the function like this : Text(convert(msg.findMentionText().joined(separator: " "), string: msg)).padding(.top, 8)
.
But it does not work at all.
My goal something like this:
extension String {
func findMentionText() -> [String] {
var arr_hasStrings:[String] = []
let regex = try? NSRegularExpression(pattern: "(#[a-zA-Z0-9_\\p{Arabic}\\p{N}]*)", options: [])
if let matches = regex?.matches(in: self, options:[], range:NSMakeRange(0, self.count)) {
for match in matches {
arr_hasStrings.append(NSString(string: self).substring(with: NSRange(location:match.range.location, length: match.range.length )))
}
}
return arr_hasStrings
}
}
func convert(_ hashElements:[String], string: String) -> NSAttributedString {
let hasAttribute = [NSAttributedString.Key.foregroundColor: UIColor.orange]
let normalAttribute = [NSAttributedString.Key.foregroundColor: UIColor.black]
let mainAttributedString = NSMutableAttributedString(string: string, attributes: normalAttribute)
let txtViewReviewText = string as NSString
hashElements.forEach { if string.contains($0) {
mainAttributedString.addAttributes(hasAttribute, range: txtViewReviewText.range(of: $0))
}
}
return mainAttributedString
}
You need to initailize Text() with a String, but instead you are attempting to initialize it with an Array of String.
You could either just display the first one if the array is not empty:
msg.findMentionText().first.map { Text($0) }
Or you could join the elements array into a single String:
Text(msg.findMentionText().joined(separator: " "))

UITextView Attributed text with 2 links, each with different colors not working

I want to display in an attributed string 2 links, each link with a different color. I do not understand how to do that. It will always set just one color. I've been struggling with this for days and still can't figure out how to make it work. Does anybody know? I can set two colors but not for links! All links are the same color.
This is my whole implementation: (UPDATE)
var checkIn = ""
var friends = ""
//MARK: Change Name Color / Font / Add a second LABEL into the same label
func setColorAndFontAttributesToNameAndCheckIn() {
let nameSurname = "\(postAddSetup.nameSurname.text!)"
checkIn = ""
friends = ""
if selectedFriends.count == 0 {
print("we have no friends...")
friends = ""
} else if selectedFriends.count == 1 {
print("we have only one friend...")
friends = ""
friends = " is with \(self.firstFriendToShow)"
} else if selectedFriends.count > 1 {
print("we have more than one friend...")
friends = ""
friends = " is with \(self.firstFriendToShow) and \(self.numberOfFriendsCount) more"
}
if checkIn == "" {
checkIn = ""
}
var string = postAddSetup.nameSurname.text
string = "\(nameSurname)\(friends)\(checkIn) "
let attributedString = NSMutableAttributedString(string: string!)
attributedString.addAttribute(NSFontAttributeName, value: UIFont.boldSystemFont(ofSize: 14), range: (string! as NSString).range(of: nameSurname))
attributedString.addAttribute(NSFontAttributeName, value: UIFont.systemFont(ofSize: 13), range: (string! as NSString).range(of: checkIn))
attributedString.addAttribute(NSFontAttributeName, value: UIFont.systemFont(ofSize: 13), range: (string! as NSString).range(of: friends))
attributedString.addLink("checkIn", linkColor: UIColor.darkGray, text: checkIn)
attributedString.addLink("tagFriends", linkColor: UIColor.red, text: friends)
//attributedString.addAttribute(NSLinkAttributeName, value: "checkIn", range: (string! as NSString).range(of: checkIn))
//attributedString.addAttribute(NSLinkAttributeName, value: "tagFriends", range: (string! as NSString).range(of: friends))
//postAddSetup.nameSurname.linkTextAttributes = [NSForegroundColorAttributeName:UIColor.redIWorkOut(), NSFontAttributeName: UIFont.systemFont(ofSize: 13)]
//attributedString.addAttribute(NSForegroundColorAttributeName, value: UIColor.darkGray, range: (string! as NSString).range(of: checkIn))
postAddSetup.nameSurname.attributedText = attributedString
print("atribute: \(attributedString)")
}
func string1Action() {
print("action for string 1...")
}
func string2Action() {
print("action for string 2...")
}
func textView(_ textView: UITextView, shouldInteractWith URL: URL, in characterRange: NSRange, interaction: UITextItemInteraction) -> Bool {
if URL.absoluteString == "string1" {
string1Action()
} else if URL.absoluteString == "string2" {
string2Action()
}
return false
}
extension NSMutableAttributedString {
func addLink(_ link: String, linkColor: UIColor, text: String) {
let pattern = "(\(text))"
let regex = try! NSRegularExpression(pattern: pattern,
options: NSRegularExpression.Options(rawValue: 0))
let matchResults = regex.matches(in: self.string,
options: NSRegularExpression.MatchingOptions(rawValue: 0),
range: NSRange(location: 0, length: self.string.characters.count))
for result in matchResults {
self.addAttribute(NSLinkAttributeName, value: link, range: result.rangeAt(0))
self.addAttribute(NSForegroundColorAttributeName, value: linkColor, range: result.rangeAt(0))
}
}
}
I have used in a project this NSMutableAttributedString extension adapted from this Article.
Using NSRegularExpression you can assign your respective color matching the range of your link text:
The extension:
extension NSMutableAttributedString {
func addLink(_ link: String, linkColor: UIColor, text: String) {
let pattern = "(\(text))"
let regex = try! NSRegularExpression(pattern: pattern,
options: NSRegularExpression.Options(rawValue: 0))
let matchResults = regex.matches(in: self.string,
options: NSRegularExpression.MatchingOptions(rawValue: 0),
range: NSRange(location: 0, length: self.string.characters.count))
for result in matchResults {
self.addAttribute(NSLinkAttributeName, value: link, range: result.rangeAt(0))
self.addAttribute(NSForegroundColorAttributeName, value: linkColor, range: result.rangeAt(0))
}
}
}
Edit:
Set a custom UITextView class to use this extension and using the delegate function shouldInteractWith url it’s possible to simulate the hyperlink logic of UITextView:
class CustomTextView: UITextView {
private let linksAttributes = [NSLinkAttributeName]
override func awakeFromNib() {
super.awakeFromNib()
let tapGest = UITapGestureRecognizer(target: self, action: #selector(self.onTapAction))
self.addGestureRecognizer(tapGest)
}
#objc private func onTapAction(_ tapGest: UITapGestureRecognizer) {
let location = tapGest.location(in: self)
let charIndex = self.layoutManager.characterIndex(for: location, in: self.textContainer, fractionOfDistanceBetweenInsertionPoints: nil)
if charIndex < self.textStorage.length {
var range = NSMakeRange(0, 0)
for linkAttribute in linksAttributes {
if let link = self.attributedText.attribute(linkAttribute, at: charIndex, effectiveRange: &range) as? String {
guard let url = URL(string: link) else { return }
_ = self.delegate?.textView?(self, shouldInteractWith: url, in: range, interaction: .invokeDefaultAction)
}
}
}
}
}
How to use:
attributedString.addLink(yourLinkUrl, linkColor: yourLinkColor, text: yourLinkText)
let textView = CustomTextView()
textView.attributedText = attributedString

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