How do you stroke the _outside_ of an NSAttributedString? - objective-c

I've been using NSStrokeWidthAttributeName on NSAttributedString objects to put an outline around text as it's drawn. The problem is that the stroke is inside the fill area of the text. When the text is small (e.g. 1 pixel thick), the stroking makes the text hard to read. What I really want is a stroke on the outside. Is there any way to do that?
I've tried an NSShadow with no offset and a blur, but it's too blurry and hard to see. If there was a way to increase the size of the shadow without any blur, that would work too.

While there may be other ways, one way to accomplish this is to first draw the string with only a stroke, then draw the string with only a fill, directly overtop of what was previously drawn. (Adobe InDesign actually has this built-in, where it will appear to only apply the stroke to the outside of letter, which helps with readability).
This is just an example view that shows how to accomplish this (inspired by http://developer.apple.com/library/mac/#qa/qa2008/qa1531.html):
First set up the attributes:
#implementation MDInDesignTextView
static NSMutableDictionary *regularAttributes = nil;
static NSMutableDictionary *indesignBackgroundAttributes = nil;
static NSMutableDictionary *indesignForegroundAttributes = nil;
- (void)drawRect:(NSRect)frame {
NSString *string = #"Got stroke?";
if (regularAttributes == nil) {
regularAttributes = [[NSMutableDictionary
dictionaryWithObjectsAndKeys:
[NSFont systemFontOfSize:64.0],NSFontAttributeName,
[NSColor whiteColor],NSForegroundColorAttributeName,
[NSNumber numberWithFloat:-5.0],NSStrokeWidthAttributeName,
[NSColor blackColor],NSStrokeColorAttributeName, nil] retain];
}
if (indesignBackgroundAttributes == nil) {
indesignBackgroundAttributes = [[NSMutableDictionary
dictionaryWithObjectsAndKeys:
[NSFont systemFontOfSize:64.0],NSFontAttributeName,
[NSNumber numberWithFloat:-5.0],NSStrokeWidthAttributeName,
[NSColor blackColor],NSStrokeColorAttributeName, nil] retain];
}
if (indesignForegroundAttributes == nil) {
indesignForegroundAttributes = [[NSMutableDictionary
dictionaryWithObjectsAndKeys:
[NSFont systemFontOfSize:64.0],NSFontAttributeName,
[NSColor whiteColor],NSForegroundColorAttributeName, nil] retain];
}
[[NSColor grayColor] set];
[NSBezierPath fillRect:frame];
// draw top string
[string drawAtPoint:
NSMakePoint(frame.origin.x + 200.0, frame.origin.y + 200.0)
withAttributes:regularAttributes];
// draw bottom string in two passes
[string drawAtPoint:
NSMakePoint(frame.origin.x + 200.0, frame.origin.y + 140.0)
withAttributes:indesignBackgroundAttributes];
[string drawAtPoint:
NSMakePoint(frame.origin.x + 200.0, frame.origin.y + 140.0)
withAttributes:indesignForegroundAttributes];
}
#end
This produces the following output:
Now, it's not perfect, since the glyphs will sometimes fall on fractional boundaries, but, it certainly looks better than the default.
If performance is an issue, you could always look into dropping down to a slightly lower level, such as CoreGraphics or CoreText.

Just leave here my solution based on answer of #NSGod, result is pretty the same just having proper positioning inside UILabel
It is also useful when having bugs on iOS 14 when stroking letters with default system font (refer also this question)
Bug:
#interface StrokedTextLabel : UILabel
#end
/**
* https://stackoverflow.com/a/4468880/3004003
*/
#implementation StrokedTextLabel
- (void)drawTextInRect:(CGRect)rect
{
if (!self.attributedText) {
[super drawTextInRect:rect];
return;
}
NSMutableAttributedString *attributedText = self.attributedText.mutableCopy;
[attributedText enumerateAttributesInRange:NSMakeRange(0, attributedText.length) options:0 usingBlock:^(NSDictionary<NSAttributedStringKey, id> *attrs, NSRange range, BOOL *stop) {
if (attrs[NSStrokeWidthAttributeName]) {
// 1. draw underlying stroked string
// use doubled stroke width to simulate outer border, because border is being stroked
// in both outer & inner directions with half width
CGFloat strokeWidth = [attrs[NSStrokeWidthAttributeName] floatValue] * 2;
[attributedText addAttributes:#{NSStrokeWidthAttributeName : #(strokeWidth)} range:range];
self.attributedText = attributedText;
// perform default drawing
[super drawTextInRect:rect];
// 2. draw unstroked string above
NSMutableParagraphStyle *style = [NSMutableParagraphStyle new];
style.alignment = self.textAlignment;
[attributedText addAttributes:#{
NSStrokeWidthAttributeName : #(0),
NSForegroundColorAttributeName : self.textColor,
NSFontAttributeName : self.font,
NSParagraphStyleAttributeName : style
} range:range];
// we use here custom bounding rect detection method instead of
// [attributedText boundingRectWithSize:...] because the latter gives incorrect result
// in this case
CGRect textRect = [self boundingRectWithAttributedString:attributedText forCharacterRange:NSMakeRange(0, attributedText.length)];
[attributedText boundingRectWithSize:rect.size options:NSStringDrawingUsesLineFragmentOrigin
context:nil];
// adjust vertical position because returned bounding rect has zero origin
textRect.origin.y = (rect.size.height - textRect.size.height) / 2;
[attributedText drawInRect:textRect];
}
}];
}
/**
* https://stackoverflow.com/a/20633388/3004003
*/
- (CGRect)boundingRectWithAttributedString:(NSAttributedString *)attributedString
forCharacterRange:(NSRange)range
{
NSTextStorage *textStorage = [[NSTextStorage alloc] initWithAttributedString:attributedString];
NSLayoutManager *layoutManager = [[NSLayoutManager alloc] init];
[textStorage addLayoutManager:layoutManager];
NSTextContainer *textContainer = [[NSTextContainer alloc] initWithSize:[self bounds].size];
textContainer.lineFragmentPadding = 0;
[layoutManager addTextContainer:textContainer];
NSRange glyphRange;
// Convert the range for glyphs.
[layoutManager characterRangeForGlyphRange:range actualGlyphRange:&glyphRange];
return [layoutManager boundingRectForGlyphRange:glyphRange inTextContainer:textContainer];
}
#end
Swift version
import Foundation
import UIKit
/// https://stackoverflow.com/a/4468880/3004003
#objc(MUIStrokedTextLabel)
public class StrokedTextLabel : UILabel {
override public func drawText(in rect: CGRect) {
guard let attributedText = attributedText?.mutableCopy() as? NSMutableAttributedString else {
super.drawText(in: rect)
return
}
attributedText.enumerateAttributes(in: NSRange(location: 0, length: attributedText.length), options: [], using: { attrs, range, stop in
guard let strokeWidth = attrs[NSAttributedString.Key.strokeWidth] as? CGFloat else {
return
}
// 1. draw underlying stroked string
// use doubled stroke width to simulate outer border, because border is being stroked
// in both outer & inner directions with half width
attributedText.addAttributes([
NSAttributedString.Key.strokeWidth: strokeWidth * 2
], range: range)
self.attributedText = attributedText
// perform default drawing
super.drawText(in: rect)
// 2. draw unstroked string above
let style = NSMutableParagraphStyle()
style.alignment = textAlignment
let attributes = [
NSAttributedString.Key.strokeWidth: NSNumber(value: 0),
NSAttributedString.Key.foregroundColor: textColor ?? UIColor.black,
NSAttributedString.Key.font: font ?? UIFont.systemFont(ofSize: 17),
NSAttributedString.Key.paragraphStyle: style
]
attributedText.addAttributes(attributes, range: range)
// we use here custom bounding rect detection method instead of
// [attributedText boundingRectWithSize:...] because the latter gives incorrect result
// in this case
var textRect = boundingRect(with: attributedText, forCharacterRange: NSRange(location: 0, length: attributedText.length))
attributedText.boundingRect(
with: rect.size,
options: .usesLineFragmentOrigin,
context: nil)
// adjust vertical position because returned bounding rect has zero origin
textRect.origin.y = (rect.size.height - textRect.size.height) / 2
attributedText.draw(in: textRect)
})
}
/// https://stackoverflow.com/a/20633388/3004003
private func boundingRect(
with attributedString: NSAttributedString?,
forCharacterRange range: NSRange
) -> CGRect {
guard let attributedString = attributedString else {
return .zero
}
let textStorage = NSTextStorage(attributedString: attributedString)
let layoutManager = NSLayoutManager()
textStorage.addLayoutManager(layoutManager)
let textContainer = NSTextContainer(size: bounds.size)
textContainer.lineFragmentPadding = 0
layoutManager.addTextContainer(textContainer)
var glyphRange = NSRange()
// Convert the range for glyphs.
layoutManager.characterRange(forGlyphRange: range, actualGlyphRange: &glyphRange)
return layoutManager.boundingRect(forGlyphRange: glyphRange, in: textContainer)
}
}

Related

Autosize Text in Label for PaintCode CGContext

I'm using the following to draw text inside a Bezier Path. How can i adjust this to allow the text to autosize.
EDIT
I was able to update to iOS7 methods but still nothing. I can autosize text within a UILabel fine, but because this is CGContext it is harder
NSString* textContent = #"LOCATION";
NSMutableParagraphStyle* locationStyle = NSMutableParagraphStyle.defaultParagraphStyle.mutableCopy;
locationStyle.alignment = NSTextAlignmentCenter;
NSDictionary* locationFontAttributes = #{NSFontAttributeName: [UIFont fontWithName:myFont size: 19], NSForegroundColorAttributeName: locationColor, NSParagraphStyleAttributeName: locationStyle};
CGFloat locationTextHeight = [textContent boundingRectWithSize: CGSizeMake(locationRect.size.width, INFINITY) options: NSStringDrawingUsesLineFragmentOrigin attributes: locationFontAttributes context: nil].size.height;
CGContextSaveGState(context);
CGContextClipToRect(context, locationRect);
[textContent drawInRect: CGRectMake(CGRectGetMinX(locationRect), CGRectGetMinY(locationRect) + (CGRectGetHeight(locationRect) - locationTextHeight) / 2, CGRectGetWidth(locationRect), locationTextHeight) withAttributes: locationFontAttributes];
CGContextRestoreGState(context);
Try using this method of NSAttributedString:
- (CGRect)boundingRectWithSize:(CGSize)size
options:(NSStringDrawingOptions)options
context:(NSStringDrawingContext *)context;
Where the context will provide you actualScaleFactor.
The usage is something like this:
NSAttributedString *string = ...;
NSStringDrawingContext *context = [NSStringDrawingContext new];
context.minimumScaleFactor = 0.5; // Set your minimum value.
CGRect bounds = [string boundingRectWithSize:maxSize
options:NSStringDrawingUsesLineFragmentOrigin
context:context];
CGFloat scale = context. actualScaleFactor;
// Use this scale to multiply font sizes in the string, so it will fit.

Why is this text changing characters in Yosemite?

I'm trying to update an app for Yosemite, and one weird problem I'm getting is that the text labels on a custom control are changing characters - not distorting, but changing from "ON" to "KJ" and "OFF" to "KBB". The documents are all encoded as UTF-8 files. If anyone has any ideas, I'd love to hear them.
The code in question:
AKDrawStringAlignedInFrame(#"OFF", [NSFont boldSystemFontOfSize:0], NSCenterTextAlignment, NSIntegralRect(textRects[0]));
which calls:
void AKDrawStringAlignedInFrame(NSString *text, NSFont *font, NSTextAlignment alignment, NSRect frame) {
NSCParameterAssert(font != nil);
NSBezierPath *textPath = [NSBezierPath bezierPathWithString:text inFont:font];
NSRect textPathBounds = NSMakeRect(NSMinX([textPath bounds]), [font descender], NSWidth([textPath bounds]), [font ascender] - [font descender]);
NSAffineTransform *scale = [NSAffineTransform transform];
CGFloat xScale = NSWidth(frame)/NSWidth(textPathBounds);
CGFloat yScale = NSHeight(frame)/NSHeight(textPathBounds);
[scale scaleBy:MIN(xScale, yScale)];
[textPath transformUsingAffineTransform:scale];
textPathBounds.origin = [scale transformPoint:textPathBounds.origin];
textPathBounds.size = [scale transformSize:textPathBounds.size];
NSAffineTransform *originCorrection = [NSAffineTransform transform];
NSPoint centeredOrigin = NSRectFromCGRect(AFRectCenteredSize(NSRectToCGRect(frame), NSSizeToCGSize(textPathBounds.size))).origin;
[originCorrection translateXBy:(centeredOrigin.x - NSMinX(textPathBounds)) yBy:(centeredOrigin.y - NSMinY(textPathBounds))];
[textPath transformUsingAffineTransform:originCorrection];
if (alignment != NSJustifiedTextAlignment && alignment != NSCenterTextAlignment) {
NSAffineTransform *alignmentTransform = [NSAffineTransform transform];
CGFloat deltaX = 0;
if (alignment == NSLeftTextAlignment) deltaX = -(NSMinX([textPath bounds]) - NSMinX(frame));
else if (alignment == NSRightTextAlignment) deltaX = (NSMaxX(frame) - NSMaxX([textPath bounds]));
[alignmentTransform translateXBy:deltaX yBy:0];
[textPath transformUsingAffineTransform:alignmentTransform];
}
[textPath fill];
}
and +[NSBezierPath bezierPathWithString:inFont:] is just
+ (NSBezierPath *)bezierPathWithString:(NSString *)text inFont:(NSFont *)font {
NSBezierPath *textPath = [self bezierPath];
[textPath appendBezierPathWithString:text inFont:font];
return textPath;
}
Lastly, -[appendBezierPathWithString:text] is:
- (void)appendBezierPathWithString:(NSString *)text inFont:(NSFont *)font {
if ([self isEmpty]) [self moveToPoint:NSZeroPoint];
NSAttributedString *attributedString = [[NSAttributedString alloc] initWithString:text];
CTLineRef line = CTLineCreateWithAttributedString((CFAttributedStringRef)attributedString);
CFArrayRef glyphRuns = CTLineGetGlyphRuns(line);
CFIndex count = CFArrayGetCount(glyphRuns);
for (CFIndex index = 0; index < count; index++) {
CTRunRef currentRun = (CTRunRef)CFArrayGetValueAtIndex(glyphRuns, index);
CFIndex glyphCount = CTRunGetGlyphCount(currentRun);
CGGlyph glyphs[glyphCount];
CTRunGetGlyphs(currentRun, CTRunGetStringRange(currentRun), glyphs);
NSGlyph bezierPathGlyphs[glyphCount];
for (CFIndex glyphIndex = 0; glyphIndex < glyphCount; glyphIndex++)
bezierPathGlyphs[glyphIndex] = glyphs[glyphIndex];
[self appendBezierPathWithGlyphs:bezierPathGlyphs count:glyphCount inFont:font];
}
CFRelease(line);
}
Glyph indices are specific to a font. The appendBezierPathWithString:inFont: method gets the glyph indices from Core Text (CTLine and CTRun) but it's not providing the font. Presumably, Core Text is using a default font. Later, it's using those glyph indices but it's passing the desired font, not the font that Core Text used. So, the glyph indices don't mean the same thing.
I think the solution is to construct the attributed string in that method with a font attribute:
NSAttributedString *attributedString = [[NSAttributedString alloc] initWithString:text attributes:#{ NSFontAttributeName: font }];
(Normally, you have to be careful about using attributes that Core Text will understand, but I believe that NSFontAttributeName maps to kCTFontAttributeName and NSFont is toll-free bridged to CTFont.)

Showing NSAttributedString at the Center With NSView

I have the following function to make an attributed string.
- (NSMutableAttributedString *)makeText:(NSString *)txt : (NSString *)fontname : (CGFloat)tsize :(NSColor *)textColor :(NSColor *)shadowColor :(NSSize)offset {
NSShadow *shadow = [[NSShadow alloc] init];
shadow.shadowColor = shadowColor;
shadow.shadowOffset = offset;
NSMutableParagraphStyle *paragraphStyle = [[NSMutableParagraphStyle alloc] init];
paragraphStyle.lineSpacing = 10.0f;
paragraphStyle.alignment = NSCenterTextAlignment;
NSMutableAttributedString *atext = [[NSMutableAttributedString alloc] initWithString:txt];
NSRange range = NSMakeRange(0,txt.length);
// alignment
[atext addAttribute:NSParagraphStyleAttributeName value:paragraphStyle range:NSMakeRange(0,[atext length])];
...
...
return atext;
}
If I set this attributed string to NSTextField, the resulting attributed string will be aligned correctly. But if I send it to NSView's subclass, the string will be left-aligned. Is there any way by which I can display this attributed string with correct alignment?
// .h
#interface imageView1 : NSView {
NSAttributedString *aStr;
}
// .m
- (void)drawRect:(NSRect)dirtyRect {
[aStr drawAtPoint:NSMakePoint((self.frame.size.width-aStr.size.width)/2.0f,(self.frame.size.height-aStr.size.height)/2.0f)];
}
Thank you for your help. The OS version is 10.8.
If you draw at a point there are no bounds in relation to which to center. You need to specify a rect. See the docs on drawWithRect:options:. Note the admonition there pointing out that to be in that rect your options will need to be (or include) NSStringDrawingUsesLineFragmentOrigin.

Replacement for deprecated sizeWithFont: in iOS 7?

In iOS 7, sizeWithFont: is now deprecated. How do I now pass in the UIFont object into the replacement method sizeWithAttributes:?
Use sizeWithAttributes: instead, which now takes an NSDictionary. Pass in the pair with key UITextAttributeFont and your font object like this:
CGRect rawRect = {};
rawRect.size = [string sizeWithAttributes: #{
NSFontAttributeName: [UIFont systemFontOfSize:17.0f],
}];
// Values are fractional -- you should take the ceil to get equivalent values
CGSize adjustedSize = CGRectIntegral(rawRect).size;
I believe the function was deprecated because that series of NSString+UIKit functions (sizewithFont:..., etc) were based on the UIStringDrawing library, which wasn't thread safe. If you tried to run them not on the main thread (like any other UIKit functionality), you'll get unpredictable behaviors. In particular, if you ran the function on multiple threads simultaneously, it'll probably crash your app. This is why in iOS 6, they introduced a the boundingRectWithSize:... method for NSAttributedString. This was built on top of the NSStringDrawing libraries and is thread safe.
If you look at the new NSString boundingRectWithSize:... function, it asks for an attributes array in the same manner as a NSAttributeString. If I had to guess, this new NSString function in iOS 7 is merely a wrapper for the NSAttributeString function from iOS 6.
On that note, if you were only supporting iOS 6 and iOS 7, then I would definitely change all of your NSString sizeWithFont:... to the NSAttributeString boundingRectWithSize. It'll save you a lot of headache if you happen to have a weird multi-threading corner case! Here's how I converted NSString sizeWithFont:constrainedToSize::
What used to be:
NSString *text = ...;
CGFloat width = ...;
UIFont *font = ...;
CGSize size = [text sizeWithFont:font
constrainedToSize:(CGSize){width, CGFLOAT_MAX}];
Can be replaced with:
NSString *text = ...;
CGFloat width = ...;
UIFont *font = ...;
NSAttributedString *attributedText =
[[NSAttributedString alloc] initWithString:text
attributes:#{NSFontAttributeName: font}];
CGRect rect = [attributedText boundingRectWithSize:(CGSize){width, CGFLOAT_MAX}
options:NSStringDrawingUsesLineFragmentOrigin
context:nil];
CGSize size = rect.size;
Please note the documentation mentions:
In iOS 7 and later, this method returns fractional sizes (in the size
component of the returned CGRect); to use a returned size to size
views, you must use raise its value to the nearest higher integer
using the ceil function.
So to pull out the calculated height or width to be used for sizing views, I would use:
CGFloat height = ceilf(size.height);
CGFloat width = ceilf(size.width);
As you can see sizeWithFont at Apple Developer site it is deprecated so we need to use sizeWithAttributes.
#define SYSTEM_VERSION_LESS_THAN(v) ([[[UIDevice currentDevice] systemVersion] compare:v options:NSNumericSearch] == NSOrderedAscending)
NSString *text = #"Hello iOS 7.0";
if (SYSTEM_VERSION_LESS_THAN(#"7.0")) {
// code here for iOS 5.0,6.0 and so on
CGSize fontSize = [text sizeWithFont:[UIFont fontWithName:#"Helvetica"
size:12]];
} else {
// code here for iOS 7.0
CGSize fontSize = [text sizeWithAttributes:
#{NSFontAttributeName:
[UIFont fontWithName:#"Helvetica" size:12]}];
}
I created a category to handle this problem, here it is :
#import "NSString+StringSizeWithFont.h"
#implementation NSString (StringSizeWithFont)
- (CGSize) sizeWithMyFont:(UIFont *)fontToUse
{
if ([self respondsToSelector:#selector(sizeWithAttributes:)])
{
NSDictionary* attribs = #{NSFontAttributeName:fontToUse};
return ([self sizeWithAttributes:attribs]);
}
return ([self sizeWithFont:fontToUse]);
}
This way you only have to find/replace sizeWithFont: with sizeWithMyFont: and you're good to go.
In iOS7 I needed the logic to return the correct height for the tableview:heightForRowAtIndexPath method, but the sizeWithAttributes always returns the same height regardless of the string length because it doesn't know that it is going to be put in a fixed width table cell. I found this works great for me and calculates the correct height taking in consideration the width for the table cell! This is based on Mr. T's answer above.
NSString *text = #"The text that I want to wrap in a table cell."
CGFloat width = tableView.frame.size.width - 15 - 30 - 15; //tableView width - left border width - accessory indicator - right border width
UIFont *font = [UIFont systemFontOfSize:17];
NSAttributedString *attributedText = [[NSAttributedString alloc] initWithString:text attributes:#{NSFontAttributeName: font}];
CGRect rect = [attributedText boundingRectWithSize:(CGSize){width, CGFLOAT_MAX}
options:NSStringDrawingUsesLineFragmentOrigin
context:nil];
CGSize size = rect.size;
size.height = ceilf(size.height);
size.width = ceilf(size.width);
return size.height + 15; //Add a little more padding for big thumbs and the detailText label
Multi-line labels using dynamic height may require additional information to set the size properly. You can use sizeWithAttributes with UIFont and NSParagraphStyle to specify both the font and the line-break mode.
You would define the Paragraph Style and use an NSDictionary like this:
// set paragraph style
NSMutableParagraphStyle *style = [[NSParagraphStyle defaultParagraphStyle] mutableCopy];
[style setLineBreakMode:NSLineBreakByWordWrapping];
// make dictionary of attributes with paragraph style
NSDictionary *sizeAttributes = #{NSFontAttributeName:myLabel.font, NSParagraphStyleAttributeName: style};
// get the CGSize
CGSize adjustedSize = CGSizeMake(label.frame.size.width, CGFLOAT_MAX);
// alternatively you can also get a CGRect to determine height
CGRect rect = [myLabel.text boundingRectWithSize:adjustedSize
options:NSStringDrawingUsesLineFragmentOrigin
attributes:sizeAttributes
context:nil];
You can use the CGSize 'adjustedSize' or CGRect as rect.size.height property if you're looking for the height.
More info on NSParagraphStyle here: https://developer.apple.com/library/mac/documentation/cocoa/reference/applicationkit/classes/NSParagraphStyle_Class/Reference/Reference.html
// max size constraint
CGSize maximumLabelSize = CGSizeMake(184, FLT_MAX)
// font
UIFont *font = [UIFont fontWithName:TRADE_GOTHIC_REGULAR size:20.0f];
// set paragraph style
NSMutableParagraphStyle *paragraphStyle = [[NSMutableParagraphStyle alloc] init];
paragraphStyle.lineBreakMode = NSLineBreakByWordWrapping;
// dictionary of attributes
NSDictionary *attributes = #{NSFontAttributeName:font,
NSParagraphStyleAttributeName: paragraphStyle.copy};
CGRect textRect = [string boundingRectWithSize: maximumLabelSize
options:NSStringDrawingUsesLineFragmentOrigin
attributes:attributes
context:nil];
CGSize expectedLabelSize = CGSizeMake(ceil(textRect.size.width), ceil(textRect.size.height));
Create a function that takes a UILabel instance. and returns CGSize
CGSize constraint = CGSizeMake(label.frame.size.width , 2000.0);
// Adjust according to requirement
CGSize size;
if([[[UIDevice currentDevice] systemVersion] floatValue] >= 7.0){
NSRange range = NSMakeRange(0, [label.attributedText length]);
NSDictionary *attributes = [label.attributedText attributesAtIndex:0 effectiveRange:&range];
CGSize boundingBox = [label.text boundingRectWithSize:constraint options: NSStringDrawingUsesLineFragmentOrigin attributes:attributes context:nil].size;
size = CGSizeMake(ceil(boundingBox.width), ceil(boundingBox.height));
}
else{
size = [label.text sizeWithFont:label.font constrainedToSize:constraint lineBreakMode:label.lineBreakMode];
}
return size;
Alternate solution-
CGSize expectedLabelSize;
if ([subTitle respondsToSelector:#selector(sizeWithAttributes:)])
{
expectedLabelSize = [subTitle sizeWithAttributes:#{NSFontAttributeName:subTitleLabel.font}];
}else{
expectedLabelSize = [subTitle sizeWithFont:subTitleLabel.font constrainedToSize:subTitleLabel.frame.size lineBreakMode:NSLineBreakByWordWrapping];
}
Building on #bitsand, this is a new method I just added to my NSString+Extras category:
- (CGRect) boundingRectWithFont:(UIFont *) font constrainedToSize:(CGSize) constraintSize lineBreakMode:(NSLineBreakMode) lineBreakMode;
{
// set paragraph style
NSMutableParagraphStyle *style = [[NSParagraphStyle defaultParagraphStyle] mutableCopy];
[style setLineBreakMode:lineBreakMode];
// make dictionary of attributes with paragraph style
NSDictionary *sizeAttributes = #{NSFontAttributeName:font, NSParagraphStyleAttributeName: style};
CGRect frame = [self boundingRectWithSize:constraintSize options:NSStringDrawingUsesLineFragmentOrigin attributes:sizeAttributes context:nil];
/*
// OLD
CGSize stringSize = [self sizeWithFont:font
constrainedToSize:constraintSize
lineBreakMode:lineBreakMode];
// OLD
*/
return frame;
}
I just use the size of the resulting frame.
You can still use sizeWithFont. but, in iOS >= 7.0 method cause crashing if the string contains leading and trailing spaces or end lines \n.
Trimming text before using it
label.text = [label.text stringByTrimmingCharactersInSet:
[NSCharacterSet whitespaceAndNewlineCharacterSet]];
That's also may apply to sizeWithAttributes and [label sizeToFit].
also, whenever you have nsstringdrawingtextstorage message sent to deallocated instance in iOS 7.0 device it deals with this.
Better use automatic dimensions (Swift):
tableView.estimatedRowHeight = 68.0
tableView.rowHeight = UITableViewAutomaticDimension
NB:
1. UITableViewCell prototype should be properly designed (for the instance don't forget set UILabel.numberOfLines = 0 etc)
2. Remove HeightForRowAtIndexPath method
VIDEO:
https://youtu.be/Sz3XfCsSb6k
boundingRectWithSize:options:attributes:context:
Accepted answer in Xamarin would be (use sizeWithAttributes and UITextAttributeFont):
UIStringAttributes attributes = new UIStringAttributes
{
Font = UIFont.SystemFontOfSize(17)
};
var size = text.GetSizeUsingAttributes(attributes);
As the #Ayush answer:
As you can see sizeWithFont at Apple Developer site it is deprecated so we need to use sizeWithAttributes.
Well, supposing that in 2019+ you are probably using Swift and String instead of Objective-c and NSString, here's the correct way do get the size of a String with predefined font:
let stringSize = NSString(string: label.text!).size(withAttributes: [.font : UIFont(name: "OpenSans-Regular", size: 15)!])
- (CGSize) sizeWithMyFont:(UIFont *)fontToUse
{
if ([self respondsToSelector:#selector(sizeWithAttributes:)])
{
NSDictionary* attribs = #{NSFontAttributeName:fontToUse};
return ([self sizeWithAttributes:attribs]);
}
return ([self sizeWithFont:fontToUse]);
}
Here is the monotouch equivalent if anyone needs it:
/// <summary>
/// Measures the height of the string for the given width.
/// </summary>
/// <param name="text">The text.</param>
/// <param name="font">The font.</param>
/// <param name="width">The width.</param>
/// <param name="padding">The padding.</param>
/// <returns></returns>
public static float MeasureStringHeightForWidth(this string text, UIFont font, float width, float padding = 20)
{
NSAttributedString attributedString = new NSAttributedString(text, new UIStringAttributes() { Font = font });
RectangleF rect = attributedString.GetBoundingRect(new SizeF(width, float.MaxValue), NSStringDrawingOptions.UsesLineFragmentOrigin, null);
return rect.Height + padding;
}
which can be used like this:
public override float GetHeightForRow(UITableView tableView, NSIndexPath indexPath)
{
//Elements is a string array
return Elements[indexPath.Row].MeasureStringHeightForWidth(UIFont.SystemFontOfSize(UIFont.LabelFontSize), tableView.Frame.Size.Width - 15 - 30 - 15);
}
CGSize maximumLabelSize = CGSizeMake(label.frame.size.width, FLT_MAX);
CGSize expectedLabelSize = [label sizeThatFits:maximumLabelSize];
float heightUse = expectedLabelSize.height;
Try this syntax:
NSAttributedString *attributedText =
[[NSAttributedString alloc] initWithString:text
attributes:#{NSFontAttributeName: font}];
None of this worked for me in ios 7. Here is what I ended up doing. I put this in my custom cell class and call the method in my heightForCellAtIndexPath method.
My cell looks similar to the description cell when viewing an app in the app store.
First in the storyboard, set your label to 'attributedText', set the number of lines to 0 (which will resize the label automatically (ios 6+ only)) and set it to word wrap.
Then i just add up all the heights of the content of the cell in my custom Cell Class. In my case I have a Label at the top that always says "Description" (_descriptionHeadingLabel), a smaller label that is variable in size that contains the actual description (_descriptionLabel) a constraint from the top of the cell to the heading (_descriptionHeadingLabelTopConstraint). I also added 3 to space out the bottom a little bit (about the same amount apple places on the subtitle type cell.)
- (CGFloat)calculateHeight
{
CGFloat width = _descriptionLabel.frame.size.width;
NSAttributedString *attributedText = _descriptionLabel.attributedText;
CGRect rect = [attributedText boundingRectWithSize:(CGSize){width, CGFLOAT_MAX} options: NSStringDrawingUsesLineFragmentOrigin context:nil];
return rect.size.height + _descriptionHeadingLabel.frame.size.height + _descriptionHeadingLabelTopConstraint.constant + 3;
}
And in my Table View delegate:
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath;
{
if (indexPath.row == 0) {
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:#"descriptionCell"];
DescriptionCell *descriptionCell = (DescriptionCell *)cell;
NSString *text = [_event objectForKey:#"description"];
descriptionCell.descriptionLabel.text = text;
return [descriptionCell calculateHeight];
}
return 44.0f;
}
You can change the if statement to be a little 'smarter' and actually get the cell identifier from some sort of data source. In my case the cells are going to be hard coded since there will be fixed amount of them in a specific order.

NSButton how to color the text

on OSX I have an NSButton with a pretty dark image and unfortunately it is not possible to change the color using the attributes inspector. See picture the big black button, the text is Go.
Any clues for a possibility to change the text color? I looked in to the NSButton class but there is no method to do that. I´m aware that I could make the image with white font but that is not what I want to do.
Greetings from Switzerland, Ronald Hofmann
--- 
Here is two other solutions:
http://denis-druz.okis.ru/news.534557.Text-Color-in-NSButton.html
solution 1:
-(void)awakeFromNib
{
NSColor *color = [NSColor greenColor];
NSMutableAttributedString *colorTitle = [[NSMutableAttributedString alloc] initWithAttributedString:[button attributedTitle]];
NSRange titleRange = NSMakeRange(0, [colorTitle length]);
[colorTitle addAttribute:NSForegroundColorAttributeName value:color range:titleRange];
[button setAttributedTitle:colorTitle];
}
solution 2:
in *.m file:
- (void)setButtonTitleFor:(NSButton*)button toString:(NSString*)title withColor:(NSColor*)color
{
NSMutableParagraphStyle *style = [[NSMutableParagraphStyle alloc] init];
[style setAlignment:NSCenterTextAlignment];
NSDictionary *attrsDictionary = [NSDictionary dictionaryWithObjectsAndKeys:color, NSForegroundColorAttributeName, style, NSParagraphStyleAttributeName, nil];
NSAttributedString *attrString = [[NSAttributedString alloc]initWithString:title attributes:attrsDictionary];
[button setAttributedTitle:attrString];
}
-(void)awakeFromNib
{
NSString *title = #"+Add page";
NSColor *color = [NSColor greenColor];
[self setButtonTitleFor:button toString:title withColor:color];
}
My solution:
.h
IB_DESIGNABLE
#interface DVButton : NSButton
#property (nonatomic, strong) IBInspectable NSColor *BGColor;
#property (nonatomic, strong) IBInspectable NSColor *TextColor;
#end
.m
#implementation DVButton
- (void)awakeFromNib
{
if (self.TextColor)
{
NSMutableParagraphStyle *style = [[NSMutableParagraphStyle alloc] init];
[style setAlignment:NSCenterTextAlignment];
NSDictionary *attrsDictionary = [NSDictionary dictionaryWithObjectsAndKeys:
self.TextColor, NSForegroundColorAttributeName,
self.font, NSFontAttributeName,
style, NSParagraphStyleAttributeName, nil];
NSAttributedString *attrString = [[NSAttributedString alloc]initWithString:self.title attributes:attrsDictionary];
[self setAttributedTitle:attrString];
}
}
- (void)drawRect:(NSRect)dirtyRect
{
if (self.BGColor)
{
// add a background colour
[self.BGColor setFill];
NSRectFill(dirtyRect);
}
[super drawRect:dirtyRect];
}
#end
And here’s a Swift 3 version:
import Cocoa
#IBDesignable
class DVButton: NSButton
{
#IBInspectable var bgColor: NSColor?
#IBInspectable var textColor: NSColor?
override func awakeFromNib()
{
if let textColor = textColor, let font = font
{
let style = NSMutableParagraphStyle()
style.alignment = .center
let attributes =
[
NSForegroundColorAttributeName: textColor,
NSFontAttributeName: font,
NSParagraphStyleAttributeName: style
] as [String : Any]
let attributedTitle = NSAttributedString(string: title, attributes: attributes)
self.attributedTitle = attributedTitle
}
}
override func draw(_ dirtyRect: NSRect)
{
if let bgColor = bgColor
{
bgColor.setFill()
NSRectFill(dirtyRect)
}
super.draw(dirtyRect)
}
}
and Swift 4.0 version:
import Cocoa
#IBDesignable
class Button: NSButton
{
#IBInspectable var bgColor: NSColor?
#IBInspectable var textColor: NSColor?
override func awakeFromNib()
{
if let textColor = textColor, let font = font
{
let style = NSMutableParagraphStyle()
style.alignment = .center
let attributes =
[
NSAttributedStringKey.foregroundColor: textColor,
NSAttributedStringKey.font: font,
NSAttributedStringKey.paragraphStyle: style
] as [NSAttributedStringKey : Any]
let attributedTitle = NSAttributedString(string: title, attributes: attributes)
self.attributedTitle = attributedTitle
}
}
override func draw(_ dirtyRect: NSRect)
{
if let bgColor = bgColor
{
bgColor.setFill()
__NSRectFill(dirtyRect)
}
super.draw(dirtyRect)
}
}
This is how I get it done in Swift 4
#IBOutlet weak var myButton: NSButton!
// create the attributed string
let myString = "My Button Title"
let myAttribute = [ NSAttributedStringKey.foregroundColor: NSColor.blue ]
let myAttrString = NSAttributedString(string: myString, attributes: myAttribute)
// assign it to the button
myButton.attributedTitle = myAttrString
Apple have code for setting the text colour of an NSButton as part of the Popover example.
Below is the crux of the example (modified slightly for this post, untested):
NSButton *button = ...;
NSMutableAttributedString *attrTitle =
[[NSMutableAttributedString alloc] initWithString:#"Make Me Red"];
NSUInteger len = [attrTitle length];
NSRange range = NSMakeRange(0, len);
[attrTitle addAttribute:NSForegroundColorAttributeName value:[NSColor redColor] range:range];
[attrTitle fixAttributesInRange:range];
[button setAttributedTitle:attrTitle];
Note that the call to fixAttributesInRange: seems to be important (an AppKit extension), but I can't find documentation as to why that is the case. The only concern I have with using attributed strings in an NSButton is if an image is also defined for the button (such as an icon), the attributed string will occupy a large rectangle and push the image to the edge of the button. Something to bear in mind.
Otherwise it seems the best way is to make your own drawRect: override instead, which has many other pitfalls that are outside the scope of this question.
I've created a NSButton subclass called FlatButton that makes it super-easy to change the text color in the Attributes Inspector of Interface Builder like you are asking for. It should provide a simple and extensive solution to your problem.
It also exposes other relevant styling attributes such as color and shape.
You'll find it here: https://github.com/OskarGroth/FlatButton
Add a category on the NSButton and simply set the color to what you want, and preseve the existing attributes, since the title can be centered, left aligned etc
#implementation NSButton (NSButton_IDDAppKit)
- (NSColor*)titleTextColor {
return [NSColor redColor];
}
- (void)setTitleTextColor:(NSColor*)aColor {
NSMutableAttributedString* attributedString = [[NSMutableAttributedString alloc] initWithAttributedString:self.attributedTitle];
NSString* title = self.title;
NSRange range = NSMakeRange(0.0, self.title.length);
[attributedString addAttribute:NSForegroundColorAttributeName value:aColor range:range];
[self setAttributedTitle:attributedString];
[attributedString release];
}
#end
A really simple, reusable solution without subclassing NSButton:
[self setButton:self.myButton fontColor:[NSColor whiteColor]] ;
-(void) setButton:(NSButton *)button fontColor:(NSColor *)color {
NSMutableAttributedString *colorTitle = [[NSMutableAttributedString alloc] initWithAttributedString:[button attributedTitle]];
[colorTitle addAttribute:NSForegroundColorAttributeName value:color range:NSMakeRange(0, button.attributedTitle.length)];
[button setAttributedTitle:colorTitle];
}
When your target is macOS 10.14 or newer, you can use the new contentTintColor property of the NSButton control to set the text color.
button.contentTintColor = NSColor.systemGreenColor;
If deployment Target > 10.14,you can use contentTintColor set color.
/** Applies a tint color to template image and text content, in combination with other theme-appropriate effects. Only applicable to borderless buttons. A nil value indicates the standard set of effects without color modification. The default value is nil. Non-template images and attributed string values are not affected by the contentTintColor. */
#available(macOS 10.14, *)
#NSCopying open var contentTintColor: NSColor?
sample code:
var confirmBtn = NSButton()
confirmBtn.title = "color"
confirmBtn.contentTintColor = NSColor.red
Swift 5
You can use this extension:
extension NSButton {
#IBInspectable var titleColor: NSColor? {
get {
return NSColor.white
}
set {
let pstyle = NSMutableParagraphStyle()
pstyle.alignment = .center
self.attributedTitle = NSAttributedString(
string: self.title,
attributes: [ NSAttributedString.Key.foregroundColor :newValue, NSAttributedString.Key.paragraphStyle: pstyle]
)
}
}
}
Now you can set title color in storyboard easily. Cheers!
NSColor color = NSColor.White;
NSMutableAttributedString colorTitle = new NSMutableAttributedString (cb.Cell.Title);
NSRange titleRange = new NSRange (0, (nint)cb.Cell.Title.Length);
colorTitle.AddAttribute (NSStringAttributeKey.ForegroundColor, color, titleRange);
cb.Cell.AttributedTitle = colorTitle;
Using the info above, I wrote a NSButton extension that sets the foreground color, along with the system font and text alignment.
This is for Cocoa on Swift 4.x, but could be easily adjusted for iOS.
import Cocoa
extension NSButton {
func setAttributes(foreground: NSColor? = nil, fontSize: CGFloat = -1.0, alignment: NSTextAlignment? = nil) {
var attributes: [NSAttributedStringKey: Any] = [:]
if let foreground = foreground {
attributes[NSAttributedStringKey.foregroundColor] = foreground
}
if fontSize != -1 {
attributes[NSAttributedStringKey.font] = NSFont.systemFont(ofSize: fontSize)
}
if let alignment = alignment {
let paragraph = NSMutableParagraphStyle()
paragraph.alignment = alignment
attributes[NSAttributedStringKey.paragraphStyle] = paragraph
}
let attributed = NSAttributedString(string: self.title, attributes: attributes)
self.attributedTitle = attributed
}
}
Swift 4.2 version of David Boyd's solution
extension NSButton {
func setAttributes(foreground: NSColor? = nil, fontSize: CGFloat = -1.0, alignment: NSTextAlignment? = nil) {
var attributes: [NSAttributedString.Key: Any] = [:]
if let foreground = foreground {
attributes[NSAttributedString.Key.foregroundColor] = foreground
}
if fontSize != -1 {
attributes[NSAttributedString.Key.font] = NSFont.systemFont(ofSize: fontSize)
}
if let alignment = alignment {
let paragraph = NSMutableParagraphStyle()
paragraph.alignment = alignment
attributes[NSAttributedString.Key.paragraphStyle] = paragraph
}
let attributed = NSAttributedString(string: self.title, attributes: attributes)
self.attributedTitle = attributed
}
}