Strikethrough is not being displayed, but underline does. The code is as below, its fairly straight forward. When I comment out the underline the text is displayed without a strikethrough, when I comment out the strikethrough and display the underline it works fine. I've tried everything — I must be missing something obvious, the docs say strikeout should work.
I'm running macOS 10.13.6 and Xcode 10.1.
import AppKit
import PlaygroundSupport
class CustomView: NSView {
override func draw(_ dirtyRect: NSRect) {
let attrString = NSAttributedString(
string: "Hello",
attributes: [
//NSAttributedString.Key.underlineStyle: NSUnderlineStyle.thick.rawValue,
NSAttributedString.Key.strikethroughStyle: NSUnderlineStyle.thick.rawValue
]
)
let line = CTLineCreateWithAttributedString(attrString)
// Set text position and draw the line into the graphics context.
let context = (NSGraphicsContext.current?.cgContext)!
context.translateBy(x: 10, y: 10)
CTLineDraw(line, context)
}
}
let customView = CustomView(frame: NSRect(x: 0, y: 0, width: 400, height: 400))
PlaygroundPage.current.liveView = customView
Playground settings. To run this in a Xcode Playground just be sure to change the platform to macOS in the inspector settings (all new playgrounds are set to iOS by default).
CoreText does not have native support for the strikethrough typographic text decoration (but supports underlines just fine as you noted in your question). The full list of string attributes supported by CoreText is described here.
This old, Objective-C based blog post provides the necessary details to implement strikethrough manually using CoreText APIs. Unfortunately, not a quick fix at all.
The higher-level TextKit framework does provide strikethrough support though. To quickly see that working just replace this line in your original code:
CTLineDraw(line, context)
with this instead:
attrString.draw(at: .zero)
And, by the way, that somehow helps validate that there was nothing wrong with your original NSAttributedString to begin with ;)
Of course, depending on how much your code relies on CoreText, switching over to TextKit might be a non-trivial task, so you need to keep that in mind as well.
Related
I created a multi-platform game in Xcode and UIBezierPath gives a "Cannot find 'UIBezierPath' in scope" error. I'm sure I'm just missing importing a kit but I don't know which one.
short answer: if you're trying to use UIBezierPath you need to import UIKit
longer note re "multi-platform": your question doesn't provide much context.. but if you mean you started developing a UIBezierPath for iOS, and then switched to a different platform target (ex: macOS), then yes many of your classes won't work. often, but not always, there is simply an equivalent class for macOS that uses the "NS" prefix instead of the "UI" prefix. (NS is a legacy reference to "Next Step"). so try using AppKit classes like NSBezierPath when you target macOS, instead of UIKit classes.
#if os(macOS)
import AppKit
#elseif os(iOS)
import UIKit
#endif
warning: UI/NS classes frequently have different implementation. so it's normal to have different constructors, function calls, etc. meaning you often have to have two implementations in code using the above if/elseif/endif structure. yes this can be tedious.
UPDATE
Here is an example for drawing an ellipse in both macOS and iOS. note: the macOS version uses a CGMutablePath (since there's not a simple way to construct a SKShapeNode from an NSBezierPath on macOS like there is on iOS). if you do want to use NSBezierPath, you might also try these techniques for converting NSBezierPath to CGPath
override func didMove(to view: SKView) {
let rect = CGRect(x: 0, y: 0, width: 200, height: 200)
#if os(macOS)
let path = CGMutablePath(ellipseIn:rect, transform:nil)
let shape = SKShapeNode(path:path)
#elseif os(iOS)
let path = UIBezierPath(ovalIn: rect)
let shape = SKShapeNode(path: path.cgPath)
#endif
addChild(shape)
}
I have been reading through the various options on how to set the vertical alignment on an NSTextField. I want the text to be displayed in the center and to do it programatically in Swift. Here are the things I have looked so far:
http://www.cocoabuilder.com/archive/cocoa/174994-repositioning-an-nstextfieldcell.html
https://red-sweater.com/blog/148/what-a-difference-a-cell-makes
Vertically Centre Text in NSSecureTextField with subclassing
Get NSTextField contents to scale
vertically align text in a CATextLayer?
One thing I have tried in Swift is to set the following property:
textField.usesSingleLineMode = true
Any tips on the best way to vertically center text would be much appreciated!
This is very hard to do, as Apple makes this very difficult. I achieved it by subclassing NSTextFieldCell and overriding the drawingRectForBounds: method like so:
override func drawingRectForBounds(theRect: NSRect) -> NSRect {
let newRect = NSRect(x: 0, y: (theRect.size.height - 22) / 2, width: theRect.size.width, height: 22)
return super.drawingRectForBounds(newRect)
}
This is just my way to do it, I'm sure there are better ways, which I don't know (yet). And this only works for the standard font size in TextFields (which gives a text height of 22). That's why I hardcoded that. Haven't figured out yet, how to get the height in the cell if you change the font.
Result:
Try this on a playground, it centers the text perfectly, use it on your projects! Hope it helps!
import Cocoa
let cell = NSTableCellView()
cell.frame = NSRect(x: 0, y: 0, width: 100, height: 100)
let tf = NSTextField()
tf.frame = cell.frame
tf.stringValue = "MyTextfield"
tf.alignment = .Center
let stringHeight: CGFloat = tf.attributedStringValue.size().height
let frame = tf.frame
var titleRect: NSRect = tf.cell!.titleRectForBounds(frame)
titleRect.size.height = stringHeight + ( stringHeight - (tf.font!.ascender + tf.font!.descender ) )
titleRect.origin.y = frame.size.height / 2 - tf.lastBaselineOffsetFromBottom - tf.font!.xHeight / 2
tf.frame = titleRect
cell.addSubview(tf)
I have added the NSTextField inside a NSView and centered it.
Another solution was (in an iOS project) to create a UILabel and allow it adjust its size (sizeToFit()) and again embed it inside a UIView.
I personally don't like the calculations in previous answers and the second solution for iOS works for all texts size and row numbers.
I was also facing vertical alignment issue with NSTextField. My requirement involved, rendering a single-line string inside a NSTextField. Additionally,
textfield needed to be resize implying we had programatically resized the font-point-size of the text inside text-field on resize. In this scenario we faced vertical-alignment issues - the mis-alignment was tough to grasp/understand in a straight forward way.
What finally worked:
So, in my scenario a simple,
turn off the "Single Line Mode" in interface builder
for the text-field solved the issue.
The accepted answer works perfectly and here's the Swift3 version.
class VerticallyAlignedTextFieldCell: NSTextFieldCell {
override func drawingRect(forBounds rect: NSRect) -> NSRect {
let newRect = NSRect(x: 0, y: (rect.size.height - 22) / 2, width: rect.size.width, height: 22)
return super.drawingRect(forBounds: newRect)
}
}
I am still learning SwiftUI & dev in general. Anything NSWhatever related still throws me off.
And I know that my issue is just a lack of understanding :D
Rn, I am building a SwiftUI macOS 11 app & it works pretty well... but SwiftUI has its limits.
Now, I want to use a NSTextField instead of SwiftUI's TextField(), bc custom UI etc...
My code:
func makeNSView(context: Context) -> NSTextField {
let textField = FocusAwareTextField()
textField.placeholderAttributedString = NSAttributedString(
string: placeholder,
attributes: [
NSAttributedString.Key.foregroundColor: NSColor(Color.secondary),
NSAttributedString.Key.font: NSFont(name: "SF Pro", size: 32)!
]
)
textField.isBordered = false
textField.delegate = context.coordinator
textField.backgroundColor = NSColor.clear
...
...
...
My issue:
NSAttributedString.Key.font: expects a NSFont.
And the above code builds fine but I'd rather stick to SwiftUI's Dynamic Font System and use Font.largeTitle.bold() instead of manually defining to use SF Pro and a font size.
I know how to convert Color.black to NSColor(Color.black) but didn't find a working example for Fonts.
Also, I'd appreciate it if someone could actually explain what is going on so I can understand, instead of just doing copy & paste.
Thank you so much!
I'm using an NSAttributedString's draw(at:) function inside of an NSView to render some text in a custom font inside my window.
However, the font looks weirdly blurry and "too heavy" when run on a non-retina MacBook (both on the internal display & on an external LCD).
Since I'm able to perfectly reproduce the desired outcome in Sketch on the same machine, I'm assuming this to be an issue with my code.
Here's my code so far:
import Cocoa
class StepNameLabel: NSView {
// Im aware that this is not the intended way of loading Apple's system fonts
// However, this was the best way I could find to make sure that both Sketch
// and the app were using the exact same fonts.
var font = NSFont(name: "SF Pro Text Semibold", size: 22)
override func draw(_ dirtyRect: NSRect) {
super.draw(dirtyRect)
let text = "Choose your Images"
let attributes: [NSAttributedString.Key: Any] = [
.font: font,
.foregroundColor: NSColor.white
]
print(font?.fontName)
let drawableString = NSAttributedString(string: text, attributes: attributes)
frame.size = drawableString.size()
drawableString.draw(at: NSPoint(x: 0, y: 0))
}
}
And here's a screenshot showing the difference between the Sketch-File & the app running on the same display (left: Graphic in Sketch app, right: The output of above code):
The app's code & the Sketch graphic both use Apple's "SF Pro Text" font with a font-weight of "Semibold" at a size of 22 units.
Any help in finding out what's going wrong here would be greatly appreciated.
This might be the infamous 'half pixel' problem. Try:
drawableString.draw(at: NSPoint(x: 0.5, y: 0.5))
There is some information about this here (search the page for 'Points and Pixels').
I am using CoreGraphics to draw single glyphs alongside with primitives in a CGContext. The following code works in a swift playground in XCode 9.2 When started in the playground a small rectangle with twice the letter A should appear at the given coordinates in the playground liveView.
import Cocoa
import PlaygroundSupport
class MyView: NSView {
init(inFrame: CGRect) {
super.init(frame: inFrame)
}
required init?(coder decoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func draw(_ rect: CGRect) {
// setup context properties
let context: CGContext = NSGraphicsContext.current!.cgContext
context.setStrokeColor(CGColor.black)
context.setTextDrawingMode(.fill)
context.setFillColor(CGColor(red: 0.99, green: 0.99, blue: 0.85, alpha: 1))
context.beginPath()
context.addRect(rect)
context.fillPath()
context.setFillColor(.black)
// prepare variables and constants
var font = CTFontCreateWithName("Helvetica" as CFString, 48, nil)
var glyph = CTFontGetGlyphWithName(font, "A" as CFString)
var glyph1Position = CGPoint(x: self.frame.minX, y: self.frame.maxY/2)
var glyph2Position = CGPoint(x: self.frame.minX+150, y: self.frame.maxY/2)
let text = "Hello"
var textOrigin = NSPoint(x: self.frame.minX+50, y: self.frame.maxY/2)
// draw one character
CTFontDrawGlyphs(font, &glyph, &glyph1Position, 1, context)
// *** *** when the next line is uncommented the bug appears *** ***
// text.draw(at: textOrigin)
CTFontDrawGlyphs(font, &glyph, &glyph2Position, 1, context)
}
}
var frameRect = CGRect(x: 0, y: 0, width: 200, height: 100)
PlaygroundPage.current.liveView = MyView(inFrame: frameRect)
Now I want to draw regular text in the same context. However, when the text string is drawn between the drawing of the two glyphs via its own drawing method, the current context seems to be messed up, the second glyph will not show. When the text is drawn after both single glyphs are drawn everything is fine.
So obviously drawing the text seems to have an impact on the current CGContext, but I cannot find out what exactly is happening. I tried the saveGstate() method befor drawing the string and restoring afterwards, but without success.
I also tried using CoreText methods to create an attributed String with CTFramesetterCreateWithAttributedString and showing it with CTFramesetterCreateFrame, it also does not work, here after the creation of the framesetter the context is messed up.
My actual playground is more complex, there the glyphs do not entirely disappear but are shown at a wrong vertical position, but the basic problem - and question is the same:
How can I draw the text into the currentContext whithout any other changes to the context being done in the background?
You need to set the text matrix (the transform applied to text drawing). You should always set this before drawing text, because it isn't part of the graphics state and may get trashed by other drawing code such as AppKit's string drawing extensions.
Add the following before your call to CTFontDrawGlyphs:
context.textMatrix = .identity
This should be called in the initial setup of the context since there is no promise that the text matrix will be identity before calling drawRect. Then, any time you have made calls to something that modifies the text matrix you will need to set it back to what you want (identity in this case, but it could be something else if you wanted to draw in a fancy way).
It is not always obvious what will modify the text matrix. AppKit drawing functions almost always do (though I'm not aware of any documentation indicating this). Core Text functions that modify the context, like CTFrameDraw and CTLineDraw, will generally document this fact with a statement such as:
This call can leave the context in any state and does not flush it after the draw operation.
Similarly CTFontDrawGlyphs warns:
This function modifies graphics state including font, text size, and text matrix if these attributes are specified in font. These attributes are not restored.
As a rule I discourage mixing text drawing systems. Use AppKit or Core Text, but don't mix them. If you pick one and stick to it, then this generally isn't a problem (as long as you initialize the matrix once at the top of drawRect). For example, if you did all the drawing with CTFontDrawGlyphs, you wouldn't need to reset the matrix each time; you'd stay in the Core Text matrix and it'd be fine (which is why this works when you comment out the draw(at:) call).