SwiftUI Canvas Drawing Text - swift

I'm trying to draw labels above lines using a Canvas in SwiftUI. The lines draw at the correction position, however the labels are never drawn above the lines (see screenshot which shows both the "1" and "2" label drawn atop each other).
I use a for loop to get the position data from an array:
Canvas {context, size in
let font = Font.custom("Arial Rounded MT Bold", size: 16)
for line in TLDataModel.lines {
let resolved = context.resolve(Text(line.tickLabel).font(font))
let midPoint = CGPoint(x: line.points.startIndex, y: line.points.endIndex)
context.draw(resolved, at: midPoint, anchor: .center)
print("Line Label: \(line.tickLabel)")
print("..for points: \(line.points)")
var path = Path()
path.addLines(line.points)
context.stroke(path, with: .color(.blue), lineWidth: 1.0)
}
}.frame(width: (400), height: 100, alignment: .leading)
And my class:
class testDataModel: ObservableObject {
#Published var lines: [line] = []
init(){
var nl = line(points: [CGPoint(x: 20.0, y: 0.0), CGPoint(x: 20.0, y: 100.0)], tickLabel: "1")
addLines(newLine: nl)
nl = line(points: [CGPoint(x: 50.0, y: 0.0), CGPoint(x: 50, y: 100.0)], tickLabel: "2")
addLines(newLine: nl)
}
func addLines (newLine: line){
lines.append(newLine)
}
}
public struct line: Identifiable {
public let id = UUID()
let points: [CGPoint]
let tickLabel: String
}

Related

SwiftUI Add Bottom Gradient To Line Graph

I want to add a bottom gradient in my line graph like in the picture. I have the lines plotted out but I am not sure how to actually add the gradient. I was able to do this in UIKit but am not sure how to replicate it for SwiftUI.
SwiftUI Line Plot View:
var endingBalanceChart: some View {
GeometryReader { geometry in
Path { path in
for index in viewModel.endingBalance.indices {
let xPosition: CGFloat = geometry.size.width / CGFloat(viewModel.endingBalance.count) * CGFloat(index + 1)
let yAxis: CGFloat = maxY - minY
let maxYPosition: CGFloat = (1 - CGFloat((Double(viewModel.endingBalance[index].y) - minY) / yAxis)) * geometry.size.height
let yPosition: CGFloat = index == 0 ? 200 : maxYPosition
if index == 0 {
path.move(to: CGPoint(x: 0, y: yPosition))
}
path.addLine(to: CGPoint(x: xPosition, y: yPosition))
}
}
.stroke(Color.blue, style: StrokeStyle(lineWidth: 1, lineCap: .round, lineJoin: .round))
}
}
I tried converting the path in to a UIBezierPath and trying my UIKit implementation but no luck
This was my UIKit implementation:
func addGradient(path: UIBezierPath, hexString: String){
let color = UIColor(hexString: hexString).withAlphaComponent(0.4).cgColor
guard let clippingPath = path.copy() as? UIBezierPath else { return }
clippingPath.addLine(to: CGPoint(x: self.bounds.width, y: self.bounds.height))
clippingPath.addLine(to: CGPoint(x: 0, y: bounds.height))
clippingPath.close()
clippingPath.addClip()
let colors = [color, UIColor.clear.cgColor]
let colorSpace = CGColorSpaceCreateDeviceRGB()
let colorLocations: [CGFloat] = [0.0, 1.0]
guard let gradient = CGGradient(colorsSpace: colorSpace, colors: colors as CFArray, locations: colorLocations) else { return }
guard let context = UIGraphicsGetCurrentContext() else { return }
let startPoint = CGPoint(x: 1, y: 1)
let endPoint = CGPoint(x: 1, y: bounds.maxY)
context.drawLinearGradient(gradient, start: startPoint, end: endPoint, options: .drawsAfterEndLocation)
}
Here would be a pure SwiftUI implementation.
I suggest to convert the var into an own struct that conforms to Shape protocol. Then you can use it for both stroke and fill background.
This has the positive side effect that you don't need a GeometryReader any more, as Shape provides you with the drawing rectangle with func path(in rect: CGRect).
The result looks like this:
let endingBalance: [Double] = [0, 1, 2, 4, 7, 11, 16, 22, 29, 37, 46, 56] // dummy data
struct ContentView: View {
var body: some View {
VStack {
Text("Chart")
EndingBalanceChart()
.stroke(Color.blue, style: StrokeStyle(lineWidth: 1, lineCap: .round, lineJoin: .round)) // line
.background(
EndingBalanceChart(isBackground: true)
.fill(.linearGradient(colors: [.cyan, .clear], startPoint: .top, endPoint: .bottom)) // background fill
)
.frame(height: 200)
.padding()
}
}
}
struct EndingBalanceChart: Shape { // chnaged var to a Shape struct
var isBackground: Bool = false
func path(in rect: CGRect) -> Path {
Path { path in
for index in endingBalance.indices {
let xPosition: CGFloat = rect.width / CGFloat(endingBalance.count) * CGFloat(index + 1)
let maxY = endingBalance.max() ?? 0
let minY = endingBalance.min() ?? 0
let yAxis: CGFloat = maxY - minY
let yPosition: CGFloat = (1 - CGFloat((Double(endingBalance[index]) - minY) / yAxis)) * rect.height
if index == 0 {
path.move(to: CGPoint(x: 0, y: rect.height))
}
path.addLine(to: CGPoint(x: xPosition, y: yPosition))
}
if isBackground { // this is needed so the backkground shape is filled correctly (closing the shape)
path.addLine(to: CGPoint(x: rect.width, y: rect.height))
}
}
}
}
Here is an approach:
let amountPaid: [Double] = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11] // dummy data
let endingBalance: [Double] = [0, 1, 2, 4, 7, 11, 16, 22, 29, 37, 46, 56] // dummy data
struct ContentView: View {
#State private var value: Double = 0
var body: some View {
VStack {
Text("Chart")
CurveChart(data: endingBalance)
.stroke(Color.blue, style: StrokeStyle(lineWidth: 1, lineCap: .round, lineJoin: .round)) // line
.background(
CurveChart(data: endingBalance, isBackground: true)
.fill(.linearGradient(colors: [.cyan, .clear], startPoint: .top, endPoint: .bottom)) // background fill
)
.frame(height: 200)
.overlay(alignment: .topTrailing, content: {
plotPoint(data: endingBalance, index: Int(value))
.fill(.blue)
})
CurveChart(data: amountPaid)
.stroke(Color.blue, style: StrokeStyle(lineWidth: 1, lineCap: .round, lineJoin: .round)) // line
.background(
CurveChart(data: amountPaid, isBackground: true)
.fill(.linearGradient(colors: [.green, .clear], startPoint: .top, endPoint: .bottom)) // background fill
)
.frame(height: 60)
.overlay(alignment: .topTrailing, content: {
plotPoint(data: amountPaid, index: Int(value))
.fill(.green)
})
.padding(.bottom)
Slider(value: $value, in: 0...Double(endingBalance.count-1), step: 1.0)
}
.padding()
}
}
struct CurveChart: Shape { // chnaged var to a Shape struct
let data: [Double]
var isBackground: Bool = false
func path(in rect: CGRect) -> Path {
Path { path in
for index in data.indices {
let xPosition: CGFloat = rect.width / CGFloat(data.count-1) * CGFloat(index)
let maxY = data.max() ?? 0
let minY = data.min() ?? 0
let yAxis: CGFloat = maxY - minY
let yPosition: CGFloat = (1 - CGFloat((Double(data[index]) - minY) / yAxis)) * rect.height
if index == 0 {
path.move(to: CGPoint(x: 0, y: rect.height))
}
path.addLine(to: CGPoint(x: xPosition, y: yPosition))
}
if isBackground { // this is needed so the backkground shape is filled correctly (closing the shape)
path.addLine(to: CGPoint(x: rect.width, y: rect.height))
}
}
}
}
struct plotPoint: Shape { // chnaged var to a Shape struct
let data: [Double]
let index: Int
let size = 20.0
func path(in rect: CGRect) -> Path {
let xStep = rect.width / Double(data.count-1)
let yStep = rect.height / (data.max() ?? 0)
let xCenter = Double(index) * xStep
let yCenter = rect.height - yStep * data[index]
var path = Path()
path.addEllipse(in: CGRect(x: xCenter - size/2, y: yCenter - size/2, width: size, height: size))
return path
}
}

Is SKCropNode limited to a width below 5000?

I am trying to use a SKCropNode with a width of at least 5000 pixels, yet if the maskNode property width is greater than 4100 something strange happens -- the first 100-200 or so pixels are not cropped, and the cropping spans approx only 4100 pixels.
I'm on a Mac Mini M1, Ventura 13.0.1 and Xcode 14.1 (14B47b)
Here is the working code:
import SwiftUI
import SpriteKit
struct ContentView: View {
#State var spriteView : SpriteView!
var body: some View {
ScrollView(.horizontal, showsIndicators: true) {
ZStack {
if spriteView != nil {
spriteView
}
VStack {
Image(systemName: "globe")
.imageScale(.large)
.foregroundColor(.accentColor)
Text("Hello, world!")
}
}
.padding()
.onAppear {
spriteView = .init(scene: myScene)
}
.frame(minWidth: 1000, minHeight: 500)
}
}
var myScene : SKScene {
let r = SKScene.init(size: .init(width: 2000, height: 500))
r.backgroundColor = .yellow
let maskRect = SKShapeNode.init(rect: .init(x: 0, y: 100, width: 5000, height: 300))
// let maskRect = SKShapeNode.init(rect: .init(x: 0, y: 100, width: 4096+2, height: 300))
maskRect.fillColor = .white
maskRect.lineWidth = 0
let redRect = SKShapeNode.init(rect: .init(origin: .zero, size: .init(width: 5000, height: 500)))
redRect.fillColor = .red
redRect.lineWidth = 0
let path = CGMutablePath.init()
path.move(to: .zero)
path.addLine(to: .init(x: 1000, y: 500))
let blueLine = SKShapeNode.init(path: path)
blueLine.strokeColor = .blue
blueLine.lineWidth = 12
redRect.addChild(blueLine)
let cropNode = SKCropNode.init()
cropNode.maskNode = SKNode.init()
cropNode.maskNode?.addChild(maskRect)
cropNode.addChild(redRect)
let blackRect = SKShapeNode.init(rect: .init(origin: .init(x: 0, y: 100), size: .init(width: 5000, height: 300)))
blackRect.fillColor = .black
blackRect.lineWidth = 0
r.addChild(blackRect)
r.addChild(cropNode)
return r
}
}
The result I get is
If I change the width to 4096 I get the correct result without the black rectangle on the left hand side, ie let maskRect = SKShapeNode.init(rect: .init(x: 0, y: 100, width: 4096, height: 300))
EDIT:
Using SKSpriteNode as suggested produced the same results for me:
let maskRect = SKSpriteNode.init(color: .black, size: .init(width: 5000, height: 300))
maskRect.position.x = 0 + maskRect.size.width/2
maskRect.position.y = 100 + maskRect.size.height/2
this appears to be an issue with SKShapeNode.
solution: use SKSpriteNode instead -- both for your mask and for any other large elements in the scene. here is an illustration (swap the red and green nodes to see the glitch):
import SwiftUI
import SpriteKit
struct GameSceneCrop: View {
#State private var scene = CropScene()
var body: some View {
SpriteView(scene: scene)
}
}
class CropScene: SKScene {
let skCameraNode = SKCameraNode()
var WIDTH:CGFloat = 5000
var HEIGHT:CGFloat = 1000
let container = SKNode()
override func didMove(to view: SKView) {
scaleMode = .aspectFill // Set the scale mode to scale to fit the window
backgroundColor = .yellow
anchorPoint = CGPoint(x: 0.5, y: 0.5)
//set up camera node
camera = skCameraNode
addChild(skCameraNode)
skCameraNode.setScale( 10000 )
addChild(container)
rebuild()
}
func rebuild() {
container.removeAllChildren()
//a green SKSpriteNode
let green = SKSpriteNode(color: .green, size: CGSize(width: WIDTH, height: HEIGHT))
//a red SKShapeNode
let red = SKShapeNode(rectOf: CGSize(width: WIDTH, height: HEIGHT))
red.fillColor = .red
//crop node
let mask = SKSpriteNode(color: .black, size: red.frame.size) //using SKSpriteNode here instead of SKShapeNode
let crop_node = SKCropNode()
crop_node.maskNode = mask
crop_node.addChild(green) //<--- replace green SKSpriteNode with red SKShapeNode to see clipping glitch
container.addChild(crop_node)
//draw outline around red block
let reference_frame = SKShapeNode(rect: red.frame.insetBy(dx: -50, dy: -50))
reference_frame.strokeColor = .black
reference_frame.lineWidth = 50
container.addChild(reference_frame)
}
override func update(_ currentTime: TimeInterval) {
let x = 0.65 + (sin(currentTime*2) + 1) / 4
WIDTH = 5000 * x
rebuild()
}
}

Add filled circles (markers) at SwiftUI path points

I'm trying to draw a line with markers at each point by using a Shape view in SwiftUI. I want the line to have a filled circle at each CGPoint on the line. The closest to this that I've gotten is by adding an arc at each point. Instead of using the arc, how can I add a Circle() shape at each point? I'm open to other approaches to accomplish this. My only requirements are to use SwiftUI and have the ability to interact with the markers.
import SwiftUI
struct LineShape: Shape {
let values: [Double]
func path(in rect: CGRect) -> Path {
let xStep = rect.width / CGFloat(values.count - 1)
var path = Path()
path.move(to: CGPoint(x: 0.0, y: (1 - values[0]) * Double(rect.height)))
for i in 1..<values.count {
let pt = CGPoint(x: Double(i) * Double(xStep), y: (1 - values[i]) * Double(rect.height))
path.addLine(to: pt)
path.addArc(center: pt, radius: 8, startAngle: Angle(degrees: 0), endAngle: Angle(degrees: 360), clockwise: false)
}
return path
}
}
struct ContentView: View {
var body: some View {
LineShape(values: [0.2, 0.4, 0.3, 0.8, 0.5])
.stroke(.red, lineWidth: 2.0)
.padding()
.frame(width: 400, height: 300)
}
}
You can make two different shapes, one for the line and one for the markers, and overlay them. Then you can also control their coloring individually:
struct LineShape: Shape {
let values: [Double]
func path(in rect: CGRect) -> Path {
let xStep = rect.width / CGFloat(values.count - 1)
var path = Path()
path.move(to: CGPoint(x: 0.0, y: (1 - values[0]) * Double(rect.height)))
for i in 1..<values.count {
let pt = CGPoint(x: Double(i) * Double(xStep), y: (1 - values[i]) * Double(rect.height))
path.addLine(to: pt)
}
return path
}
}
struct MarkersShape: Shape {
let values: [Double]
func path(in rect: CGRect) -> Path {
let xStep = rect.width / CGFloat(values.count - 1)
var path = Path()
for i in 1..<values.count {
let pt = CGPoint(x: Double(i) * Double(xStep), y: (1 - values[i]) * Double(rect.height))
path.addEllipse(in: CGRect(x: pt.x - 8, y: pt.y - 8, width: 16, height: 16))
}
return path
}
}
struct ContentView: View {
var body: some View {
LineShape(values: [0.2, 0.4, 0.3, 0.8, 0.5])
.stroke(.red, lineWidth: 2.0)
.overlay(
MarkersShape(values: [0.2, 0.4, 0.3, 0.8, 0.5])
.fill(.blue)
)
.frame(width: 350, height: 300)
}
}
According to previous posts and comments here is my solution:
First there is need to use extension for filling and setting shape stroke:
extension Shape {
public func fill<Shape: ShapeStyle>(_ fillContent: Shape, strokeColor: Color, lineWidth: CGFloat) -> some View {
ZStack {
self.fill(fillContent)
self.stroke(strokeColor, lineWidth: lineWidth)
}
}
}
Another thing is Scaler struct used to make scaling calculations:
struct Scaler {
let bounds: CGPoint
let maxVal: Double
let minVal: Double
let valuesCount: Int
var xFactor: CGFloat {
valuesCount < 2 ? bounds.x : bounds.x / CGFloat(valuesCount - 1)
}
var yFactor: CGFloat {
bounds.y / maxVal
}
}
Now it's time for real chart shape drawing:
struct ChartLineDotsShape: Shape {
let values: [Double]
let dotRadius: CGFloat
func path(in rect: CGRect) -> Path {
guard let maxVal = values.max(), let minVal = values.min() else { return Path() }
let scaler = Scaler(bounds: CGPoint(x: rect.width, y: rect.height), maxVal: maxVal, minVal: minVal, valuesCount: values.count)
return Path { path in
var valuesIterator = values.makeIterator()
let dx = scaler.xFactor
var x = 0.0
guard let y = valuesIterator.next() else { return }
path.move(to: CGPoint(x: x, y: calculate(y, scaler: scaler)))
draw(point: CGPoint(x: x, y: calculate(y, scaler: scaler)), on: &path)
x += dx
while let y = valuesIterator.next() {
draw(point: CGPoint(x: x, y: calculate(y, scaler: scaler)), on: &path)
x += dx
}
}
}
private func calculate(_ value: CGFloat, scaler: Scaler) -> CGFloat {
scaler.bounds.y - value * scaler.yFactor
}
private func draw(point: CGPoint, on path: inout Path) {
path.addLine(to: CGPoint(x: point.x, y: point.y))
path.addEllipse(in: CGRect(x: point.x - dotRadius * 0.5, y: point.y - dotRadius * 0.5, width: dotRadius, height: dotRadius))
path.move(to: CGPoint(x: point.x, y: point.y))
}
}
If there is single iteration loop lines dots drawing shape we can try to use it in some view:
public struct ChartView: View {
public var body: some View {
ChartLineDotsShape(values: [0.3, 0.5, 1.0, 2.0, 2.5, 2.0, 3.0, 6.0, 2.0, 1.5, 2.0], dotRadius: 4.0)
.fill(Color.blue, strokeColor: Color.blue, lineWidth: 1.0)
.shadow(color: Color.blue, radius: 2.0, x: 0.0, y: 0.0)
.frame(width: 320.0, height: 200.0)
.background(Color.blue.opacity(0.1))
.clipped()
}
}
Finally preview setup:
struct ChartView_Previews: PreviewProvider {
static var previews: some View {
ChartView()
.preferredColorScheme(.dark)
}
}
and viola:

Can't use 'shape' as Type in SwiftUI

I'm trying to create a Set of cards. Each card has an object (shape) assigned, that is then shown with a different colour and appears in different numbers on each card. -> as a template I've created the struct SetCard
Already within the definition of the struct SetCard the var shape: Shape returns the error message:
Value of protocol type 'Shape' cannot conform to 'View'; only struct/enum/class types can conform to protocols"
import Foundation
import SwiftUI
var setCardSet: Array<SetCard> = []
var counter: Int = 0
func createSet() -> Array<SetCard> {
for object in 0..<3 {
var chosenShape = objectLibrary[object]
for color in 0..<3 {
var chosenColor = colors[color]
for numberOfShapes in 0..<3 {
counter += 1
setCardSet.append(SetCard(id: counter, shape: chosenShape, numberOfShapes: numberOfShapes, colorOfShapes: chosenColor))
}
}
}
return setCardSet
}
struct SetCard: Identifiable {
var id: Int
var shape: Shape
var numberOfShapes: Int
var colorOfShapes: Color
// var shadingOfShapes: Double
}
struct GameObject {
var name: String
var object: Shape
}
let objectLibrary: [Shape] = [Diamond(), Oval()]
let colors: [Color] = [Color.green, Color.red, Color.purple]
At a later step, the individual objects are shown and "stacked" on the same card:
import SwiftUI
struct ContentView: View {
let observed: Array<SetCard> = createSet()
var body: some View {
VStack (observed) { card in
CardView(card: card).onTapGesture {
withAnimation(.linear(duration: 0.75)) {
print(card.id)
}
}
.padding(2)
}
}
}
struct CardView: View {
var card: SetCard
var body: some View {
ZStack {
VStack{
ForEach(0 ..< card.numberOfShapes+1) {_ in
card.shape
}
}
}
}
}
let gradientStart = Color(red: 239.0 / 255, green: 120.0 / 255, blue: 221.0 / 255)
let gradientEnd = Color(red: 239.0 / 255, green: 172.0 / 255, blue: 120.0 / 255)
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
What am I doing wrong here?
Definition of shapes:
import SwiftUI
struct Diamond: Shape {
func path(in rect: CGRect) -> Path {
let height = min(rect.width, rect.height)/4
let length = min(rect.width, rect.height)/3
let center = CGPoint(x: rect.midX, y: rect.midY)
let top: CGPoint = CGPoint(x: center.x + length, y: center.y)
let left: CGPoint = CGPoint(x: center.x, y: center.y - height)
let bottom: CGPoint = CGPoint(x: center.x - length, y: center.y)
let right: CGPoint = CGPoint(x: center.x, y: center.y + height)
var p = Path()
p.move(to: top)
p.addLine(to: left)
p.addLine(to: bottom)
p.addLine(to: right)
p.addLine(to: top)
return p
}
}
struct Oval: Shape {
func path(in rect: CGRect) -> Path {
let height = min(rect.width, rect.height)/4
let length = min(rect.width, rect.height)/3
let center = CGPoint(x: rect.midX, y: rect.midY)
let centerLeft = CGPoint(x: center.x - length + (height/2), y: center.y)
let centerRight = CGPoint(x: center.x + length - (height/2), y: center.y)
let bottomRight = CGPoint(x: centerRight.x, y: center.y - height)
let topLeft = CGPoint(x: centerLeft.x, y: center.y + height)
var p = Path()
p.move(to: topLeft)
p.addArc(center: centerLeft, radius: height, startAngle: Angle(degrees: 90), endAngle: Angle(degrees: 270), clockwise: false)
p.addLine(to: bottomRight)
p.addArc(center: centerRight, radius: height, startAngle: Angle(degrees: 270), endAngle: Angle(degrees: 90), clockwise: false)
p.addLine(to: topLeft)
return p
}
}
You need to use one type for shape in your model, like
struct SetCard: Identifiable {
var id: Int
var shape: AnyShape // << here !!
var numberOfShapes: Int
var colorOfShapes: Color
// var shadingOfShapes: Double
}
The AnyShape declaration and demo of usage can be observed in my different answer https://stackoverflow.com/a/62605936/12299030
And, of course, you have to update all other dependent parts to use it (I skipped that for simplicity).

How to draw text when using layers in NSView with Swift

I'm using layers in my NSView as much of the display will change infrequently. This is working fine. I'm now at the point where I'd like to add text to the layer. This is not working.
To draw the text, I'm trying to use code from another answer on how to draw text on a curve. I've researched and nothing I've tried helps. I've created a Playground to demonstrate. I can see when I run the Playground that the code to display the text is actually called, but nothing is rendered to the screen. I'm sure I've got something wrong, just not sure what.
The playground is below, along with the code referenced from above (updated for Swift 5, NS* versus UI*, and comments removed for size).
import AppKit
import PlaygroundSupport
func centreArcPerpendicular(text str: String,
context: CGContext,
radius r: CGFloat,
angle theta: CGFloat,
colour c: NSColor,
font: NSFont,
clockwise: Bool){
let characters: [String] = str.map { String($0) }
let l = characters.count
let attributes = [NSAttributedString.Key.font: font]
var arcs: [CGFloat] = []
var totalArc: CGFloat = 0
for i in 0 ..< l {
arcs += [chordToArc(characters[i].size(withAttributes: attributes).width, radius: r)]
totalArc += arcs[i]
}
let direction: CGFloat = clockwise ? -1 : 1
let slantCorrection: CGFloat = clockwise ? -.pi / 2 : .pi / 2
var thetaI = theta - direction * totalArc / 2
for i in 0 ..< l {
thetaI += direction * arcs[i] / 2
centre(text: characters[i],
context: context,
radius: r,
angle: thetaI,
colour: c,
font: font,
slantAngle: thetaI + slantCorrection)
thetaI += direction * arcs[i] / 2
}
}
func chordToArc(_ chord: CGFloat, radius: CGFloat) -> CGFloat {
return 2 * asin(chord / (2 * radius))
}
func centre(text str: String,
context: CGContext,
radius r: CGFloat,
angle theta: CGFloat,
colour c: NSColor,
font: NSFont,
slantAngle: CGFloat) {
let attributes = [NSAttributedString.Key.foregroundColor: c, NSAttributedString.Key.font: font]
context.saveGState()
context.translateBy(x: r * cos(theta), y: -(r * sin(theta)))
context.rotate(by: -slantAngle)
let offset = str.size(withAttributes: attributes)
context.translateBy (x: -offset.width / 2, y: -offset.height / 2)
str.draw(at: CGPoint(x: 0, y: 0), withAttributes: attributes)
context.restoreGState()
}
class CustomView: NSView {
let textOnCurve = TextOnCurve()
override init(frame: NSRect) {
super.init(frame: frame)
wantsLayer = true
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func updateLayer() {
let textOnCurveLayer = CAShapeLayer()
textOnCurveLayer.frame = self.frame
textOnCurveLayer.delegate = textOnCurve
textOnCurveLayer.setNeedsDisplay()
layer?.addSublayer(textOnCurveLayer)
}
}
class TextOnCurve: NSObject, CALayerDelegate {
func draw(_ layer: CALayer, in ctx: CGContext) {
ctx.setFillColor(.white)
ctx.move(to: CGPoint(x: 100, y: 100))
ctx.addLine(to: CGPoint(x: 150, y: 100))
ctx.addLine(to: CGPoint(x: 150, y: 150))
ctx.addLine(to: CGPoint(x: 100, y: 150))
ctx.closePath()
ctx.drawPath(using: .fill)
ctx.translateBy(x: 200.0, y: 200.0)
centreArcPerpendicular(text: "Anticlockwise",
context: ctx,
radius: 100,
angle: CGFloat(-.pi/2.0),
colour: .red,
font: NSFont.systemFont(ofSize: 16),
clockwise: false)
centre(text: "Hello flat world",
context: ctx,
radius: 0,
angle: 0 ,
colour: .yellow,
font: NSFont.systemFont(ofSize: 16),
slantAngle: .pi / 4)
}
}
let containerView = CustomView(frame: CGRect(x: 0, y: 0, width: 400, height: 400))
PlaygroundPage.current.liveView = containerView
The box is drawn, but the text is not.
If I do not use layers, then I can draw text by calling the methods in the standard draw: method. So the code should work, but, something about the use of layers is preventing it.