Customize underline pattern in NSAttributedString (iOS7+) - ios

I'm trying to add a dotted underline style to an NSAttributedString (on iOS7+/TextKit). Of course, I tried the built-in NSUnderlinePatternDot:
NSString *string = self.label.text;
NSRange underlineRange = [string rangeOfString:#"consetetur sadipscing elitr"];
NSMutableAttributedString *attString = [[NSMutableAttributedString alloc] initWithString:string];
[attString addAttributes:#{NSUnderlineStyleAttributeName: #(NSUnderlineStyleSingle | NSUnderlinePatternDot)} range:underlineRange];
self.label.attributedText = attString;
However, the style produced by this is actually rather dashed than dotted:
Am I missing something obvious here (NSUnderlinePatternReallyDotted? ;) ) or is there perhaps a way to customize the line-dot-pattern?

According to Eiko&Dave's answer, i made an example like this
i've try to finish that by using UILabel rather than UITextView, but could not find a solution after searching from stackoverflow or other websites. So, i used UITextView to do that.
let storage = NSTextStorage()
let layout = UnderlineLayout()
storage.addLayoutManager(layout)
let container = NSTextContainer()
container.widthTracksTextView = true
container.lineFragmentPadding = 0
container.maximumNumberOfLines = 2
container.lineBreakMode = .byTruncatingTail
layout.addTextContainer(container)
let textView = UITextView(frame: CGRect(x: 0, y: 0, width: UIScreen.main.bounds.width-40, height: 50), textContainer: container)
textView.isUserInteractionEnabled = true
textView.isEditable = false
textView.textContainerInset = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 0)
textView.text = "1sadasdasdasdasdsadasdfdsf"
textView.backgroundColor = UIColor.white
let rg = NSString(string: textView.text!).range(of: textView.text!)
let attributes = [NSAttributedString.Key.underlineStyle.rawValue: 0x11,
NSAttributedString.Key.underlineColor: UIColor.blue.withAlphaComponent(0.2),
NSAttributedString.Key.foregroundColor: UIColor.black, NSAttributedString.Key.font: UIFont.systemFont(ofSize: 17),
NSAttributedString.Key.baselineOffset:10] as! [NSAttributedString.Key : Any]
storage.addAttributes(attributes, range: rg)
view.addSubview(textView)
override LayoutManage Method
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-15
let width = offsetRect.width
let path = UIBezierPath()
path.lineWidth = 3
path.move(to: CGPoint(x: left, y: bottom))
path.addLine(to: CGPoint(x: left + width, y: bottom))
path.stroke()
}
}
in my solution, i've to expand lineSpacing also keep a customize underline by using NSAttributedString property NSMutableParagraphStyle().lineSpacing, but it seems that didn't work, but NSAttributedString.Key.baselineOffset is worked. hope can

I've spent a little bit of time playing around with Text Kit to get this and other similar scenarios to work. The actual Text Kit solution for this problem is to subclass NSLayoutManager and override drawUnderline(forGlyphRange:underlineType:baselineOffset:lineFragmentRect:lineFragmentGlyphRange:containerOrigin:)
This is the method that gets called to actually draw the underline that is requested by the attributes in the NSTextStorage. Here's a description of the parameters that get passed in:
glyphRange the index of the first glyph and the number of following glyphs to be underlined
underlineType the NSUnderlineStyle value of the NSUnderlineAttributeName attribute
baselineOffset the distance between the bottom of the bounding box and the baseline offset for the glyphs in the provided range
lineFragmentRect the rectangle that encloses the entire line that contains glyphRange
lineFragmentGlyphRange the glyph range that makes up the entire line that contains glyphRange
containerOrigin the offset of the container within the text view to which it belongs
Steps to draw underline:
Find the NSTextContainer that contains glyphRange using NSLayoutManager.textContainer(forGlyphAt:effectiveRange:).
Get the bounding rectangle for glyphRange using NSLayoutManager.boundingRect(forGlyphRange:in:)
Offset the bounding rectangle by the text container origin using CGRect.offsetBy(dx:dy:)
Draw your custom underline somewhere between the bottom of the offset bounding rectangle and the baseline (as determined by baselineOffset)

I know this is a 2 year old story but maybe it will help someone facing the same problem, like I did last week.
My workaround was to subclass UITextView, add a subview (the dotted lines container) and use the textView’s layoutManager method enumerateLineFragmentsForGlyphRange to get the number of lines and their frames.
Knowing the line’s frame I calculated the y where I wanted the dots. So I created a view (lineView in which I drawn dots), set clipsToBounds to YES, the width to that of the textView and then
added as a subview in the linesContainer at that y.
After that I set the lineView’s width to that returned by enumerateLineFragmentsForGlyphRange.
For multiple rows the same approach: update frames, add new lines or remove according to what enumerateLineFragmentsForGlyphRange returns.
This method is called in textViewDidChange after each text update.
Here is the code to get the array of lines and their frames
NSLayoutManager *layoutManager = [self layoutManager];
[layoutManager enumerateLineFragmentsForGlyphRange:NSMakeRange(0, [layoutManager numberOfGlyphs])
usingBlock:^(CGRect rect, CGRect usedRect, NSTextContainer *textContainer, NSRange glyphRange, BOOL *stop) {
[linesFramesArray addObject:NSStringFromCGRect(usedRect)];
}
];

Related

clipsToBounds on UILabel not working

I have a UILabel, that I am setting up like this:
class someSuperclass {
var firstLetterLabel = UILabel(frame: CGRect(x: 58, y: 0, width: 81, height: 120))
func commonInit() {
firstLetterLabel.font = UIFont(name: "MuseoSans-500", size: 110.0)
firstLetterLabel.textColor = UIColor.museumRed
firstLetterLabel.textAlignment = .center
firstLetterLabel.numberOfLines = 1
firstLetterLabel.lineBreakMode = .byClipping
firstLetterLabel.clipsToBounds = false
addSubview(firstLetterLabel)
}
}
But it is still being clipped by its bounds
Since clipsToBounds does not seem to apply to the content of a label. How can I stop label content from being clipped by it's frame/bounds?
ClipsToBounds allows subviews or sub layers to spill out of a view or prevent that but it does not do that for drawn content by a view and in this case it is a UILabel. You cannot draw past the bounds of the view/label. That is why it is clipped. This difference always clips drawn content.
possible solutions
1) let intrinsic size of the single letter labels keep it from clipping. Place all the the labels in a horizontal stack view.
2) enable minimum font scale on the label to allow it to fit.
3) lastly it appears it is not drawing centered. Not really sure why as you have given very little to look at.
You can use UIButton instead, and setting button.userInteractionEnabled = false.

UITextView - Setting max lines hides the cursor

This is what I am doing to set max number of lines :
self.text_description.textContainer.maximumNumberOfLines = 2
self.text_description.layoutManager.textContainerChangedGeometry(self.text_description.textContainer)
When 3rd line is about to get started, the cursor disappears. To put it back I have to tap backspace.
Assuming, in the text view cursor doesn't stays behind the keyboard. I have written a helper method that makes UITextField text scroll to end of text.
- (void)scrollToCaretInTextView:(UITextView *)textView animated:(BOOL)animated
{
CGRect rect = [textView caretRectForPosition:textView.selectedTextRange.end]; //Get the content size of textview
rect.size.height += textView.textContainerInset.bottom;
[textView scrollRectToVisible:rect animated:animated];
}
I don't quite sure what you are expecting. If you want two lines max, there should be no 3rd line.
Use this:
self.text_description.textContainer.maximumNumberOfLines = 2
self.text_description.textContainer.lineBreakMode = .byTruncatingTail
Limit the number of lines for UITextview
It seems what you actually want is a textview with two lines height? If that is true, try this in you view controller.
self.text_description = UITextView(frame: CGRect(x: 100.0, y: 100.0, width: 200.0, height: 28.0))
self.text_description.backgroundColor = #colorLiteral(red: 0.8039215803, green: 0.8039215803, blue: 0.8039215803, alpha: 1)
self.view.addSubview(self.text_description)

Calculate where text starts on centre alignment

I have a UILabel that I want to add a UIView exactly where the text starts.
The problem is that the text in the label is aligned to the centre so I don't know how to determinate where the actual text starts and where to position this UIView.
Any idea how can I calculate this thing?
Thank you!
You can use a UIView container which will wrap the UILabel and your new UIView. Then you can let the UILabel decides the width depending on its content and set it in the center of the container. Once you have this working you can just read the UILabel x to understand where it starts.
so the UILabel will have the constraints to top, bottom, the height you want and at center to horizontal whereas the little UIView to top, bottom, height, width and leading equal to UILabel
From :How to find the position(x,y) of a letter in UILabel
NSRange range = [#"Good,Morning" rangeOfString:#","];
NSString *prefix = [#"Good,Morning" substringToIndex:range.location];
CGSize size = [prefix sizeWithFont:[UIFont systemFontOfSize:18]];
CGPoint p = CGPointMake(size.width, 0);
NSLog(#"p.x: %f",p.x);
NSLog(#"p.y: %f",p.y);
you can modify it for your purpose
you can get text size using "nsattributedstring"
let label = UILabel(frame: CGRectMake(0, 0, 100, 100))
label.text = "dshfk,j"
label.textAlignment = .Center
label.textColor = UIColor.redColor()
let string = label.attributedText
let width = string!.size().width
let height = string!.size().height
let view = UIView(frame: CGRectMake(width, height, width, height))
view.backgroundColor = UIColor.blackColor()
then you can find rect using this ..
sample view
You could also use
yourLabel.sizeToFit()
to automatically resize your label for the space it needs. And then read out the frame of your label with:
yourLabel.frame.size (width or height)

UITextView exclusionPaths bottom right

I am trying to do something whatsapp like for messages for having a cell that resizes based on contentsize and in the bottom right corner to have the date... the problem is that I don't know the rectangle position for the excluded path because the text is dynamic and I have something like this but works only for multiple lines, but not for a single line:
NSString *txt=#"I'm writing ";//a text renderer using Core Text, and I discovered I’ll need to wrap text around objects (such as is done in any DTP program). I couldn’t find any easy answers as to how to do this in the documentation, so having finally got it working I’ll share what I did. To lay out text in a custom shape in Core Text, you can pass a CGPath in when you create your CTFramesetterRef. Originally this only supported rectangular paths, but now it supports fully custom paths. My first thought was to see if I could subtract the region to wrap around from the path for my frame’s border, and pass the result in to Core Text as a path. It turns out firstly that subtracting one path from another in Core Graphics is not trivial. However, if you simply add two shapes to the same path, Core Graphics can use a winding rule to work out which areas to draw. Core Text, at least as of iOS 4.2, can also use this kind of algorithm. This will work for many cases: if you can guarantee that your object to be wrapped will be fully inside the frame (and not overlapping the edge), just go ahead and add its border to the same path as your frame, and Core"; // Text will do the rest.";
txtText.text=txt;
CGSize textFrame = [txtText sizeThatFits:CGSizeMake(235, MAXFLOAT)];
UIBezierPath * imgRect = [UIBezierPath bezierPathWithRect:CGRectMake(textFrame.width-35, textFrame.height-20, 35, 20)];
txtText.textContainer.exclusionPaths = #[imgRect];
textFrame = [txtText sizeThatFits:CGSizeMake(235, MAXFLOAT)];
//int frameWidth=375;
txtText.frame=CGRectIntegral(CGRectMake(10, 10, textFrame.width, textFrame.height));
lblHour.frame=CGRectIntegral(CGRectMake(textFrame.width-35, textFrame.height-15, 35, 20));
viewCanvasAround.frame=CGRectIntegral(CGRectMake(50, 100, textFrame.width+20, textFrame.height+20));
I had the same problem too. I added attributedString with textAttachment which it has a size. You will set an estimated maximum size. There's no problem with a single line either.
(Sorry, I am not good at English.)
This is the sample code.
var text: String! {
didSet {
let font = UIFont.systemFont(ofSize: 13);
let att = NSMutableAttributedString.init(string: text, attributes: [NSAttributedString.Key.foregroundColor : UIColor.black,
NSAttributedString.Key.font : font]);
let attachment = NSTextAttachment.init(data: nil, ofType: nil);
var size = extraSize;
size.height = font.lineHeight;
attachment.bounds = CGRect.init(origin: CGPoint.init(), size: size);
att.append(NSAttributedString.init(attachment: attachment));
textView.attributedText = att;
}
}
There is a nice solution my colleague found in actor.im. https://github.com/actorapp/actor-platform/blob/master/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Content/Conversation/Cell/AABubbleTextCell.swift#L312
let container = YYTextContainer(size: CGSizeMake(maxTextWidth, CGFloat.max))
textLayout = YYTextLayout(container: container, text: attributedText)!
// print("Text Layouted")
// Measuring text and padded text heights
let textSize = textLayout.textBoundingSize
if textLayout.lines.count == 1 {
if textLayout.textBoundingSize.width < maxTextWidth - timeWidth {
//
// <line_0> <date>
//
bubbleSize = CGSize(width: textSize.width + timeWidth, height: textSize.height)
} else {
//
// <line_________0>
// <date>
//
bubbleSize = CGSize(width: textSize.width, height: textSize.height + 16)
}
} else {
let maxWidth = textSize.width
let lastLine = textLayout.lines.last!.width
if lastLine + timeWidth < maxWidth {
//
// <line_________0>
// <line_________1>
// ..
// <line_n> <date>
//
bubbleSize = textSize
} else if lastLine + timeWidth < maxTextWidth {
//
// |------------------|
// <line______0>
// <line______1>
// ..
// <line______n> <date>
//
bubbleSize = CGSize(width: max(lastLine + timeWidth, maxWidth), height: textSize.height)
} else {
//
// <line_________0>
// <line_________1>
// ..
// <line_________n>
// <date>
//
bubbleSize = CGSize(width: max(timeWidth, maxWidth), height: textSize.height + 16)
}
}
I had the same problem (for me it was a button in right corner) and there is the way how I resolved it:
- (void)viewWillLayoutSubviews {
[super viewWillLayoutSubviews];
[self updateTextExclusionPaths];
}
- (void)updateTextExclusionPaths {
// Remove old exclusionPaths to calculate right rects of new
textView.textContainer.exclusionPaths = #[];
// Force textView to layout the text without exclusionPaths
[textView.layoutManager glyphRangeForTextContainer:textView.textContainer];
/// Because of some specifics of my implementation I had a problem with wrong frame of my button. So I calculated it manually
// Calculate new exclusionPaths
CGRect buttonFrameInTextView = [superViewOfTextView convertRect:button.frame toView:textView];
// Calculate textView.text size without any exclusionPaths to calculate right button frame
CGSize textViewSize = [textView sizeThatFits:CGSizeMake(textView.frame.size.width, CGFLOAT_MAX)];
// Update Y position of button
buttonFrameInTextView.origin.y = size.height - buttonFrameInTextView.size.height;
// Add new exclusionPaths
UIBezierPath *buttonRectBezierPath = [UIBezierPath bezierPathWithRect:buttonFrameInTextView];
textView.textContainer.exclusionPaths = #[ buttonRectBezierPath ];
}

How to truncate the title of a tabbarcontroller item

I'm creating tabbaritems dynamically, and sometimes the title of the item exceeds the item's space and it take the space of the next tabbaritem.
Somebody knows how to prevent it? How to truncate the name?
Sorry, but I can't post photos yet.
Thanks in advance!
Actually there is no easy way to do it.
You can truncate NSString to some defined width( in ex. "TestBarTitle"->"TestB.." ) before setting it as a title:
- (NSString*)stringByTruncatingStringWithFont:(UIFont *)font forWidth:(CGFloat)width lineBreakMode:(UILineBreakMode)lineBreakMode {
NSMutableString *resultString = [[self mutableCopy] autorelease];
NSRange range = {resultString.length-1, 1};
while ([resultString sizeWithFont:font forWidth:FLT_MAX lineBreakMode:lineBreakMode].width > width) {
// delete the last character
[resultString deleteCharactersInRange:range];
range.location--;
// replace the last but one character with an ellipsis
[resultString replaceCharactersInRange:range withString:truncateReplacementString];
}
return resultString;
}
Or you can manually implement UITabBar ( UIImageView + UIButtons and UILabels ), so you will have 100% control of this UI element;
Solution for Swift 4.2:
var resultString = title
var attributedText = NSAttributedString(string: resultString, attributes: [NSAttributedStringKey.font: font])
while attributedText.boundingRect(with: CGSize(width: CGFloat.greatestFiniteMagnitude, height: 15), options: .usesLineFragmentOrigin, context: nil).size.width > availableWidth {
// delete last character
resultString.removeLast(2)
resultString.append(".")
attributedText = NSAttributedString(string: resultString, attributes: [NSAttributedStringKey.font: font])
}

Resources