Create a simple Swift program that draws a line

I am new to Swift. The Apple website has some great resources. This one shows how to draw in an ios playground. Crustacean.playground. I found it in the Sample Code {} Crustacean.playground link on this site: https://developer.apple.com/swift/resources/. I tried to get a simple app to do the same drawings in macOS (not ios) and failed. I am wondering it somebody can please help me to create a simple Cocoa project that renders dquivalent images in Cocoa. What would really be helpful would be the code required for each file so I could create the files in a new Cocoa project and just paste the code and see how it works.


The code below was taken from the Apple playground. I got it frrom Project Navigator | Sources | CoreGraphicsDiagramView.swift. When I attempt to convert it to a Cocoa macOS project I get all kinds of errors. Thank you.


import UIKit
import PlaygroundSupport
let drawingArea = CGRect(x: 0.0, y: 0.0, width: 375.0, height: 667.0)
/
/
/
class CoreGraphicsDiagramView : UIView {
    override func draw(_ bounds: CGRect) {
        let context = UIGraphicsGetCurrentContext()!
        context.saveGState()
        draw(context)
     
        let lightBlue = CGColor(
            colorSpace: CGColorSpaceCreateDeviceRGB(), components: [0.222, 0.617, 0.976, 1.0])!
        context.setStrokeColor(lightBlue)
        context.setLineWidth(3)
        context.strokePath()
        context.restoreGState()
    }

    var draw: (CGContext)->() = { _ in () }
}
/
/
/
public func showCoreGraphicsDiagram(title: String, draw: @escaping (CGContext)->()) {
    let diagramView = CoreGraphicsDiagramView(frame: drawingArea)
    diagramView.draw = draw
    diagramView.setNeedsDisplay()
    PlaygroundPage.current.liveView = diagramView
}


import UIKit
import PlaygroundSupport
internal let drawingArea: <<error type>>
/
/
/
internal class CoreGraphicsDiagramView : UIView {
    override internal func draw(_ bounds: CGRect) -> <<error type>>
    internal var draw: (CGContext) -> ()
}
/
/
/
public func showCoreGraphicsDiagram(title: String, draw: (CGContext) -> ()) -> <<error type>>
Answered by QuinceyMorris in 273757022

You probably should avoid trying to translate your existing code. Some of the underlying logic will be useful to have tried, but on macOS your code exists within a larger ecosystem, and you need to get a bit of experience with how that works.


The macOS equivalent of iOS's UIKit is AppKit, which is included as part of the "import Cocoa" that Claude suggested. Note that macOS equivalents of similar types have NS… names instead of UI… names.


However, I suggest you proceed as follows:


1. Create a new single-window macOS app project from the Xcode template. Build and run to see that you get a single window with nothing in it.


2. Create a new NSView subclass in a new Swift file. There should be a template for this in the dialog that comes up when you create a new source file. Don't add any code yet.


3. Go to your storyboard, and find the content view of the window. In the attribute inspector, look at the class name. It should say (grayed out) NSView. Change this to the name of your NSView subclass. Build and run to make sure there are no errors.


4. Go back to your NSView subclass source file. The template should have put an empty "draw" function there. Now you can start adding drawing commands.


Don't try to muck about with the CGContext directly. That was only necessary in the playground because you had to carefully co-exist with the playground's built-in functionality. In your subclass, you control what's drawn. Try creating a NSBezierPath for one of its standard shapes. (Consult the NSBezierPath class documentation for the various methods available.) Use the NSColor class to access standard colors, or create your own.


Use NSColor's set, setStroke and setFill methods (in your "draw(:)" method) to set up the colors you want. Use NSBezierPath's fill and stroke methods to actually draw.


That should get you started. Note that for drawing purposes, you are NOT going to add code to the app delegate. That's not what it's for.

You need to replace IOS API with OSX.


- first, need to change import UIKit with import Cocoa.


- change

let context = UIGraphicsGetCurrentContext()!

with

let context = NSGraphicsContext.current!.cgContext


- UIView becomes NSView


- what do you intend with :

internal let drawingArea: <<error type>>


Do it all and tell what other errors you get.

Thank you for yourreply. I created a new macOS project - Cocoa APP and called it SimpleDrawing. I had to paste parts of the code into different scope areas of AppDelegate.app or I got errors that appeared to be scope errors. So I rearranged the code to get rid of the scope errors. Here is what I got when I started, after making the changes you suggested... I got errors on line 26, 27, 28 and 29 o fthis nature "Value of type 'NSGraphicsContext' has no member".

import Cocoa
import CoreGraphics
let drawingArea = CGRect(x: 0.0, y: 0.0, width: 375.0, height: 667.0)
/
/
/
class CoreGraphicsDiagramView : NSView {
    override func draw(_ bounds: CGRect) {
        /
        let context = NSGraphicsContext()
        context.saveGraphicsState()
        /
        draw(context as! CGContext)
      
        let lightBlue = CGColor(
            colorSpace: CGColorSpaceCreateDeviceRGB(), components: [0.222, 0.617, 0.976, 1.0])!
        context.setStrokeColor(lightBlue)  /
        context.setLineWidth(3) /
        context.strokePath()  /
        context.restoreGState()  /
    }
  
    var draw: (CGContext)->() = { _ in () }
}
/
    /
/
public func showCoreGraphicsDiagram(title: String, draw: @escaping (CGContext)->()) {
    let diagramView = CoreGraphicsDiagramView(frame: drawingArea)
    diagramView.draw = draw
    diagramView.setNeedsDisplay(<#NSRect#>)
    /
}
@NSApplicationMain
class AppDelegate: NSObject, NSApplicationDelegate {
    func applicationDidFinishLaunching(_ aNotification: Notification) {
        /
    }
    func applicationWillTerminate(_ aNotification: Notification) {
        /
    }
}


Then I rearranged the code and got rid of the errors but nothing gets drawn when I run the project. This is what I have. I tried debug stepping through it and could not discover what was happening. Perhaps all I have to do is rearrange things here but I cannot figure it out.

import Cocoa
import CoreGraphics
@NSApplicationMain
class AppDelegate: NSObject, NSApplicationDelegate {
    func applicationDidFinishLaunching(_ aNotification: Notification) {
     
        /
    }


    func applicationWillTerminate(_ aNotification: Notification) {
        /
    }

    let drawingArea = CGRect(x: 0.0, y: 0.0, width: 375.0, height: 667.0)
    let twoPi = CGFloat.pi * 2
    public func showCoreGraphicsDiagram(title: String, draw: @escaping (CGContext)->()) {
        let diagramView = CoreGraphicsDiagramView(frame: drawingArea)
        diagramView.draw = draw
        /
        /
    }
    class CoreGraphicsDiagramView : NSView {
        override func draw(_ bounds: CGRect) {
            let context = NSGraphicsContext.current!.cgContext
            context.saveGState()
            draw(context)
         
            let lightBlue = CGColor(
                colorSpace: CGColorSpaceCreateDeviceRGB(), components: [0.222, 0.617, 0.976, 1.0])!
            context.setStrokeColor(lightBlue)
            context.setLineWidth(3)
            context.strokePath()
            context.restoreGState()
        }
     
        var draw: (CGContext)->() = { _ in () }
    }

    /
    /
    /
    /
    /
    struct TestRenderer : Renderer {
        func move(to p: CGPoint) { print("move(to: CGPoint(\(p.x), \(p.y)))") }
     
        func line(to p: CGPoint) { print("line(to: CGPoint(\(p.x), \(p.y)))") }
     
        func arc(at center: CGPoint, radius: CGFloat, startAngle: CGFloat, endAngle: CGFloat) {
            print("arc(at: \(center), radius: \(radius)," + " startAngle: \(startAngle), endAngle: \(endAngle))")
        }
    }
    /
    struct Polygon : Drawable {
        func draw(into renderer: Renderer) {
            renderer.move(to: corners.last!)
            for p in corners { renderer.line(to: p) }
        }
        var corners: [CGPoint] = []
    }

    struct Circle : Drawable {
        func draw(into renderer: Renderer) {
            renderer.arc(at: center, radius: radius, startAngle: 0.0, endAngle: 6.28)
        }
        var center: CGPoint
        var radius: CGFloat
    }

    /
    /
    struct Diagram : Drawable {
        func draw(into renderer: Renderer) {
            for f in elements {
                f.draw(into: renderer)
            }
        }
        mutating func add(other: Drawable) {
            elements.append(other)
        }
        var elements: [Drawable] = []
    }
    var circle = Circle(center: CGPoint(x: 187.5, y: 333.5), radius: 93.75)

    var triangle = Polygon(corners: [
        CGPoint(x: 187.5, y: 427.25),
        CGPoint(x: 268.69, y: 286.625),
        CGPoint(x: 106.31, y: 286.625)])
    /
    /
    /
    /
    /
    /
    /
    /
    /
    /
    /
    /
    /
    /
    /

    /
    /
    struct ScaledRenderer : Renderer {
        let base: Renderer
        let scale: CGFloat
     
        func move(to p: CGPoint) {
            base.move(to: CGPoint(x: p.x * scale, y: p.y * scale))
        }
     
        func line(to p: CGPoint) {
            base.line(to: CGPoint(x: p.x * scale, y: p.y * scale))
        }
     
        func arc(at center: CGPoint, radius: CGFloat, startAngle: CGFloat, endAngle: CGFloat) {
            let scaledCenter = CGPoint(x: center.x * scale, y: center.y * scale)
            base.arc(at: scaledCenter, radius: radius * scale, startAngle: startAngle, endAngle: endAngle)
        }
    }

    /
    struct Scaled<Base: Drawable> : Drawable {
        var scale: CGFloat
        var subject: Base
     
        func draw(into renderer: Renderer) {
            subject.draw(into: ScaledRenderer(base: renderer, scale: scale))
        }
    }

    /
    /
    diagram.elements.append(Scaled(scale: 0.3, subject: diagram))

    /
    /
    diagram.draw(into: TestRenderer())

    /
    /
    showCoreGraphicsDiagram(title: "Diagram") { diagram.draw(into: $0) }
    */
    /
}
protocol Renderer {
    /
    func move(to position: CGPoint)

    /
    /
    func line(to position: CGPoint)

    /
    /
    /
    func arc(at center: CGPoint, radius: CGFloat, startAngle: CGFloat, endAngle: CGFloat)
}
/
protocol Drawable {
    /
    func draw(into renderer: Renderer)
}
/
/
/
/
/
extension CGContext : Renderer {
    func line(to position: CGPoint) {
        addLine(to: position)
    }
    func arc(at center: CGPoint, radius: CGFloat, startAngle: CGFloat, endAngle: CGFloat) {
        let arc = CGMutablePath()
        arc.addArc(center: center, radius: radius, startAngle: startAngle, endAngle: endAngle, clockwise: true)
        addPath(arc)
    }
}


I am beginning to think that this is not going to be a Swift solution. It looks like anything created for ios graphics is not a good sample program for macOS graphics. The two graphics programs appear to be completely different. It also appears that a person cannot just cut and paste what a playground project into an actual project and expect it to work. It appears that playground code must be placed in a project keeping scope and inheritance in mind.


Are you aware of a simple project example for drawing one line in macOS that just works without all of this complexity. This complexity is too much for a newcomere who is just looking to get a template that will allow drawing of simple graphics. Thanks again for your help. Maybe I am close. But I am beginning to suspect that drawing in macOS in Swift 4 is not for newcomers.

Accepted Answer

You probably should avoid trying to translate your existing code. Some of the underlying logic will be useful to have tried, but on macOS your code exists within a larger ecosystem, and you need to get a bit of experience with how that works.


The macOS equivalent of iOS's UIKit is AppKit, which is included as part of the "import Cocoa" that Claude suggested. Note that macOS equivalents of similar types have NS… names instead of UI… names.


However, I suggest you proceed as follows:


1. Create a new single-window macOS app project from the Xcode template. Build and run to see that you get a single window with nothing in it.


2. Create a new NSView subclass in a new Swift file. There should be a template for this in the dialog that comes up when you create a new source file. Don't add any code yet.


3. Go to your storyboard, and find the content view of the window. In the attribute inspector, look at the class name. It should say (grayed out) NSView. Change this to the name of your NSView subclass. Build and run to make sure there are no errors.


4. Go back to your NSView subclass source file. The template should have put an empty "draw" function there. Now you can start adding drawing commands.


Don't try to muck about with the CGContext directly. That was only necessary in the playground because you had to carefully co-exist with the playground's built-in functionality. In your subclass, you control what's drawn. Try creating a NSBezierPath for one of its standard shapes. (Consult the NSBezierPath class documentation for the various methods available.) Use the NSColor class to access standard colors, or create your own.


Use NSColor's set, setStroke and setFill methods (in your "draw(:)" method) to set up the colors you want. Use NSBezierPath's fill and stroke methods to actually draw.


That should get you started. Note that for drawing purposes, you are NOT going to add code to the app delegate. That's not what it's for.

Thank you for the excellent answer. It worked perfectly. For those who are new and trying to do this, all the code I required for the class file I created following the instructions from QuinceyMorris is given below. It draws a nice blue line from point (2,5) to point (100,60). Note that it required a CFFloat value for the fromPoint and toPoint values. It will fail with Float variables IE this code generates errors: fromPoint = NSMakePoint(Float(2) , Float(5)). The errors are of the form: "Use of unresolved identifier 'fromPoint'".

import Cocoa
class NSViewSubClass: NSView {
    override func draw(_ dirtyRect: NSRect) {
        super.draw(dirtyRect)
        let path = NSBezierPath()
        NSColor.blue.set()
        let fromPoint = NSMakePoint(CGFloat(2) , CGFloat(5))
        let toPoint = NSMakePoint(CGFloat(100) , CGFloat(60))
        path.move(to: fromPoint)
        path.line(to: toPoint)
        path.lineWidth = 1.0
        path.stroke()
    }
}

Glad you got it working. One comment about a historical quirk of AppKit.


There used to be a NSPoint that was technically different to CGPoint, but in all current (i.e. 64-bit) Mac apps, they are the same thing. In fact, there's no need to use NSPoint at all any more — which has the advantage of being source-compatible with iOS, where there is no NSPoint.


Swift's CGPoint type has type-overloaded initializers for Int, CGFloat and Double parameters, so you can create a point like this:


     let fromPoint = CGPoint (2, 5)


There's no reason ever to use NSMakePoint any more. In Obj-C it was a macro. In Swift it is implemented (for compaatibility) as a function, but you have to cast the types manually in cases like this. The CGPoint initializers are simpler, more direct, and more Swift-like.

Thank you.

Create a simple Swift program that draws a line
 
 
Q