Getting the range of links in attributed string - ios

I would like to find the range of links in attributed text, so I could apply custom underline only to the relevant words.
At the moment, the underline is under all of the text.
I want it to be only under the links.
The code is a bit complex as the requested underline is super customised.
import UIKit
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let text = "random text <a href='http://www.google.com'>http://www.google.com </a> more random text"
let storage = NSTextStorage()
let layout = UnderlineLayout()
storage.addLayoutManager(layout)
let container = NSTextContainer()
layout.addTextContainer(container)
let textView = UITextView(frame: CGRect(x: 30, y: 380, width: 300, height: 200), textContainer: container)
textView.isUserInteractionEnabled = true
textView.isEditable = false
textView.textContainerInset = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 0)
textView.attributedText = htmlStyleAttributeText(text: text)
textView.backgroundColor = UIColor.white
textView.textColor = UIColor.black
let underLineColor: UIColor = UIColor(red: 245/255, green: 190/255, blue: 166/255, alpha: 1)
let attributes = [NSAttributedString.Key.underlineStyle.rawValue: 0x15,
NSAttributedString.Key.underlineColor: underLineColor,
NSAttributedString.Key.font: UIFont.systemFont(ofSize: 25),
NSAttributedString.Key.baselineOffset:0] as! [NSAttributedString.Key : Any]
let rg = NSRange(location: 0, length: textView.attributedText!.string.count)
storage.addAttributes(attributes, range: rg)
view.addSubview(textView)
}
public func htmlStyleAttributeText(text: String) -> NSMutableAttributedString? {
if let htmlData = text.data(using: .utf8) {
let options: [NSAttributedString.DocumentReadingOptionKey: Any] = [NSAttributedString.DocumentReadingOptionKey.documentType: NSAttributedString.DocumentType.html, NSAttributedString.DocumentReadingOptionKey.characterEncoding: String.Encoding.utf8.rawValue]
let attributedString = try? NSMutableAttributedString(data: htmlData, options: options, documentAttributes: nil)
return attributedString
}
return nil
}
}
import UIKit
class UnderlineLayout: NSLayoutManager {
override func drawUnderline(forGlyphRange glyphRange: NSRange, underlineType underlineVal: NSUnderlineStyle, baselineOffset: CGFloat, lineFragmentRect lineRect: CGRect, lineFragmentGlyphRange lineGlyphRange: NSRange, containerOrigin: CGPoint) {
if let container = textContainer(forGlyphAt: glyphRange.location, effectiveRange: nil) {
let boundingRect = self.boundingRect(forGlyphRange: glyphRange, in: container)
let offsetRect = boundingRect.offsetBy(dx: containerOrigin.x, dy: containerOrigin.y)
let left = offsetRect.minX
let bottom = offsetRect.maxY
let width = offsetRect.width
let path = UIBezierPath()
path.lineWidth = 4
path.move(to: CGPoint(x: left, y: bottom))
path.addLine(to: CGPoint(x: left + width, y: bottom))
path.stroke()
}
}
}

With:
let attributedText = htmlStyleAttributeText(text: text)!
...
textView.attributedText = attributedText
Separate the attributes:
let underlinesAttributes: [NSAttributedString.Key: Any] = [.underlineStyle: 0x15,
.underlineColor: underLineColor]
let attributes: [NSAttributedString.Key: Any] = [.font: UIFont.systemFont(ofSize: 25),
.baselineOffset: 0]
Apply the "basic ones" to the whole text:
let wholeRange = NSRange(attributedText.string.startIndex..., in: attributedText.string)
storage.addAttributes(attributes, range: wholeRange)
We now enumerate looking for the links, and apply the effect for each one found:
attributedText.enumerateAttribute(.link, in: wholeRange, options: []) { (value, range, pointee) in
if value != nil {
storage.addAttributes(underlinesAttributes, range: range)
}
}

Related

Change attributed String dynamically

I have a TableViewCell in which I have a clickable textView:
let linkTextView: UITextView = {
let v = UITextView()
v.backgroundColor = .clear
v.textAlignment = .left
v.isScrollEnabled = false
let padding = v.textContainer.lineFragmentPadding
v.textContainerInset = UIEdgeInsets(top: 0, left: -padding, bottom: 0, right: -padding)
v.tintColor = .darkCustom
v.isEditable = false
v.isSelectable = true
v.translatesAutoresizingMaskIntoConstraints = false
return v
}()
let interactableText = NSMutableAttributedString(string: "Link öffnen")
In Cell I also have these two functions to style it and make it clickable:
func setupTextView(){
interactableText.addAttributes([.underlineStyle: NSUnderlineStyle.single.rawValue,
NSAttributedString.Key.font: UIFont(name: "AvenirNext-Medium", size: 15)!,
NSAttributedString.Key.underlineColor: UIColor.darkCustom],
range: NSRange(location: 0, length: interactableText.length))
interactableText.addAttribute(NSAttributedString.Key.link,
value: "https://www.google.de/?hl=de",
range: NSRange(location: 0, length: interactableText.length))
}
func textView(_ textView: UITextView, shouldInteractWith URL: URL, in characterRange: NSRange) -> Bool {
self.linkTappedCallback!(URL)
return false
}
This setup works. However it's not what I really want. I would like to be able to change the value of the link for each cell. I tried it like this in cellForRowAt:
print(currentWish.link)
cell.interactableText.addAttribute(NSAttributedString.Key.link,
value: currentWish.link,
range: NSRange(location: 0, length: cell.interactableText.length))
But when having it like this and dont set the link inside Cell the textView is no longer clickable. What am I missing here?
With the help of #elarcoiris I am now using this function and it works exactly the way I want it to:
extension UITextView {
func hyperLink(originalText: String, hyperLink: String, urlString: String) {
let style = NSMutableParagraphStyle()
style.alignment = .left
let attributedOriginalText = NSMutableAttributedString(string: originalText)
let linkRange = attributedOriginalText.mutableString.range(of: hyperLink)
let fullRange = NSMakeRange(0, attributedOriginalText.length)
attributedOriginalText.addAttribute(NSAttributedString.Key.link, value: urlString, range: linkRange)
attributedOriginalText.addAttribute(NSAttributedString.Key.paragraphStyle, value: style, range: fullRange)
attributedOriginalText.addAttribute(NSAttributedString.Key.foregroundColor, value: UIColor.darkCustom, range: fullRange)
attributedOriginalText.addAttribute(NSAttributedString.Key.underlineColor, value: UIColor.darkCustom, range: fullRange)
attributedOriginalText.addAttribute(NSAttributedString.Key.font, value: UIFont(name: "AvenirNext-Medium", size: 15)!, range: fullRange)
self.linkTextAttributes = [
kCTForegroundColorAttributeName: UIColor.darkCustom,
kCTUnderlineStyleAttributeName: NSUnderlineStyle.single.rawValue,
] as [NSAttributedString.Key : Any]
self.attributedText = attributedOriginalText
}
}
Usage in cellForRowAt:
let cell = tableView.dequeueReusableCell(withIdentifier: WhishCell.reuseID, for: indexPath) as! WhishCell
cell.linkTextView.hyperLink(originalText: "Link öffnen", hyperLink: "Link öffnen", urlString: currentWish.link)

Wrong height for UILabel when using custom lineSpacing and kern

I am getting wrong height for an UILabel if I use NSAttributedString that has custom kern and lineSpacing.
Here is how I set the custom kern and line spacing:
override func viewDidLoad() {
super.viewDidLoad()
let shortText = "Single line"
self.label.attributedText = self.getAttributedText(text: shortText, kern: 0.2, lineSpacing: 8)
self.label2.attributedText = self.getAttributedText(text: shortText, kern: 0, lineSpacing: 8)
}
private func getAttributedText(text: String, kern: CGFloat, lineSpacing: CGFloat) -> NSAttributedString {
let attributedString = NSMutableAttributedString(string: text)
let style = NSMutableParagraphStyle()
style.lineSpacing = lineSpacing
let attributes: [NSAttributedStringKey : Any] =
[.paragraphStyle : style,
.kern: kern]
attributedString.addAttributes(attributes,
range: NSMakeRange(0, attributedString.length))
return attributedString
}
And here is what I get:
The first label (the one that has custom kern), has its height wrong. It's exactly 8 points taller than it should be - that's the custom line height that I am using.
This only happens for single line labels. If I use text that is on a couple of lines, it works as expected.
This is a bug with NSAttributedStringKey.kern. As a workaround, you can calculate the number of lines of your UILabel with the suggestions in this answer. If it has one line only, set lineSpacing to 0.
private func getAttributedText(text: String, kern: CGFloat, lineSpacing: CGFloat) -> NSAttributedString {
let attributedString = NSMutableAttributedString(string: text)
let font = UIFont.systemFont(ofSize: 16)
let attributes: [NSAttributedStringKey : Any] = [.kern: kern,
.font: font]
attributedString.addAttributes(attributes, range: NSMakeRange(0, attributedString.length))
let maxSize = CGSize(width: [custom width], height: CGFloat.greatestFiniteMagnitude)
let sizeOfLabel = attributedString.boundingRect(with: maxSize, options: .usesLineFragmentOrigin, context: nil)
if sizeOfLabel.height > font.lineHeight {
let style = NSMutableParagraphStyle()
style.lineSpacing = lineSpacing
attributedString.addAttribute(.paragraphStyle, value: style, range: NSMakeRange(0, attributedString.length))
}
return attributedString
}

UITextContainerView hidding clickable link of UITextView

I made a clickable link in a UITextView using NSMutableAttributeString.
All it changes is that the text is highlighted
As we can see : floating over my UITextView there is a UIContainerView (I really don't know if its because of that.. I'm trying)
Here is my UIView code:
class InfoBox: UIView {
let Heading: UITextView = {
let textView = UITextView(frame: CGRect(x: 15, y: 0, width: 200, height: 35))
textView.font = UIFont.systemFont(ofSize: 20)
textView.textColor = UIColor.white
textView.isScrollEnabled = false
textView.backgroundColor = UIColor.clear
textView.isEditable = false
textView.isSelectable = true
return textView
}()
let TextContent: UITextView = {
let textView = UITextView(frame: CGRect(x: 15, y: 27, width: UIScreen.main.bounds.width, height: 30))
textView.font = UIFont.systemFont(ofSize: 17)
textView.textColor = UIColor.white
textView.isScrollEnabled = false
textView.backgroundColor = UIColor.clear
textView.isEditable = false
textView.isSelectable = true
return textView
}()}
The NSAttributedString code:
func transformText(text: String, underlined: Bool, linkURL: String) -> NSAttributedString {
let textRange = NSMakeRange(0, text.characters.count)
let attributedText = NSMutableAttributedString(string: text)
if underlined{
attributedText.addAttribute(NSUnderlineStyleAttributeName , value: NSUnderlineStyle.styleSingle.rawValue, range: textRange)
attributedText.addAttribute(NSUnderlineColorAttributeName , value: UIColor.lightGray, range: textRange)
}
attributedText.addAttribute(NSFontAttributeName , value: UIFont(name: "Helvetica-Light", size: 17)!, range: textRange)
attributedText.addAttribute(NSForegroundColorAttributeName , value: UIColor.lightGray, range: textRange)
if(linkURL != "")
{
let attrib = [NSLinkAttributeName: NSURL(string: linkURL)!]
attributedText.addAttributes(attrib, range: textRange)
}
return attributedText
}
And this is how it is called:
self.TelBox.TextContent.attributedText = transformText(text: self.TelBox.TextContent.text, underlined: true, linkURL: "https://www.google.fr")
Secondary question : is it possible to make a clickable link in a UITextView for a telephone number so that when clicked it calls that number? Did it with a UIButton .
I am not sure what is wrong with your UIContainerView, as far as i can see there's nothing wrong there.
Here's the method to make the link call a number:
func transformText(text: String, underlined: Bool, phoneNumber: String) -> NSAttributedString {
let textRange = NSMakeRange(0, text.characters.count)
let attributedText = NSMutableAttributedString(string: text)
if underlined{
attributedText.addAttribute(NSAttributedStringKey.underlineStyle , value: NSUnderlineStyle.styleSingle.rawValue, range: textRange)
attributedText.addAttribute(NSAttributedStringKey.underlineColor , value: UIColor.lightGray, range: textRange)
}
attributedText.addAttribute(NSAttributedStringKey.font , value: UIFont(name: "Helvetica-Light", size: 17)!, range: textRange)
attributedText.addAttribute(NSAttributedStringKey.foregroundColor , value: UIColor.lightGray, range: textRange)
if(phoneNumber != "")
{
let attrib = [
NSAttributedStringKey.link: URL(string: "tel://" + phoneNumber.replacingOccurrences(of: " ", with: ""))]
attributedText.addAttributes(attrib, range: textRange)
}
return attributedText
}
You can use it like this: TextContent.attributedText = transformText(text: "12345", underlined: true, phoneNumber: "12345")

how to change color of the part of header title in tableView in swift 3?

I have read similar questions But I didn't got the answer :
I have Table View With Headers and the first one has title and a number I want to change the color of that number in the header in table view
here is my codes :
var headerList = ["Account" , "Help" , "" ]
override func viewDidLoad() {
super.viewDidLoad()
self.headerList[0] = "Account \(userAccountMoney)"
}
override func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
let returnedView = UIView(frame: CGRect(x: 0, y: 0, width: view.frame.size.width, height: 25))
returnedView.backgroundColor = UIColor.init(red: 229/255, green: 233/255, blue: 236/255, alpha: 1.0)
let label = UILabel(frame: CGRect(x: -20, y: 7, width: view.frame.size.width, height: 25))
label.text = self.headerList[section]
label.textAlignment = .right
label.textColor = .lightGray
}
as you See in my codes the header Titles Are Gray But I want in the first header Title the Account word Still be Gray But the userAccountMoney be the Green the problem is that because userAccountMoney is another variable I couldn't use similar questions
If you want to change the color of a part of a string you need to use NS(Mutable)AttributedString and add the color attribute only to the specific range:
if section == 0 {
let sectionTitle = self.headerList[0]
let attributedString = NSMutableAttributedString(string: sectionTitle)
if sectionTitle.characters.count > 8 {
attributedString.addAttribute(NSForegroundColorAttributeName, value: UIColor.red, range: NSRange(location: 8, length: sectionTitle.characters.count - 8))
}
label.attributedText = attributedString
} else {
label.text = self.headerList[section]
}
The index 8 is the hard-coded index after "Account "
However in your case I'd recommend to create headerList as constant
let headerList = ["" , "Help" , "" ]
then delete the code in viewDidLoad and use this code to create the account amount dynamically
if section == 0 {
let attributedString = NSMutableAttributedString(string: "Account ")
attributedString.append(NSAttributedString(string: "\(userAccountMoney)", attributes: [NSForegroundColorAttributeName : UIColor.red]))
label.attributedText = attributedString
} else {
label.text = self.headerList[section]
}
Using NSAttributedString you can create two multiple colour text, even you can change the font. Here is the snippet:
let titleAtt = NSAttributedString(string: "Hello", attributes: [NSFontAttributeName:UIFont(name: "Georgia", size: 18.0)!, NSForegroundColorAttributeName: UIColor.blue])
let numberAtt = NSAttributedString(string: "123", attributes: [NSFontAttributeName:UIFont(name: "Georgia", size: 18.0)!, NSForegroundColorAttributeName: UIColor.red])
let combinationAtt = NSMutableAttributedString()
combinationAtt.append(titleAtt)
combinationAtt.append(numberAtt)
label.attributedText = combinationAtt
You can achieve it by writing like below code:
I have created one common function to reuse it in every controller.
//MARK:- Set title with two different color
func setTitle(title : String, loc : Int, len : Int) -> NSMutableAttributedString
{
let myMutableString = NSMutableAttributedString(
string: title,
attributes: [:])
myMutableString.addAttribute(
NSForegroundColorAttributeName,
value: UIColor(red: 29/255, green: 140/255, blue: 242/255, alpha: 1.0),
range: NSRange(
location:loc,
length:len))
return myMutableString
}
I am using this fuction like,
For same controller you can write it,
self.lblTitle.attributedText = self.setTitle(title: "My Profile", loc: 3, len: 7)
You can also use this to change color of specific characters. You need to specify lenght of string and starting location of characters.
I might not understand your problem completely but I think you want to set different color for your first tableview header.
You want do this by using If-else statement on section variable.
Like this
func setAttributedString(text string : String, subStringRange range : NSRange, subStringFont font1 : UIFont, mainStringFont font2 : UIFont, subStringColor color1 : UIColor = UIColor.gray, mainStringColor color2 : UIColor = UIColor.green) -> NSAttributedString {
let mainStringAttribute = [ NSFontAttributeName: font2, NSForegroundColorAttributeName : color2] as [String : Any]
let attributedString = NSMutableAttributedString(string: string, attributes: mainStringAttribute)
let subStringAttribute = [ NSFontAttributeName: font1, NSForegroundColorAttributeName : color1] as [String : Any]
attributedString.addAttributes(subStringAttribute, range: range)
return attributedString
}
override func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
if (section == 0) // this is first header
{
let returnedView = UIView(frame: CGRect(x: 0, y: 0, width: view.frame.size.width, height: 25))
returnedView.backgroundColor = UIColor.init(red: 229/255, green: 233/255, blue: 236/255, alpha: 1.0)
let label = UILabel(frame: CGRect(x: -20, y: 7, width: view.frame.size.width, height: 25))
label.text = self.headerList[section]
label.textAlignment = .right
let font1 = UIFont.systemFont(ofSize: 14.0)
let font2 = UIFont.systemFont(ofSize: 14.0)
label.attributedText = setAttributedString(text: "", subStringRange: NSRange(location: 0, length: 1), subStringFont: font1, mainStringFont: font2)
}
else /// remaining headers
{
let returnedView = UIView(frame: CGRect(x: 0, y: 0, width: view.frame.size.width, height: 25))
returnedView.backgroundColor = UIColor.init(red: 229/255, green: 233/255, blue: 236/255, alpha: 1.0)
let label = UILabel(frame: CGRect(x: -20, y: 7, width: view.frame.size.width, height: 25))
label.text = self.headerList[section]
label.textAlignment = .right
label.textColor = .lightGray
}
}

How to concatenate two UITextView

I'm trying to concatenate two UItextView and it work.
They have different properties (for example different UIFont) but in the final UITextView they have the same properties. How to fix this?
textViewFirst!.text = "\n Example"
textViewFirst!.font = UIFont(name: "Helvetica Neue", size: 10);
textViewSecond.text = textViewSecond.text + textViewFirst.text
for example : this makes your text bold from the 4th char to 7th
let myFullString:String = textViewSecond.text + textViewFirst.text as String
var attributedText: NSMutableAttributedString = NSMutableAttributedString(string: myFullString)
attributedText.addAttributes([NSFontAttributeName: UIFont.boldSystemFontOfSize(14)], range: NSRange(location: 3, length: 3))
textViewSecond.attributedText = attributedText
Try this:
var attributedString = NSMutableAttributedString(string: textView2.text, attributes: [NSFontAttributeName : textView2.font])
attributedString.appendAttributedString(NSAttributedString(string: textView1.text, attributes: [NSFontAttributeName : textView1.font]))
textView2.attributedText = attributedString
In order to preserve both fonts and maybe other attributes (like text color) you must make use of NSAttributedString
let font = UIFont(name: "Helvetica Neue", size: 10.0) ?? UIFont.systemFontOfSize(18.0)
let textFont = [NSFontAttributeName:font]
// Create a string that will be our paragraph
let para1 = NSMutableAttributedString()
let para2 = NSMutableAttributedString()
// Create locally formatted strings
let attrString1 = NSAttributedString(string: "Hello ", attributes:textFont)
let attrString2 = NSAttributedString(string: "World ", attributes:textFont)
// Add locally formatted strings to paragraph
para1.appendAttributedString(attrString1)
para2.appendAttributedString(attrString2)
// Define paragraph styling
let paraStyle = NSMutableParagraphStyle()
paraStyle.firstLineHeadIndent = 15.0
paraStyle.paragraphSpacingBefore = 10.0
// Apply paragraph styles to paragraph
para1.addAttribute(NSParagraphStyleAttributeName, value: paraStyle, range: NSRange(location: 0,length: para1.length))
para2.addAttribute(NSParagraphStyleAttributeName, value: paraStyle, range: NSRange(location: 0,length: para1.length))
// Create UITextView
let view1 = UITextView(frame: CGRect(x: 0, y: 20, width: CGRectGetWidth(self.view.frame), height: 100))
let view2 = UITextView(frame: CGRect(x: 0, y: 100, width: CGRectGetWidth(self.view.frame), height: 100))
let view3 = UITextView(frame: CGRect(x: 0, y: 200, width: CGRectGetWidth(self.view.frame), height: 100))
// Add string to UITextView
view1.attributedText = para1
view2.attributedText = para2
var attributedString = NSMutableAttributedString(string: view1.text, attributes: [NSFontAttributeName : view1.font])
attributedString.appendAttributedString(NSAttributedString(string: view2.text, attributes: [NSFontAttributeName : view2.font]))
view3.attributedText = attributedString
// Add UITextView to main view
self.view.addSubview(view1)
self.view.addSubview(view2)
self.view.addSubview(view3)

Resources