It’s quite hard to find any Swift examples on the web. It took me hours to write a simple program that draws a rectangle in Swift and saves it to a file. 50% of the code that comes up in search results uses old APIs and Objective C in some cases. The other 50% uses Swift UI and I am not making a GUI app. I am making a CLI app. Let’s see how to do it. All code here is w.r.t. Swift 5.7.2.
First, we have to download and install Swift. You don’t need to download Xcode to use Swift but you won’t be able to run any unit tests without Xcode.
Next cd to an empty directory and create a new executable project (i.e., a Console or command-line application):
$ swift package init --type executable
You will see a Package.swift file generated. The good thing is that you don’t need to add any dependencies to Package.swift to do 2D drawing in Swift. 2D drawing is made possible by the CoreGraphics library which is automatically loaded on MacOS for an executable app. I don’t know about other platforms. Everything here is w.r.t. Mac OS. Think of CoreGraphics as the equivalent of GDI+ if you have done 2D Drawing or Graphics on .NET platform.
The first step we will do is to create an empty bitmap of a certain size. First we import CoreGraphics:
import CoreGraphics
and then in a function we create an object of type CGContext:
let sRGB = CGColorSpace(name: CGColorSpace.sRGB)!
let ctx = CGContext(data: nil, width: 100, height: 100, bitsPerComponent: 8, bytesPerRow: 100*4, space: sRGB, bitmapInfo: CGImageAlphaInfo.premultipliedFirst.rawValue)!
For those familiar with GDI+, think of CGContext as the equivalent of System.Drawing.Graphics. The CG stands for CoreGraphics. It took me hours to figure out the two line code above and was the hardest part. After this we will draw the rectangle and fill it with a color. It can be done like this:
let origin = CGPoint(x: 20, y: 40)
let size = CGSize(width: 30, height: 15)
let rect = CGRect(origin: origin, size: size)
ctx.setFillColor(red: 1.0, green: 0, blue: 0, alpha: 0.5)
ctx.fill(rect)
Compared to GDI+ we don’t create any Brush. To draw a rectangle without filling it with a color you would call:
ctx.setStrokeColor(cgColor)
ctx.stroke(cgRect, width: strokeWidth)
Again if you compare to GDI+, we don’t create any Pen object.
Next, all that remains is to save the image to a file. But before we do that, hold on. Where is the image? We don’t have any image yet. We can get it from CGContext as follows:
let img = ctx.makeImage()!
Again, if you compare to GDI+, you will notice its backwards. In GDI+, we first create a Bitmap or Image and then get a Graphics object from it. In Swift we first create CGContext and then get CGImage from it. Now to save this image in a file we use following function:
import Foundation
import ImageIO
import CoreGraphics
import UniformTypeIdentifiers
// https://gist.github.com/KrisYu/abf3d03a76b781ffc2a26848d713b11e
@discardableResult static func writeCGImage(_ image: CGImage, to destinationURL: URL) throws -> Bool {
guard let destination = CGImageDestinationCreateWithURL(destinationURL as CFURL, UTType.jpeg.identifier as CFString, 1, nil) else {
throw Errors.fileIOException
}
CGImageDestinationAddImage(destination, image, nil)
return CGImageDestinationFinalize(destination)
}
This function can be called like:
writeCGImage(img, to: URL(fileURLWithPath: "test.jpg"))
And now we have a complete program that draws a rectangle and saves it to a file.
Compared to GDI+, I feel GDI+ API is slightly better as it uses intuitive objects like Brush and Pen for drawing.
Trivia: Swift language is very annoying and you have to prefix arguments with their labels. E.g., if you write:
let origin = CGPoint(20, 40)
You will get a compile time error. To be able to write:
let origin = CGPoint(20, 40)
The CGPoint function has to be declared like:
func CGPoint(_ x: Float, _ y: Float)