Hey folks – I'm new to TextKit and trying to draw backgrounds & borders around specific attributes. I've gotten fairly close, but haven't yet found methods that don't generate very inconsistent sizing, which tends to look bad. Here's my first crack at it:
class MyLayout: NSLayoutManager {
override func drawBackground(forGlyphRange glyphsToShow: NSRange, at origin: CGPoint) {
super.drawBackground(forGlyphRange: glyphsToShow, at: origin)
guard let storage = textStorage else {
return
}
guard let context = UIGraphicsGetCurrentContext() else {
return
}
var codeBlockRect: CGRect? = nil
enumerateLineFragments(forGlyphRange: glyphsToShow) { (rect, usedRect, container, subRange, stop) in
var effectiveRange = NSRange()
let attributes = storage.attributes(at: subRange.location, effectiveRange: &effectiveRange)
storage.enumerateAttribute(.inlineCodeBlock, in: subRange) { (value, attributeRange, stop) in
guard value != nil else {
return
}
var background = self.boundingRect(forGlyphRange: attributeRange, in: container)
background.origin.x += origin.x
background.origin.y += origin.y
context.setFillColor(UIColor.lightGrey.cgColor)
context.setStrokeColor(UIColor.mediumGrey.cgColor)
context.stroke(background)
context.fill(background)
}
}
}
}
That produces these results:
Single line of text:
Multiple lines of text:
As you can see, there's about 3 pixels of difference between the sizes there. I imagine it's because boundingRect, as the documentation says:
Returns the smallest bounding rect which completely encloses the glyphs in the given glyphRange
But I haven't found a method that gives me a number closer to what I'm looking for. My ideal scenario is that every rectangle will have the exact same height.
Let me know if any more information is needed.
Update
It crossed my mind that this could be related to the proprietary font we're using, so I changed everything to use UIFont.systemFont, which didn't make any difference.
I found a workaround here – instead of defining my inlineCodeBlock as a background, i defined it as a custom underline style.
Doing that let me override drawUnderline(forGlyphRange:underlineType:baselineOffset:lineFragmentRect:lineFragmentGlyphRange:containerOrigin:).
Once I had that, I was able to use baselineOffset to get a consistent positioning.
Related
I want to get the string which is visible at my label and having line break mode .byClipping .
My real string is :-
"This way your API interface will be nice and pretty, you’re not exposing the internals of your class to the world. If you have constants that are used in lots of classes all over the app then it makes sense to have a separate .h and .m file just for defining these constants."
but at run time i am getting :-
"This way your API interface will be nice and pretty, you’re not exposing the internals of your class to the world. If you have constants"
i want to get the below string in a variable at runtime.
I am not sure if this is natively doable. The closest idea I have is to test the string size and removing/adding characters. Check if the following works for you:
func findTextActuallyVisibleInLabel(_ label: UILabel) -> String? {
guard let originalText = label.text else { return nil }
var text = originalText
let labelSize = label.bounds
guard labelSize.height > 0 else { return text }
let bounds: CGRect = CGRect(x: 0.0, y: 0.0, width: labelSize.width, height: CGFloat.infinity)
while !text.isEmpty && label.textRect(forBounds: bounds, limitedToNumberOfLines: label.numberOfLines).height > labelSize.height {
text.removeLast()
label.text = text
}
label.text = originalText
return text
}
This seems to do its job in my case more or less. If I use wrapping that ends text with ... the result is incorrect.
Also some optimization may be nice; currently I just use the full string and start subtracting character by character. Maybe a bisection would be nicer here.
I do still use original label so it should take all possible parameters into account like wrapping, font size, lines... I modify the actual text on the label which is then reset to original.
Possibly view animations should be disabled while this operation is in progress.
I've been playing around with attributed text in a UITextView (Swift 4.2 and noticed that once I introduced "paragraphSpacingBefore" into my design, the Caret becmae too large on the first line of each new paragraph.
I found this suggested fix on Stackoverflow which seemed to work ok to fix the caret size. The problem I found was the caret itself floats above the target line when that line was the start of a new paragraph.
UITextView lineSpacing make cursor height not same
Caret Floats above the target line
I tried solving it, maintaining the core idea of the original solution and adding some offset logic. During debugging I noticed that the original answer for caret size always adjusts the size even when not required so I added a variance filter (only adjust if variance > 10%). Did this because I think adjusting every time will interfere with my soln. to the floating caret problem.
If someone can take a look at my proposed approach, suggest improvements or a better way etc i'd be grateful:
override func caretRect(for position: UITextPosition) -> CGRect {
var superRect = super.caretRect(for: position)
guard let isFont = self.font else {
return superRect
}
let proposedHeight: CGFloat = isFont.pointSize - isFont.descender
var delta: CGFloat = superRect.size.height - proposedHeight
delta = (delta * delta).squareRoot()
//If the delta is < 10% of the original height just return the original rect
if delta / superRect.size.height < 0.1 {
return superRect
}
superRect.size.height = isFont.pointSize - isFont.descender
// "descender" is expressed as a negative value,
// so to add its height you must subtract its value
superRect.origin.y = superRect.origin.y + delta
// delta is used to correct for resized caret floating above the target line
return superRect
}
I got a solution:
// Fix long cursor height when at the end of paragraph with paragraphspacing and wrong cursor position in titles with paragraph spacing before
override public func caretRect(for position: UITextPosition) -> CGRect {
var superRect = super.caretRect(for: position)
guard let isFont = self.font else { return superRect }
let location = self.offset(from: self.beginningOfDocument, to: position)
if let paragrahStyle = self.storage.attribute(.paragraphStyle, at: location, effectiveRange: nil) as? NSParagraphStyle {
superRect.origin.y += paragrahStyle.paragraphSpacingBefore
}
superRect.size.height = isFont.pointSize - isFont.descender
return superRect
}
The real problem paragraphSpacingBefore. So all you have to do is to get the paragraph styling attributes, get the spacing and move the cursor by that spacing. This works well with all the text.
I want to obtain the location of the present caret in UITextView as CGRect, but there seems to be no information. Even the official documentation, I can't imagine how to utilize this method and find the explanation. Now I could know the way of getting the offset in the UITextView, not CGRect. But, I really want to know the CGRect.
Any comments should be highly appreciated.
Thanks.
Try this:
extension UITextView {
var caret: CGRect? {
guard let selectedTextRange = self.selectedTextRange else { return nil }
return self.caretRect(for: selectedTextRange.end)
}
}
and use it like this:
let textView: UITextView = ...
if let caret = textView.caret {
// Do your thing here.
} else {
// Caret is undefined.
}
By the way, you were on the right track :-) The above solution is based on the UITextInput method you just mentioned:
func caretRect(for position: UITextPosition) -> CGRect
Return a rectangle used to draw the caret at a given insertion point.
The UITextPosition parameter represents a position in a text container; in other words, it is an index into the backing string in a text-displaying view.
//rect is an object of CGRect
var rect = TextView.frame
How to know wether UITextView will shorten text because lack of space? I know I can calculate with boundingRectWithSize and sizeThatFit, but what if exclusionPaths of the UITextView is changed. I want lay out string in a polygon, and increase polygon size, until string laid out without shortening. Any idea, how can get a bool return, wether current setup will shorten?
self.tv.textContainer.exclusionPaths = myArrayOfBezierPaths;
func isSizeFitForTextView() -> Bool{
let layoutManager = self.textView.layoutManager
let glyphIndex = layoutManager.glyphIndexForCharacter(at:( self.textView.text as NSString).length)
let range = layoutManager.truncatedGlyphRange(inLineFragmentForGlyphAt: glyphIndex)
return range.location != NSNotFound
}
I have a label that can have one or two lines. If it has two lines, I want the second line to have at least two (or maybe three) words, never just one. Any ideas about how I can accomplish that using swift?
Thanks in advance!
Daniel
Edit: I edited out my silly first thoughts that didn't really help.
Ok, after looking around a lot I came up with what I think is the best solution.
I wrote this function:
func numberOfLinesInLabelForText(text: String) -> Int {
let attributes = [NSFontAttributeName : UIFont.preferredFontForTextStyle(UIFontTextStyleHeadline)]
let screenSize: CGRect = UIScreen.mainScreen().bounds
let labelSize = text!.boundingRectWithSize(CGSizeMake((screenSize.width - 30), CGFloat.max), options: NSStringDrawingOptions.UsesLineFragmentOrigin, attributes: attributes, context: nil)
let lines = floor(CGFloat(labelSize.height) / bookTitleLabel.font.lineHeight)
return Int(lines)
}
You put in the string that will be displayed in the label and it gives you how many lines the label will have. I'm using dynamic type and the Headline style for this particular label, hence the preferredFontForTextStyle(UIFontTextStyleHeadline) part, but you can change that to the font and size your label uses.
Then I use (screenSize.width - 30) for my label's width because it's width is not fixed, so I'm using the screen size minus leading and trailing. This is probably not the most elegant solution, I'm open to better suggestions.
The rest is pretty straightforward.
After I have the number of lines I can do this:
func splittedString(text: String) -> String {
if numberOfLinesInLabel(text) == 2 {
var chars = Array(text.characters)
var i = chars.count / 2
var x = chars.count / 2
while chars[i] != " " && chars[x] != " " {
i--
x++
}
if chars[i] == " " {
chars.insert("\n", atIndex: i+1)
} else {
chars.insert("\n", atIndex: x+1)
}
return String(chars)
}
}
Instead of just avoiding orphans I decided to split the string in two at the breaking point nearest to its half, so that's what this last function does, but it wouldn't be hard to tweak it to suit your needs.
And there you have it!