I am using the following UITapGestureRecognizer that I modified so others could copy and paste to see the same output as me.
extension UITapGestureRecognizer {
func didTapAttributedTextInLabel(label: UILabel, inRange targetRange: NSRange) -> Bool {
// Create instances of NSLayoutManager, NSTextContainer and NSTextStorage
let layoutManager = NSLayoutManager()
let textContainer = NSTextContainer(size: CGSize.zero)
//let mutableAttribString = NSMutableAttributedString(attributedString: label.attributedText!)
//mutableAttribString.addAttributes([NSAttributedString.Key.font: label.font!], range: NSRange(location: 0, length: label.attributedText!.length))
let textStorage = NSTextStorage(attributedString: label.attributedText!)
//let textStorage = NSTextStorage(attributedString: mutableAttribString)
// Configure layoutManager and textStorage
layoutManager.addTextContainer(textContainer)
textStorage.addLayoutManager(layoutManager)
// Configure textContainer
textContainer.lineFragmentPadding = 0.0
textContainer.lineBreakMode = label.lineBreakMode
textContainer.maximumNumberOfLines = label.numberOfLines
let labelSize = label.bounds.size
print("LabelSize=",labelSize)
textContainer.size = labelSize
print("TextContainerSize=",textContainer.size)
// Find the tapped character location and compare it to the specified range
let locationOfTouchInLabel = self.location(in: label)
print("LocationOfTouchInLabel=",locationOfTouchInLabel)
let textBoundingBox = layoutManager.usedRect(for: textContainer)
print("TextBoundingBox=",textBoundingBox)
//let textContainerOffset = CGPointMake((labelSize.width - textBoundingBox.size.width) * 0.5 - textBoundingBox.origin.x,
//(labelSize.height - textBoundingBox.size.height) * 0.5 - textBoundingBox.origin.y);
let textContainerOffset = CGPoint(x: (labelSize.width - textBoundingBox.size.width) * 0.5 - textBoundingBox.origin.x, y: (labelSize.height - textBoundingBox.size.height) * 0.5 - textBoundingBox.origin.y)
print("textContainerOffset",textContainerOffset)
//let locationOfTouchInTextContainer = CGPointMake(locationOfTouchInLabel.x - textContainerOffset.x,
// locationOfTouchInLabel.y - textContainerOffset.y);
let locationOfTouchInTextContainer = CGPoint(x: locationOfTouchInLabel.x - textContainerOffset.x, y: locationOfTouchInLabel.y - textContainerOffset.y)
print("LocationOfTouchInTextContainer",locationOfTouchInTextContainer)
let indexOfCharacter = layoutManager.characterIndex(for: locationOfTouchInTextContainer, in: textContainer, fractionOfDistanceBetweenInsertionPoints: nil)
print("IndexOfCharacter=",indexOfCharacter)
print("TargetRange=",targetRange)
return NSLocationInRange(indexOfCharacter, targetRange)
}
}
I set the label like so to detect clicks like other answers have said to do:
formulaLabel.addGestureRecognizer(UITapGestureRecognizer(target:self, action: #selector(tapLabel(gesture:))))
It is set to attributed.
In case it matters I have the following code that performs an animation with the text.
func doFormulaAnimation(){
//animTimer = Timer.scheduledTimer(withTimeInterval: 0.3, repeats: true, block: {_ in
//Draw correct formula based on mode.
var formula = ""
var highlight_ranges:[NSRange] = []
if(stateController?.tdfvariables.ebrtMode == true){
formulaLabel.numberOfLines = 1
formula = "BED = N * d * [ RBE + ( d / (α/β) ) ]"
self.formulaLabel.attributedText = NSMutableAttributedString(string: formula, attributes: [ NSAttributedString.Key.foregroundColor: UIColor.label ])
if(self.dosePerFractionSelected==true){
highlight_ranges.append(NSRange(location: 10, length: 1))
highlight_ranges.append(NSRange(location: 24, length: 1))
}
if(self.alphaBetaSelected==true){
highlight_ranges.append(NSRange(location: 29, length: 3))
}
if(self.totalDoseSelected==true){
highlight_ranges.append(NSRange(location: 6, length: 5))
}
if(self.numFractionsSelected==true){
highlight_ranges.append(NSRange(location: 6, length: 1))
}
}
if(stateController?.tdfvariables.ldrMode == true){
formulaLabel.numberOfLines = 3
formula = "BED = R * T * [RBE + (( g * R * T) / (α/β) ) ]\ng = ( 2 / μT ) * ( 1 - ( ( 1 - exp(-μT) / μT ) )\nμ = 0.693 / Thalf repair"
}
if(stateController?.tdfvariables.permMode == true){
formulaLabel.numberOfLines = 2
formula = "BED = ( R0 / L ) * [ RBE + ( R0 / ( ( u + L) * (α/β) ) ) ]\n"
}
UIView.transition(with: self.formulaLabel, duration: 0.5, options: .transitionCrossDissolve, animations: {
let colorAttribute = [ NSAttributedString.Key.foregroundColor: UIColor.label ]
let attributedTextFormula = NSMutableAttributedString(string: formula, attributes: colorAttribute)
let paragraphStyle = NSMutableParagraphStyle()
paragraphStyle.lineSpacing = 6
paragraphStyle.lineBreakMode = .byTruncatingTail
//paragraphStyle.alignment = .center
attributedTextFormula.addAttribute(
.paragraphStyle,
value: paragraphStyle,
range: NSRange(location: 0, length: attributedTextFormula.length
))
self.formulaLabel.attributedText = attributedTextFormula
//
}, completion: { finished in
print("finished first transition")
UIView.transition(with: self.formulaLabel, duration: 0.5, options: .transitionCrossDissolve, animations: {
let colorAttribute = [ NSAttributedString.Key.foregroundColor: UIColor.label ]
let attributedTextFormula = NSMutableAttributedString(string: formula, attributes: colorAttribute)
for range in highlight_ranges {
attributedTextFormula.addAttribute(NSAttributedString.Key.foregroundColor, value: UIColor.red, range: range)
}
let paragraphStyle = NSMutableParagraphStyle()
paragraphStyle.lineSpacing = 6
paragraphStyle.lineBreakMode = .byTruncatingTail
//paragraphStyle.alignment = .center
attributedTextFormula.addAttribute(
.paragraphStyle,
value: paragraphStyle,
range: NSRange(location: 0, length: attributedTextFormula.length
))
self.formulaLabel.attributedText = attributedTextFormula
//self.formulaLabel.addInterlineSpacing(spacingValue: 1)
}, completion: { finished in
print("finished second transition")
})
})
}
The code above demonstrates me creating the NSMutableString accordingly and setting the paragraph style and line spacing accordingly (I have a hunch this may be the issue but not sure how to test that) etc...
Finally I have my tap label function
@IBAction func tapLabel(gesture: UITapGestureRecognizer) {
guard let text = formulaLabel.attributedText?.string else {
return
}
let AB_Range = text.range(of: "(α/β)")
//let AB_Range = NSRange(location: 29, length: 3)
if gesture.didTapAttributedTextInLabel(label: formulaLabel, inRange: NSRange(AB_Range!, in: text)) {
print("Tapped a/b")
} else {
print("Tapped none")
}
}
Currently it does not tap on the correct location but is off by a very large degree. Upon debugging it becomes obvious why it's not working but I am not sure how to fix the code or where it went wrong.
When i click on the A/B like I'm supposed to I get this as the debug info.
LabelSize= (315.0, 43.0)
TextContainerSize= (315.0, 43.0)
LocationOfTouchInLabel= (266.0, 28.0)
TextBoundingBox= (0.0, 0.0, 185.373046875, 13.8)
textContainerOffset (64.8134765625, 14.6)
LocationOfTouchInTextContainer (201.1865234375, 13.4)
IndexOfCharacter= 36
TargetRange= {28, 5}
Tapped none
The part where it calculates the textbounding box or textcontaineroffset or the locationoftouchintextcontainer are most likely the error points but I'm not sure which it is since this is hard to understand.
I can tell it's not working correctly because in my string it's no where near the end and it always says indexofcharacter=36 no matter where I click past it, which means it's not calculating something correctly with one of the variables I mentioned above.
Any help is appreciated if they know why this isn't working.
To give an idea how the text looks right now I'll show you below. My end goal is to make it so I can click on parts in the formula to get values to appear, so clickable links you could say.
UPDATE::
I forgot to mention that when I updated the UITapGestureRecognizer and add these lines:
let mutableAttribString = NSMutableAttributedString(attributedString: label.attributedText!)
mutableAttribString.addAttributes([NSAttributedString.Key.font: label.font!], range: NSRange(location: 0, length: label.attributedText!.length))
and set it to the text storage, let textStorage = NSTextStorage(attributedString: mutableAttribString)
I do seem to see some improvements in the numbers but it's still way off.
LabelSize= (315.0, 43.0)
TextContainerSize= (315.0, 43.0)
LocationOfTouchInLabel= (269.0, 25.5)
TextBoundingBox= (7.505859375, 0.0, 299.98828125, 42.9609375)
textContainerOffset (0.0, 0.01953125)
LocationOfTouchInTextContainer (269.0, 25.48046875)
IndexOfCharacter= 17
TargetRange= {28, 5}
Tapped none
You will notice the textboundingbox more closely resembles the labelsize at least. I read somewhere that an issue could be if you are using different fonts so this is why I tried adding that in to be sure it's working correctly.
Also interestingly, adding this to the UITapGestureRecognizer seems to help also:
let paragraphStyle = NSMutableParagraphStyle()
paragraphStyle.lineSpacing = 6
paragraphStyle.lineBreakMode = .byTruncatingTail
//paragraphStyle.alignment = .center
mutableAttribString.addAttribute(
.paragraphStyle,
value: paragraphStyle,
range: NSRange(location: 0, length: mutableAttribString.length
))
I noticed the textbounding box shows 0,0 correctly when I do this instead of 7.50
LabelSize= (315.0, 43.0)
TextContainerSize= (315.0, 43.0)
LocationOfTouchInLabel= (262.5, 31.0)
TextBoundingBox= (0.0, 0.0, 299.98828125, 42.9609375)
textContainerOffset (7.505859375, 0.01953125)
LocationOfTouchInTextContainer (254.994140625, 30.98046875)
IndexOfCharacter= 17
TargetRange= {28, 5}
Tapped none
**MAJOR UPDATE*****
Well great news! It turns out that the font fix was part of the fix, but because I had autoshrink enabled with a minimum font size and maximum font size it was never calculating correctly! When I set to a fixed font size everything works!
Question is now how to make it work with autoshrink enabled? Note that you will have to set size to something very large to prove this works, like I set mine to 55 so it autoshrinks accordingly and then you see the error, if the default size is smaller then it seems to work fine, which tells me the error has something to do with the size being initially very large maybe?