swift text.draw method messes up CGContext - swift

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).

Related

Strikethrough not working in CoreText on macOS

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.

Swift: NSBezier

This is the code inside my custom view class:
func drawTestingPoint(_ point: CGPoint, target: Int, output: Int) {
let path = NSBezierPath()
path.appendArc(withCenter: point, radius: 5, startAngle: 0, endAngle: 360)
NSColor.black.setStroke()
if target == output {
NSColor.green.setFill()
} else {
NSColor.red.setFill()
}
path.lineWidth = 3
path.fill()
path.stroke()
}
override func draw(_ dirtyRect: NSRect) {
//If I call the drawTestingPoint function here it works
}
Inside my viewDidLoad method in my NSViewController class I set up the custom view and try to draw the testing point:
let size = getDataViewSize()
let origin = CGPoint(x: view.frame.width/2-size.width/2, y: view.frame.height/2-size.height/2)
dataView = DataView(frame: CGRect(origin: origin, size: size))
view.addSubview(dataView)
dataView.drawTestingPoint(CGPoint(x: view.frame.width/2 y: view.frame.height/2), target: target, output: output)
dataView.needsDisplay = true
My problem is that no point is getting drawn. I think there can't be anything wrong with my drawTestingPoint function because when I call it inside my draw(_ dirtyRect: NSRect) function in my custom NSView class, it works. What can I do so I can call this function inside my viewDidLoad function how you can see in the codes snippets above so my point gets drawn
You can't just draw any time you want. Normally you set up a view and implement draw(_:) as you've done. The system calls the draw method when it needs the view to draw its contents. Before calling your draw(_:) method it sets up the drawing context correctly to draw inside your view and clip if you draw outside of the view. That's the bit you're missing.
As a general rule you should NOT draw outside of the view's draw(_:) method. I've done drawing outside of the draw(_:) method so infrequently that I don't remember what you'd need to do to set up the drawing context correctly. (To be fair I do mostly iOS development these days and my MacOS is getting rusty.)
So the short answer is "Don't do that."
EDIT:
Instead, set up your custom view to save the information it needs to draw itself. As others have suggested, when you make changes to the view, set needsDisplay=true on the view. That will cause the system to call the view's draw(_:) method on the next pass through the event loop

Two color items on Tab Bar, Swift

Is is possible to set an item with two colors on the tab bar?
(I'm a beginner in swift)
What I want to achieve:
I want to have something-like a white background for each item in both active and inactive states.
-> Example of what I want
What I have tried so far:
I've been trying by using a png with transparency, white color and black color but it seems that anyhing that is not a transparent pixel is taken as the same color. (The asset I've working with is rendered as a whole color image instead of differentiating black and white)
-> The asset I thought it would work
What I think its the way but don't know how:
(This is an assumption)
I think this could be done by setting an inactive item in the background (first layer, white color) and an active item (second layer, black color) in the same coordinates with the color I want to change:
-> Two assets for Layer 1 and layer 2
I'm using an storyboard with storyboard references to set initial configuration for the assets of both active an inactive states.
I'm setting active state color programatically.
Suggestions to achive this in more elegant ways are welcome.
Thank you :)
Thank you all for your help.
Inspired by #Jake link, I kept searching and found that Xcode has different modes to render image assets.
Render as Template:
The default one, renders images as they were templates. (You can take any image and colorize it programatically, but it does not recognice if it has more than 1 color)
Render as Original:
The other mode, renders images as they are. So it actually recognizes if it has more than 1 color and leave them that way. This allows you to have tab bar items with multiple colors.
How can this property be modified?
Programatically:
(as a property)
tabBarItem.image = tabBarImage.withRenderingMode(.alwaysOriginal)
//or
tabBarItem.image = tabBarImage.withRenderingMode(.alwaysTemplate)
Via Interface Builder:
1. Assets Folder, 2. Select an asset, 3. Attribute Inspector
Render as: Default, Original Image, Template Image.
Subclass your TabbarviewController, and set the color:
tabBar.tintColor = .red
I would probably just draw this in quartz. It's a very simple vector and would be easy to do in few lines of code. It would also react to a change of state and update accordingly. See below for an example UIView that does exactly this:
class TabIcon: UIView {
var enabled: Bool = false {
didSet {
setNeedsDisplay()
}
}
override init(frame: CGRect) {
super.init(frame: frame)
self.backgroundColor = .clear
self.isOpaque = false
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
override func draw(_ rect: CGRect) {
super.draw(rect)
let ctx = UIGraphicsGetCurrentContext()
ctx?.setFillColor(UIColor.white.cgColor)
ctx?.addEllipse(in: rect)
ctx?.fillPath()
ctx?.setFillColor(enabled ? UIColor.purple.cgColor : UIColor.lightGray.cgColor)
ctx?.addEllipse(in: CGRect(x: rect.minX + 5, y: rect.minY + 5, width: rect.width - 10, height: rect.height - 10))
ctx?.fillPath()
ctx?.setFillColor(UIColor.white.cgColor)
ctx?.addEllipse(in: CGRect(x: rect.minX + 15, y: rect.minY + 15, width: rect.width - 30, height: rect.height - 30))
ctx?.fillPath()
}
}
All you have to do is use this view as the tab icon, changing the enabled property when it is selected will automatically trigger a redraw. The result looks like this:
Of course, fine tune the values I have used to match your design exactly. Also note I've hardcoded values, you could determine your frames based on a proportion of the overall size for better reusability.
Hope this helps!

How would I make a simple spinning dash in Swift? that spins on its center like a loader in terminal

I'm trying to create a simple spinning loading dash. I know how to do the loop but I can't seem to make it on a single line. Any ideas?
let loop = 1
while loop > 0 {
// spinning dash
}
I will not provide you with all the code to your question but rather a guideline of what to do. In general, its a two step algorithm.
Draw a line
Perform a 360° rotation of it for a desired time, t
The code posted below implements the first portion. I have added comments and I believe it should be self explanatory. After reviewing it, I'd recommend you read about UIBezierPath.
As for the second part, there are two ways of going about this.
1. Rotate the line itself (recommended)
Should you choose this method, here's a tutorial from Ray Wenderlich which covers it extensively along with the Math behind it. Follow through both portions of the tutorial if possible.
2. Rotate the view encompassing the line
Changing the outer view's background color to clear then rotating itself will give the illusion that the line inside is the one rotated. Here's a video guide for view rotations.
import UIKit
class ViewController: UIViewController
{
override func viewDidLoad()
{
super.viewDidLoad()
// This is the black subview in which the line will be drawn into
let lineView: GeneralDraw = GeneralDraw(frame: CGRect(origin: CGPoint(x: 20, y: 30), size: CGSize(width: 300, height: 300)))
// uncomment this to remove the black colour
// lineView.backgroundColor = .clear
// add this lineView to the mainView
self.view.addSubview(lineView)
}
}
// This handles the drawing inside a given view
class GeneralDraw: UIView
{
override init(frame: CGRect)
{
super.init(frame: frame)
}
required init?(coder aDecoder: NSCoder)
{
fatalError("init(coder:) has not been implemented")
}
override func draw(_ rect: CGRect)
{
let linePath = UIBezierPath()
// start point of the line
linePath.move(to: CGPoint(x:50, y:10))
// end point of the line
linePath.addLine(to: CGPoint(x:200, y:10))
linePath.close()
// cosmetic settings for the line
UIColor.red.set()
linePath.stroke()
linePath.fill()
}
}
I would use a CAReplicatorLayer for this. You start with a layer that draws a horizontal bar and combine it with transforms that show the bar in the other positions. Then you animate the fading out of the bar, with an offset coordinated to the fading.
In this gif, I've deliberately slowed down the animation. (There is a mild glitch at the point where the gif repeats, but ignore that; the real project doesn't have that glitch.)
1. Solution: rotate Images
create a set of images which shows the dash rotating.
set the images to an array. then animate that `UIImageView.startAnimating()
see section "Animating a Sequence of Images" of UIImageView.
2. Solution: standard iOS activity indicator
But better go with the standard UIActivityIndicatorView
see also:
iOS Human Interface Guidelines: Progress Indicators
Reference for UIActivityIndicatorView

How to Scale UIBezierPath to Fit Current View (in Swift)

I've created a class that draws a coffee mug using code I imported from PaintCode and I applied this class to a view. Using #IBDesignable, I can see in my storyboard that the mug is being drawn inside the view, however the overall shape is too big. I could redraw the shape in code so that it fits the current size of the view, but isn't there a way to scale the shape after it is drawn so that as my view changes size on different devices the shape is scaled correctly?
I've looked into CGContextScaleCTM(aRef, <#sx: CGFloat#>, <#sy: CGFloat#>) but I am not sure how to convert the CGRect of my view's bounds to the right scale factor
I didn't want to post all of it, but my drawing code begins like this
bezierPath.moveToPoint(CGPointMake(64.8, 52.81))
bezierPath.addCurveToPoint(CGPointMake(58.89, 43.44), controlPoint1: CGPointMake(64.21, 48.28), controlPoint2: CGPointMake(62.11, 44.95))
bezierPath.addCurveToPoint(CGPointMake(56.82, 42.76), controlPoint1: CGPointMake(58.24, 43.13), controlPoint2: CGPointMake(57.55, 42.9))
This goes on then
bezierPath.closePath()
bezierPath.miterLimit = 4
bezierPath.usesEvenOddFillRule = true;
Then there are are two other chunks of drawing code for drawing two little lines for the coffee steam. I append these two paths to the original bezierPath, then I set a fill color and fill the whole shape.
In code you can just scale your paths as you want using this UIBezierPath swift extension PaintCodeScale.
e.g
bezierPath.fit(into: rect).moveCenter(to: rect.center).fill()
Since I used PaintCode to generate my drawing code, I found a way to implement #dasdom's suggestion using help from the app.
In PaintCode there is a "frame" tool which you can place around your drawing. This enables constraints for your artwork so that the vectors are re-drawn relative to the frame size. The frame is a variable that is exported along with your code when you bring it into Xcode. When I added the drawing code to my class in Xcode and then added the class to my view in Storyboard, Xcode automatically scaled the frame to the view size and thus the drawing code within my class was also autmatically resized to fit my view. Now, the artwork will be automatically re-drawn to fit whatever view I add my class to. The automatic re-sizing may be occurring due to the "Automatically resize subviews" option that is enabled in Storyboard for the view that I have applied my graphics class to.
func scalePath(path: UIBezierPath) -> UIBezierPath {
let w1: CGFloat = path.bounds.size.width
let h1: CGFloat = path.bounds.size.height
let w2: CGFloat = self.frame.width
let h2: CGFloat = self.frame.height
var s: CGFloat = 1.0
// take the smaller one and scale 1:1 to fit (to keep the aspect ratio)
if w2 <= h2 {
s = w2 / w1
} else {
s = h2 / h1
}
path.apply(CGAffineTransform(scaleX: s, y: s))
return (path)
}