Day 91 - 100 Days of Swift

9 minute read

Consolidation X (part 2)

Day 91 is the second consolidation day centered around Core Graphics. He gives you a playground with a bunch of challenges to draw various things using Core Graphics, so today is going to be a lot of pictures.

I’m assuming this is also used for people who are totally new to Core Graphics because it starts with a very simple challenge: to draw a square in the center of another square:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// Every challenge includes this, so I'll skip it and only show
// the .image closure in the rest of them.
let rect = CGRect(x: 0, y: 0, width: 1000, height: 1000)
let renderer = UIGraphicsImageRenderer(bounds: rect)

let rendered = renderer.image { ctx in
    // Draws the original rectangle
    UIColor.blue.setFill()
    ctx.cgContext.fill(CGRect(x: 200,
                              y: 200,
                              width: 600,
                              height: 600))

    // Draws my rectangle
    UIColor.red.setFill()
    ctx.cgContext.fill(CGRect(x: 400,
                              y: 400,
                              width: 200,
                              height: 200))
}

// I'll also skip this because it is the same.
showOutput(rendered)
Screenshot of smaller red rectangle drawn inside blue rectangle

The next challenge was to draw five rectangles of equal height and width in five different colors:

1
2
3
4
5
6
7
8
let colors: [UIColor] = [.red, .orange, .yellow, .green, .blue]
let width = Int(rect.width) / colors.count
colors.enumerated().forEach {
    let origin = CGPoint(x: width * $0, y: 0)
    let size = CGSize(width: width, height: 1000)
    $1.setFill()
    ctx.cgContext.fill(CGRect(origin: origin, size: size))
}

I chose to write it in such a way that you can add as many colors as you want and it will render correctly. With the colors he gives you, it looks like this:

Screenshot fo various colored rectangles with equal height and width

The next challenge is to draw a “flag” which consists of a centered orange cross on top of a yellow background:

1
2
3
4
5
6
UIColor.yellow.setFill()
ctx.cgContext.fill(rect)

UIColor.orange.setFill()
ctx.cgContext.fill(CGRect(x: 0, y: 400, width: 1000, height: 200))
ctx.cgContext.fill(CGRect(x: 400, y: 0, width: 200, height: 1000))
Screenshot of orange and yellow flag

The next is to draw a checkerboard that is 10 x 10. Again, I chose to make it so that you can set whatever size you want and it will render correctly:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
UIColor.black.setFill()

let numChecks = 10
let size = edge / numChecks

for row in 0 ..< numChecks {
    for col in 0 ..< numChecks {
        if (row + col) % 2 == 0 {
            let origin = CGPoint(x: col * size, y: row * size)
            let size = CGSize(width: size, height: size)
            ctx.cgContext.fill(CGRect(origin: origin, size: size))
        }
    }
}
Screenshot of checkerboard

The next challenge is to draw four circles in four different colors. I tried to follow the same pattern of reusability with this one, but I’m not super happy with how the code turned out:

1
2
3
4
5
6
7
8
let colors: [[UIColor]] = [[.red, .blue], [.yellow, .green]]
zip([0,0,1,1], [0,1,0,1]).forEach { x, y in
    let origin = CGPoint(x: x*500, y: y*500)
    let size = CGSize(width: 500, height: 500)
    let color = colors[x][y]
    color.setFill()
    ctx.cgContext.fillEllipse(in: CGRect(origin: origin, size: size))
}
Screenshot of four differently colored circles

The next is to draw a “flower” which consists of four overlapping red circles and a smaller black circle in the middle:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
UIColor.red.setFill()
let circles = [
    CGRect(x: 100, y: 100, width: 500, height: 500),
    CGRect(x: 400, y: 100, width: 500, height: 500),
    CGRect(x: 100, y: 400, width: 500, height: 500),
    CGRect(x: 400, y: 400, width: 500, height: 500)
]
circles.forEach {
    ctx.cgContext.fillEllipse(in: $0)
}

UIColor.black.setFill()
let center = CGRect(x: 400, y: 400, width: 200, height: 200)
ctx.cgContext.fillEllipse(in: center)
Screenshot of flower

Next, you draw a “logo” which consists of four smaller circles that are filled with red and stroked in black that surround a larger circle in the center:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
UIColor.red.setFill()
UIColor.black.setStroke()
ctx.cgContext.setLineWidth(40)

let bigCircle = CGRect(x: 300, y: 300, width: 400, height: 400)
ctx.cgContext.addEllipse(in: bigCircle)
ctx.cgContext.drawPath(using: .fillStroke)

let circles = [
    CGRect(x: 400, y: 100, width: 200, height: 200),
    CGRect(x: 100, y: 400, width: 200, height: 200),
    CGRect(x: 400, y: 700, width: 200, height: 200),
    CGRect(x: 700, y: 400, width: 200, height: 200),
]

circles.forEach {
    ctx.cgContext.addEllipse(in: $0)
}
ctx.cgContext.setLineWidth(10)
ctx.cgContext.drawPath(using: .fillStroke)
Screenshot of circle logo

Next, you draw a rainbow using concentric circles:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
ctx.cgContext.setLineWidth(50)

let colors: [UIColor] = [
                        .red,
                        .orange,
                        .yellow,
                        .green,
                        .blue,
                        .purple
                        ]
var xPos = 0
var yPos = 500
var size = 1000

for color in colors {
    xPos += 50
    yPos += 50
    size -= 100

    let rect = CGRect(x: xPos, y: yPos, width: size, height: size)
    color.setStroke()
    ctx.cgContext.strokeEllipse(in: rect)
}
Screenshot of rainbow

Next, you draw an “emoji” that consists of a few smaller circles for eyes and a mouth, over a larger yellow circle as the face:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
UIColor.black.setStroke()
UIColor.yellow.setFill()
ctx.cgContext.setLineWidth(10)

let face = CGRect(x: 100, y: 100, width: 800, height: 800)
ctx.cgContext.addEllipse(in: face)
ctx.cgContext.drawPath(using: .fillStroke)

let eyes = [
    CGRect(x: 250, y: 300, width: 150, height: 150),
    CGRect(x: 600, y: 300, width: 150, height: 150)
]
eyes.forEach { ctx.cgContext.addEllipse(in: $0) }
UIColor.black.setFill()
ctx.cgContext.drawPath(using: .fillStroke)

let mouth = CGRect(x: 350, y: 500, width: 300, height: 300)
ctx.cgContext.addEllipse(in: mouth)
UIColor.brown.setFill()
ctx.cgContext.drawPath(using: .fillStroke)
Screenshot of emoji

Next, you render some text to finish up the catchy little phrase:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
let firstPosition = rect.offsetBy(dx: 0, dy: 300)
let firstText = "The early bird catches the worm."
let firstAttrs: [NSAttributedString.Key: Any] = [
    .font: UIFont.systemFont(ofSize: 72),
    .foregroundColor: UIColor.blue
]

let firstString = NSAttributedString(string: firstText,
                                     attributes: firstAttrs)
firstString.draw(in: firstPosition)

let secondPosition = rect.offsetBy(dx: 0, dy: 398)
let secondText = "But the second mouse gets the cheese."
let secondAttrs: [NSAttributedString.Key: Any] = [
    .font: UIFont.systemFont(ofSize: 72),
    .foregroundColor: UIColor.red
]

let secondString = NSAttributedString(string: secondText,
                                      attributes: secondAttrs)
secondString.draw(in: secondPosition)
Screenshot of text

Next, you draw a Scottish saltire, which consists of two diagonal white lines over a blue background:

1
2
3
4
5
6
7
8
9
10
11
UIColor(red: 0, green: 0.37, blue: 0.72, alpha: 1).setFill()
ctx.cgContext.fill(rect)

ctx.cgContext.move(to: CGPoint(x: 0, y: 0))
ctx.cgContext.addLine(to: CGPoint(x: 1000, y: 1000))
ctx.cgContext.move(to: CGPoint(x: 1000, y: 0))
ctx.cgContext.addLine(to: CGPoint(x: 0, y: 1000))

UIColor.white.setStroke()
ctx.cgContext.setLineWidth(100)
ctx.cgContext.strokePath()
Screenshot of Scottish flag

Next, you draw an image with a black border around it. For this one I added a little extension to make it easier to figure out the origin point for drawing things centered in another frame:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
extension CGSize {
    func centerOffset(in rect: CGRect) -> CGPoint {
        let x = (rect.width - self.width) / 2 + rect.origin.x
        let y = (rect.height - self.height) / 2 + rect.origin.y
        return CGPoint(x: x, y: y)
    }
}

// In image closure
UIColor.darkGray.setFill()
ctx.cgContext.fill(rect)

let frameEdge = 640
let frameSize = CGSize(width: frameEdge, height: frameEdge)
let frameOrigin = frameSize.centerOffset(in: rect)
UIColor.black.setFill()
let frameRect = CGRect(origin: frameOrigin, size: frameSize)
ctx.cgContext.fill(frameRect)

let frameWidth = 20
let imageEdge = frameEdge - 2 * frameWidth
let imageSize = CGSize(width: imageEdge, height: imageEdge)
let imageOrigin = imageSize.centerOffset(in: frameRect)
let imageRect = CGRect(origin: imageOrigin, size: imageSize)
mascot?.draw(in: imageRect)

This makes it really clean to update either the overall size of the frame or the border around the image without having to figure out all the other math yourself.

Screenshot of dog image

Next, you draw a series of circles that stretch across the context by translating the context itself:

1
2
3
4
5
6
7
8
let ellipseRectangle = CGRect(x: 0, y: 300, width: 400, height: 400)
ctx.cgContext.setLineWidth(8)
UIColor.red.setStroke()

for _ in 1...7 {
    ctx.cgContext.strokeEllipse(in: ellipseRectangle)
    ctx.cgContext.translateBy(x: 100, y: 0)
}
Screenshot of circles

Next, you draw another “logo” which consists of eight squares rotated around the center:

1
2
3
4
5
6
7
8
9
10
11
let boxRectangle = CGRect(x: 0, y: 0, width: 300, height: 300)
ctx.cgContext.setLineWidth(8)
ctx.cgContext.translateBy(x: 500, y: 500)

for _ in 1...8 {
    ctx.cgContext.addRect(boxRectangle)
    ctx.cgContext.rotate(by: .pi / 4)
}

UIColor.red.setStroke()
ctx.cgContext.strokePath()
Screenshot of square logo

Finally, you mess around with blend modes to see how Core Graphics blends overlapping elements together. My favorite is .xor:

1
2
3
4
5
6
7
ctx.cgContext.setBlendMode(.xor)

UIColor.red.setFill()
ctx.cgContext.fillEllipse(in: CGRect(x: 200, y: 200, width: 400, height: 400))
ctx.cgContext.fillEllipse(in: CGRect(x: 400, y: 200, width: 400, height: 400))
ctx.cgContext.fillEllipse(in: CGRect(x: 400, y: 400, width: 400, height: 400))
ctx.cgContext.fillEllipse(in: CGRect(x: 200, y: 400, width: 400, height: 400))
Screenshot of four overlapping circles with Xor blend mode

You can find my version of this project at the end of day 91 on Github here.