NSAttributedString Strikethrough does not draw over entire length of text - nstableview

I am inserting an NSAttributedString into an NSTableView, and depending on it's content I am adding attributes to its style dictionary.
- (id)tableView:(NSTableView *)aTableView objectValueForTableColumn:(NSTableColumn *)aTableColumn row:(NSInteger)rowIndex {
NSAttributedString *theValue;
NSMutableDictionary *attributes = [[NSMutableDictionary alloc] init];
[attributes setObject:[NSFont systemFontOfSize:11] forKey:NSFontAttributeName];
currentObject = [ticketsDataSource objectAtIndex:rowIndex];
if ([[currentObject valueForKey:aTableColumn.identifier] isKindOfClass:([NSString class])]) {
NSString *currentValue = [currentObject valueForKey:aTableColumn.identifier];
if ([currentValue isEqualToString:#"resolved"]) {
[attributes setObject:[NSColor colorWithCalibratedRed:0.344 green:0.619 blue:0.000 alpha:1.000] forKey:NSForegroundColorAttributeName];
}
if ([previousValue isEqualToString:#"resolved"]) {
[attributes setObject:[NSNumber numberWithInteger:NSUnderlinePatternSolid | NSUnderlineStyleSingle] forKey:NSStrikethroughStyleAttributeName];
}
theValue = [[NSAttributedString alloc] initWithString:currentValue attributes:attributes];
previousValue = currentValue;
}
As you can see, basically what happens is when it writes a string called "resolved" it knows that the very next column, which is the title column, gets a strikethrough. It works just fine, but for whatever reason, the strikethrough is not drawing over the entirety of the text!
Here is an image:
What is going on here?

Yet another weirdness with layer-backing. So I turned it off :(

Related

NSAttributedString drawRect doesn't draw images on-screen on Mojave

I have a working app that draws NSAttributedStrings into a custom view. The NSAttributedStrings can included embedded images. This works on versions of macOS prior to Mojave. The app can display the strings on screen, print them, and save them to image files.
This is apparently broken under Mojave. Weirdly, printing and saving to image files still works; but on-screen, the strings display only the text and not the embedded images. Proper space is left for the images, but that space is blank.
I've tested by building a small app that shows a window with an NSTextField (a label) and a custom view. It makes a single NSAttributedString with an embedded image. It applies that string to the attributedStringValue of the label, and also calls drawInRect: on the same string in the drawRect: method of the custom view. In the label, the string is displayed correctly, image and all. But in the custom view, only the text appears, and the space where the image should be is blank.
Anybody got a clue why this is happening on Mojave but not on earlier versions of macOS?
Here is the code that makes the string (and caches it, for re-use):
static NSMutableAttributedString* sgAttrString = nil;
/*
* Creates an attributed string the first time it's called,
* then returns that same string each time it's called.
*/
+ (NSAttributedString*)getAttributedString
{
if (sgAttrString == nil)
{
NSFont* font = [NSFont fontWithName:#"Helvetica" size:24.0];
NSDictionary *attrs = #{
NSFontAttributeName: font
};
sgAttrString = [[NSMutableAttributedString alloc] initWithString:#"Daisy: " attributes:attrs];
NSImage* daisy = [NSImage imageNamed:#"daisy.png"];
[daisy setSize:NSMakeSize(24,24)];
NSTextAttachment *attachment = [[NSTextAttachment alloc] init];
// I'm aware that attachment.image is available only on macOS 10.11 and later.
// It's not an issue in my real project.
attachment.image = daisy;
NSMutableAttributedString* imageStr = [[NSMutableAttributedString alloc] init];
[imageStr setAttributedString:[NSAttributedString attributedStringWithAttachment:attachment]];
[sgAttrString appendAttributedString:imageStr];
[sgAttrString appendAttributedString: [[NSAttributedString alloc] initWithString:#" !!" attributes:attrs]];
}
return sgAttrString;
}
Here is the code that applies the string to the NSTextField:
NSAttributedString* str = [Utilities getAttributedString];
self.label.attributedStringValue = str;
And here is the code that draws the string in a custom NSView:
NSAttributedString* str = [Utilities getAttributedString];
[str drawInRect:NSMakeRect(50,50, 300, 40)];
Again, this behavior seems to occur only in Mojave! Thanks in advance for any help.

watchkit WKAlertAction openSystemURL

I am trying to show the User different Phone Numbers on Apple Watch and he clicks on one than phone call alert should appear. I'll do it like this but the Alert is just dismissed without call action:
NSMutableArray *tempArray = [[NSMutableArray alloc] initWithCapacity:0];
WKExtension *myExt = [WKExtension sharedExtension];
for (NSString *phone in arr) {
NSString *tel = [NSString stringWithFormat:#"tel:%#",phone];
WKAlertAction *act = [WKAlertAction actionWithTitle:tel style:WKAlertActionStyleDefault handler:^(void){
[myExt openSystemURL:[NSURL URLWithString:phone1]];
}];
[tempArray addObject:act];
}
NSString *titleMessage = #"Call";
NSString *textMessage = #"Please select the number you want to call.";
NSString *cancel = #"Cancel";
WKAlertAction *act = [WKAlertAction actionWithTitle:cancel style:WKAlertActionStyleDestructive handler:^(void){
}];
[tempArray addObject:act];
[self presentAlertControllerWithTitle:titleMessage message:textMessage preferredStyle:WKAlertControllerStyleAlert actions:tempArray];
Buttons are shown as expected and the Handler is also called with the correct Phone Number. But it does not openSystemURL. Does somebody know why and how to fix? Thanks!
I think you forgot to add "tel" scheme ,Use below code :
[WKAlertAction actionWithTitle:#"tel" style:WKAlertActionStyleDefault handler:^(void){
[[WKExtension sharedExtension]openSystemURL:[NSURL URLWithString:[NSString stringWithFormat:#"tel:%#",#"YOUR NUMBER"]]];
}];
About Apple URL Schemes

NSTableCell setDataCell disables editing

I have a NSTableView which I dynamically add columns. I've just added a call to setDataCell to customize my cell. The code looks like:
for(NSUInteger columnIndex = 0; columnIndex < resultSet.columNames.count; ++columnIndex)
{
NSTableColumn * newColumn = [[NSTableColumn alloc] initWithIdentifier: [NSString stringWithFormat: #"%ld", columnIndex]];
[newColumn.headerCell setAlignment: NSCenterTextAlignment];
[newColumn.headerCell setStringValue: resultSet.columNames[columnIndex]];
[newColumn setDataCell: [[HSDisclosureTextFieldCell alloc] init]];
[newColumn setEditable: YES];
[resultsTableView addTableColumn: newColumn];
if(newColumn.width < 60) [newColumn sizeToFit];
} // End of column loop
If I remove the call to setDataCell then I can still double click my entry and edit it.
I have minimized the HSDisclosureTextFieldCell contains the following (no overrides):
#interface HSDisclosureTextFieldCell : NSTextFieldCell
{
}
But I still cannot double click to edit the field anymore.
Any ideas where I'm going wrong?
Turns out even though I am doing:
[newColumn setEditable: YES];
and have:
- (BOOL)tableView:(NSTableView *)tableView shouldEditTableColumn:(NSTableColumn *)tableColumn row:(NSInteger)row
implemented, I needed to switch my setDataCell to be:
HSDisclosureTextFieldCell * cell = [[HSDisclosureTextFieldCell alloc] init];
[cell setEditable: YES];
[newColumn setDataCell: cell];
Now that I have done that, everything seems to be working properly.

View-based NSOutlineView without NIB?

NSOutlineView is a subclass of NSTableView. And currently, NSTableView supports two implementations.
Cell-based.
View-based.
To make OSX 10.8 Finder style side bar (with automatic gray Icon styling), need to use view-based table view with source-list highlight style.
With NIBs, this is typical job. Nothing hard. (see SidebarDemo) But I want to avoid any NIBs or Interface Builder. I want make the side bar purely programmatically.
In this case, I have big problem. AFAIK, there's no way to supply prototype view for specific cell. When I open .xib file, I see <tableColumn> is containing <prototypeCellViews>. And this specifies what view will be used for the column. I can't find how to set this programmatically using public API.
As a workaround, I tried to make cell manually using -[NSTableView makeViewWithIdentifier:owner:] and -[NSTableView viewAtColumn:row:makeIfNecessary:], but none of them returns view instance. I created a NSTableCellView, but it doesn't have image-view and text-field instances. And I also tried to set them, but the fields are marked as assign so the instances deallocated immediately. I tried to keep it by forcing retaining them, but it doesn't work. NSTableView doesn't manage them, so I am sure that table view don't like my implementation.
I believe there's a property to set this prototype-view for a column. But I can't find them. Where can I find the property and make system-default NSOutlineView with source-list style programmatically?
If you follow the example in SidebarDemo, they use a subclass of NSTableCellView for the detail rows. In order to emulate the InterfaceBuilder mojo, you can hook everything together in the constructor. The rest is the same as the demo (see outlineView:viewForTableColumn:item:).
#interface SCTableCellView : NSTableCellView
#end
#implementation SCTableCellView
- (id)initWithFrame:(NSRect)frameRect {
self = [super initWithFrame:frameRect];
[self setAutoresizingMask:NSViewWidthSizable];
NSImageView* iv = [[NSImageView alloc] initWithFrame:NSMakeRect(0, 6, 16, 16)];
NSTextField* tf = [[NSTextField alloc] initWithFrame:NSMakeRect(21, 6, 200, 14)];
NSButton* btn = [[NSButton alloc] initWithFrame:NSMakeRect(0, 3, 16, 16)];
[iv setImageScaling:NSImageScaleProportionallyUpOrDown];
[iv setImageAlignment:NSImageAlignCenter];
[tf setBordered:NO];
[tf setDrawsBackground:NO];
[[btn cell] setControlSize:NSSmallControlSize];
[[btn cell] setBezelStyle:NSInlineBezelStyle];
[[btn cell] setButtonType:NSMomentaryPushInButton];
[[btn cell] setFont:[NSFont boldSystemFontOfSize:10]];
[[btn cell] setAlignment:NSCenterTextAlignment];
[self setImageView:iv];
[self setTextField:tf];
[self addSubview:iv];
[self addSubview:tf];
[self addSubview:btn];
return self;
}
- (NSButton*)button {
return [[self subviews] objectAtIndex:2];
}
- (void)viewWillDraw {
[super viewWillDraw];
NSButton* btn = [self button];
...
Here's #jeberle's code re-written in Swift 4 (five years later!):
class ProgrammaticTableCellView: NSTableCellView {
override init(frame frameRect: NSRect) {
super.init(frame: frameRect)
self.autoresizingMask = .width
let iv: NSImageView = NSImageView(frame: NSMakeRect(0, 6, 16, 16))
let tf: NSTextField = NSTextField(frame: NSMakeRect(21, 6, 200, 14))
let btn: NSButton = NSButton(frame: NSMakeRect(0, 3, 16, 16))
iv.imageScaling = .scaleProportionallyUpOrDown
iv.imageAlignment = .alignCenter
tf.isBordered = false
tf.drawsBackground = false
btn.cell?.controlSize = .small
// btn.bezelStyle = .inline // Deprecated?
btn.cell?.isBezeled = true // Closest property I can find.
// btn.cell?.setButtonType(.momentaryPushIn) // Deprecated?
btn.setButtonType(.momentaryPushIn)
btn.cell?.font = NSFont.boldSystemFont(ofSize: 10)
btn.cell?.alignment = .center
self.imageView = iv
self.textField = tf
self.addSubview(iv)
self.addSubview(tf)
self.addSubview(btn)
}
required init?(coder decoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
var button: NSButton {
get {
return self.subviews[2] as! NSButton
}
}
}
Edit: I found a link (that will inevitably rot away – it was last revised in 2011) to Apple's SidebarDemo that #jeberle based his code on.
In addition to #jeberle 's answer, I need to note something more.
The key to keep the text-field and image-view is adding them as subviews of the NSTableCellView.
Set NSTableView.rowSizeStyle to a proper value (non-Custom which is default value) to make the table-view layout them automatically. Otherwise, you have to layout them completely yourself.
Do not touch frame and autoresizing stuffs if you want to use predefined NSTableViewRowSizeStyle value. Otherwise, the layout might be broken.
You can adjust row-height by providing private func outlineView(outlineView: NSOutlineView, heightOfRowByItem item: AnyObject) -> CGFloat delegate method. Setting NSTableView.rowHeight is not a good idea because it needs NSTableView.rowSizeStyle set to Custom which will turn off cell text/image layout management provided by default.
You can reuse row/cell views by settings NSView.identifier property. (example)

Core Text - NSAttributedString line height done right?

I'm completely in the dark with Core Text's line spacing. I'm using NSAttributedString and I specify the following attributes on it:
- kCTFontAttributeName
- kCTParagraphStyleAttributeName
From this the CTFrameSetter gets created and drawn to context.
In the paragraph style attribute I'd like to specify the height of the lines.
When I use kCTParagraphStyleSpecifierLineHeightMultiple each line receives padding at the top of the text, instead of the text being displayed in the middle of this height.
When I use kCTParagraphStyleSpecifierLineSpacing a padding is added to the bottom of the text.
Please help me achieve a specified line height with the text(glyphs) in the middle of that height, instead of the text sitting either at the bottom or the top of the line.
Is this not possible without going down the route of explicitly creating CTLine 's and so forth?
Objective-C
NSInteger strLength = [myString length];
NSMutableParagraphStyle *style = [[NSMutableParagraphStyle alloc] init];
[style setLineSpacing:24];
[attString addAttribute:NSParagraphStyleAttributeName
value:style
range:NSMakeRange(0, strLength)];
Swift 5
let strLength = myString.length()
var style = NSMutableParagraphStyle()
style.lineSpacing = 24
attString.addAttribute(.paragraphStyle, value: style, range: NSRange(location: 0, length: strLength))
I'm still not 100% confident in my following statements, but it seems to make sense. Please correct me where I am wrong.
The line height (leading) refers to the distance between the baselines of successive lines of type. The baseline here can be interpreted as the imaginary line which the text sits on.
Spacing is the space between lines. The space appears after the line of text.
I ended up using the following solution to my problem:
// NOT SURE WHAT THE THEORY BEHIND THIS FACTOR IS. WAS FOUND VIA TRIAL AND ERROR.
CGFloat factor = 14.5/30.5;
CGFloat floatValues[4];
floatValues[0] = self.lineHeight * factor/(factor + 1);
floatValues[1] = self.lineHeight/(factor + 1);
floatValues[2] = self.lineHeight;
This matrix is used with the paragraph style parameter for NSAttributedString:
CTParagraphStyleSetting paragraphStyle[3];
paragraphStyle[0].spec = kCTParagraphStyleSpecifierLineSpacing;
paragraphStyle[0].valueSize = sizeof(CGFloat);
paragraphStyle[0].value = &floatValues[0];
paragraphStyle[1].spec = kCTParagraphStyleSpecifierMinimumLineHeight;
paragraphStyle[1].valueSize = sizeof(CGFloat);
paragraphStyle[1].value = &floatValues[1];
paragraphStyle[2].spec = kCTParagraphStyleSpecifierMaximumLineHeight;
paragraphStyle[2].valueSize = sizeof(CGFloat);
paragraphStyle[2].value = &floatValues[2];
CTParagraphStyleRef style = CTParagraphStyleCreate((const CTParagraphStyleSetting*) &paragraphStyle, 3);
[attributedString addAttribute:(NSString*)kCTParagraphStyleAttributeName value:(id)style range:NSMakeRange(0, [string length])];
CFRelease(style);
Hope this helps someone. I'll update this answer as I discover more relevant information.
In Swift 3:
let textFont = UIFont(name: "Helvetica Bold", size: 20)!
let textColor = UIColor(white: 1, alpha: 1) // White
let paragraphStyle = NSMutableParagraphStyle()
paragraphStyle.paragraphSpacing = 20 // Paragraph Spacing
paragraphStyle.lineSpacing = 40 // Line Spacing
let textFontAttributes = [
NSFontAttributeName: textFont,
NSForegroundColorAttributeName: textColor,
NSParagraphStyleAttributeName: paragraphStyle
] as [String : Any]
You can set/update line spacing and line height multiple from storyboard as well as programatically.
From Interface Builder:
Programmatically:
SWift 4
extension UILabel {
// Pass value for any one of both parameters and see result
func setLineSpacing(lineSpacing: CGFloat = 0.0, lineHeightMultiple: CGFloat = 0.0) {
guard let labelText = self.text else { return }
let paragraphStyle = NSMutableParagraphStyle()
paragraphStyle.lineSpacing = lineSpacing
paragraphStyle.lineHeightMultiple = lineHeightMultiple
let attributedString:NSMutableAttributedString
if let labelattributedText = self.attributedText {
attributedString = NSMutableAttributedString(attributedString: labelattributedText)
} else {
attributedString = NSMutableAttributedString(string: labelText)
}
// Line spacing attribute
// Swift 4.2++
attributedString.addAttribute(NSAttributedString.Key.paragraphStyle, value:paragraphStyle, range:NSMakeRange(0, attributedString.length))
// Swift 4.1--
attributedString.addAttribute(NSAttributedStringKey.paragraphStyle, value:paragraphStyle, range:NSMakeRange(0, attributedString.length))
self.attributedText = attributedString
}
}
Now call extension function
let label = UILabel()
let stringValue = "How to\ncontrol\nthe\nline spacing\nin UILabel"
// Pass value for any one argument - lineSpacing or lineHeightMultiple
label.setLineSpacing(lineSpacing: 2.0) . // try values 1.0 to 5.0
// or try lineHeightMultiple
//label.setLineSpacing(lineHeightMultiple = 2.0) // try values 0.5 to 2.0
Or using label instance (Just copy & execute this code to see result)
let label = UILabel()
let stringValue = "How to\ncontrol\nthe\nline spacing\nin UILabel"
let attrString = NSMutableAttributedString(string: stringValue)
var style = NSMutableParagraphStyle()
style.lineSpacing = 24 // change line spacing between paragraph like 36 or 48
style.minimumLineHeight = 20 // change line spacing between each line like 30 or 40
// Swift 4.2++
// Line spacing attribute
attrString.addAttribute(NSAttributedString.Key.paragraphStyle, value: style, range: NSRange(location: 0, length: stringValue.characters.count))
// Character spacing attribute
attrString.addAttribute(NSAttributedString.Key.kern, value: 2, range: NSMakeRange(0, attrString.length))
// Swift 4.1--
// Line spacing attribute
attrString.addAttribute(NSAttributedStringKey.paragraphStyle, value: style, range: NSRange(location: 0, length: stringValue.characters.count))
// Character spacing attribute
attrString.addAttribute(NSAttributedStringKey.kern, value: 2, range: NSMakeRange(0, attrString.length))
label.attributedText = attrString
Swift 3
let label = UILabel()
let stringValue = "How to\ncontrol\nthe\nline spacing\nin UILabel"
let attrString = NSMutableAttributedString(string: stringValue)
var style = NSMutableParagraphStyle()
style.lineSpacing = 24 // change line spacing between paragraph like 36 or 48
style.minimumLineHeight = 20 // change line spacing between each line like 30 or 40
attrString.addAttribute(NSParagraphStyleAttributeName, value: style, range: NSRange(location: 0, length: stringValue.characters.count))
label.attributedText = attrString
I tried all these answers, but to really get the EXACT line height that usually comes in design files from Sketch or Zeplin then you need to:
let ps = NSMutableParagraphStyle()
ps.minimumLineHeight = 34
ps.maximumLineHeight = 34
let attrText = NSAttributedString(
string: "Your long multiline text that will have exact line height spacing",
attributes: [
.paragraphStyle: ps
]
)
someLabel.attributedText = attrText
someLabel.numberOfLines = 2
...
I made an extension for this, see below. With the extension you can just set the line height like so:
let label = UILabel()
label.lineHeight = 19
This is the extension:
// Put this in a file called UILabel+Lineheight.swift, or whatever else you want to call it
import UIKit
extension UILabel {
var lineHeight: CGFloat {
set {
let paragraphStyle = NSMutableParagraphStyle()
paragraphStyle.minimumLineHeight = newValue
paragraphStyle.maximumLineHeight = newValue
_setAttribute(key: NSAttributedString.Key.paragraphStyle, value: paragraphStyle)
}
get {
let paragraphStyle = _getAttribute(key: NSAttributedString.Key.paragraphStyle) as? NSParagraphStyle
return paragraphStyle?.minimumLineHeight ?? 0
}
}
func _getAttribute(key: NSAttributedString.Key) -> Any? {
return attributedText?.attribute(key, at: 0, effectiveRange: .none)
}
func _setAttribute(key: NSAttributedString.Key, value: Any) {
let attributedString: NSMutableAttributedString!
if let currentAttrString = attributedText {
attributedString = NSMutableAttributedString(attributedString: currentAttrString)
} else {
attributedString = NSMutableAttributedString(string: text ?? "")
text = nil
}
attributedString.addAttribute(key,
value: value,
range: NSRange(location: 0, length: attributedString.length))
attributedText = attributedString
}
}
Notes:
I don't like line height multiples. My design document contains a height, like 20, not a multiple.
lineSpacing as in some other answers is something totally different. Not what you want.
The reason there's an extra _set/_getAttribute method in there is that I use the same method for setting letter spacing. Could also be used for any other NSAttributedString values but seems like I'm good with just letter spacing (kerning in Swift/UIKit) and line height.
There are two properties of NSParagraphStyle that modify the height between successive text baselines in the same paragraph: lineSpacing and lineHeightMultiple. #Schoob is right that a lineHeightMultiple above 1.0 adds additional space above the text, while a lineSpacing above 0.0 adds space below the text. This diagram shows how the various dimensions are related.
To get the text to stay centred the aim is therefore to specify one in terms of the other, in such a way that any 'padding' we add by one attribute (top/bottom) is balanced by determining the other attribute's padding (bottom/top) to match. In other words, any extra space added is distributed evenly while otherwise preserving the text's existing positioning.
The nice thing is that this way you can choose which attribute you want to specify and then just determine the other:
extension UIFont
{
func lineSpacingToMatch(lineHeightMultiple: CGFloat) -> CGFloat {
return self.lineHeight * (lineHeightMultiple - 1)
}
func lineHeightMultipleToMatch(lineSpacing: CGFloat) -> CGFloat {
return 1 + lineSpacing / self.lineHeight
}
}
From here, other answers show how these two attributes can be set in an NSAttributedString, but this should answer how the two can be related to 'centre' the text.
Swift 4 & 5
extension NSAttributedString {
/// Returns a new instance of NSAttributedString with same contents and attributes with line spacing added.
/// - Parameter spacing: value for spacing you want to assign to the text.
/// - Returns: a new instance of NSAttributedString with given line spacing.
func withLineSpacing(_ spacing: CGFloat) -> NSAttributedString {
let attributedString = NSMutableAttributedString(attributedString: self)
let paragraphStyle = NSMutableParagraphStyle()
paragraphStyle.lineBreakMode = .byTruncatingTail
paragraphStyle.lineSpacing = spacing
attributedString.addAttribute(.paragraphStyle,
value: paragraphStyle,
range: NSRange(location: 0, length: string.count))
return NSAttributedString(attributedString: attributedString)
}
}
This worked for me in Xcode 7.2. iOS 9.2.1. (Swift 2.1.):
dispatch_async(dispatch_get_main_queue()) { () -> Void in
let paragraphStyleWithSpacing = NSMutableParagraphStyle()
paragraphStyleWithSpacing.lineSpacing = 2.0 //CGFloat
let textWithLineSpacing = NSAttributedString(string: str, attributes: [NSParagraphStyleAttributeName : paragraphStyleWithSpacing])
self.MY_TEXT_VIEW_NAME.attributedText = textWithLineSpacing
}
Another way of twerking with a NSAttributedString line position is playing with
baselineOffset attribute:
let contentText = NSMutableAttributedString(
string: "I see\nI'd think it`d be both a notification and a\nplace to see past announcements\nLike a one way chat.")
contentText.addAttribute(.baselineOffset, value: 10, range: NSRange(location: 0, length: 5))
contentText.addAttribute(.baselineOffset, value: -10, range: NSRange(location: 85, length: 20))
Result:
"I see
I'd think it`d be both a notification and a
place to see past announcements
Like a one way chat."
https://stackoverflow.com/a/55876401/4683601

Resources