Day 88 - 100 Days of Swift

8 minute read

Project 27 (part 1)

Day 88 is the first part of the twenty-seventh project. It is a technique project focused on CoreGraphics where you write a bunch of methods to draw some different things into a UIImageView. You draw a rectangle and a circle. You draw checkerboard and some overlapping squares rotated around their center. You draw a kind of abstract series of lines that results in a cool spiral graphic sort of thing. And you draw an image and some text into a new image.

First, you get the project set up. You make a single view app, fill up the whole view with an image view and then place a button on top of it down at the bottom of the screen. You add an outlet for the image view and an action for the button. You also add a property called currentDrawType to help cycle through the different options:

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
var currentDrawType = 0
@IBOutlet weak var imageView: UIImageView!

@IBAction func redraw(_ sender: Any) {
    currentDrawType += 1
    currentDrawType %= 6

    switch currentDrawType {
    case 0:
        break
    case 1:
        break
    case 2:
        break
    case 3:
        break
    case 4:
        break
    case 5:
        break

    default:
        break
    }
}

Then you add the first function which is called drawRectangle:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
func drawRectangle() {
    let size = CGSize(width: 512,
                      height: 512)
    let renderer = UIGraphicsImageRenderer(size: size)

    let image = renderer.image { context in
        let rectangle = CGRect(origin: .zero,
                               size: size)
            .insetBy(dx: 10, dy: 10)

        context.cgContext.setStrokeColor(UIColor.black.cgColor)
        context.cgContext.setLineWidth(10)

        context.cgContext.addRect(rectangle)
        context.cgContext.drawPath(using: .stroke)
    }

    imageView.image = image
}

This makes a UIGraphicsImageRenderer with the size we gave it. I don’t know much about this class, but it seems like it is basically a simple way to bridge between UIKit and CoreGraphics. Then it has that renderer make an image from the closure that we pass. In that closure we set the strokeColor to black and the lineWidth to 10, add a rectangle to draw, and then draw the path of that rectangle using the stroke we set up. Finally, it sets the imageView’s image to be the image we produced.

You call this method in viewDidLoad and set it to be the content of the first case in the switch statement. This results in a rectangle that looks like this:

Screenshot of drawn rectangle

Next, you draw a circle. The code for this is almost identical, except you use addEllipse(in:) instead of addRect():

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
func drawCircle() {
    let size = CGSize(width: 512,
                      height: 512)
    let renderer = UIGraphicsImageRenderer(size: size)

    let image = renderer.image { context in
        let rectangle = CGRect(origin: .zero,
                               size: size)
            .insetBy(dx: 10, dy: 10)

        context.cgContext.setStrokeColor(UIColor.black.cgColor)
        context.cgContext.setLineWidth(10)

        context.cgContext.addEllipse(in: rectangle)
        context.cgContext.drawPath(using: .stroke)
    }

    imageView.image = image
}

You add that as the second case and it results in a circle that looks like this:

Screenshot of drawn circle

For the checkerboard you loop through and fill individual rectangles instead of adding them to the context:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
func drawCheckerboard() {
    let size = CGSize(width: 512,
                      height: 512)
    let renderer = UIGraphicsImageRenderer(size: size)

    let image = renderer.image { context in
        context.cgContext.setFillColor(UIColor.black.cgColor)

        for row in 0..<8 {
            for col in 0..<8 {
                if (row + col) % 2 == 0 {
                    context.cgContext.fill(CGRect(x: col * 64,
                                                  y: row * 64,
                                                  width: 64,
                                                  height: 64))
                }
            }
        }
    }
    imageView.image = image
}

This is the third case and it results in a checkerboard that looks like this:

Screenshot of drawn checkerboard.

For the rotated rectangles you translate the context, so that you’re rotating around the middle instead of the top left corner, then you rotate the context and add a rectangle, rotate it again and add another rectangle, etc. This one is kind of cool because there is a constant for how many rectangles you want to draw and the math will draw them evenly spaced throughout the circle:

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
26
func drawRotatedSquares() {
    let size = CGSize(width: 512,
                      height: 512)
    let renderer = UIGraphicsImageRenderer(size: size)

    let image = renderer.image { context in
        context.cgContext.translateBy(x: 256, y: 256)

        let rotations = 3
        let amount = Double.pi / Double(rotations)

        for _ in 0 ..< rotations {
            context.cgContext.rotate(by: CGFloat(amount))
            let rect = CGRect(x: -128,
                              y: -128,
                              width: 256,
                              height: 256)
            context.cgContext.addRect(rect)
        }

        context.cgContext.setStrokeColor(UIColor.black.cgColor)
        context.cgContext.strokePath()
    }

    imageView.image = image
}

You add this as the fourth case and it results in an image that looks like this:

Screenshot of three rotated rectangles.

If you change rotations to be 100, it looks like this:

Screenshot of one hundred rotated rectangles.

For the abstract lines you do a similar thing where you rotate the context and then add a line, rotate the context and add a line, etc. Only this time the line shrinks as you go, so it sort of spirals into itself.

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
26
27
28
29
30
31
func drawLines() {
    let size = CGSize(width: 512,
                      height: 512)
    let renderer = UIGraphicsImageRenderer(size: size)

    let image = renderer.image { context in
        context.cgContext.translateBy(x: 256, y: 256)

        var first = true
        var length: CGFloat = 256

        for _ in 0 ..< 256 {
            context.cgContext.rotate(by: .pi / 2)
            let point = CGPoint(x: length, y: 50)
            if first {

                context.cgContext.move(to: point)
                first = false
            } else {
                context.cgContext.addLine(to: point)
            }

            length *= 0.99
        }

        context.cgContext.setStrokeColor(UIColor.black.cgColor)
        context.cgContext.strokePath()
    }

    imageView.image = image
}

You add that as the fifth case and it results in an image that looks like this:

Screenshot of rotated lines.

For the final function, you set up an attributed string and draw it into a rectangle, and you grab a UIImage and draw that starting at a given point:

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
26
27
28
29
30
31
32
33
34
35
36
37
func drawImagesAndText() {

    let size = CGSize(width: 512,
                      height: 512)
    let renderer = UIGraphicsImageRenderer(size: size)

    let image = renderer.image { context in

        let paragraphStyle = NSMutableParagraphStyle()
        paragraphStyle.alignment = .center

        let attrs: [NSAttributedString.Key: Any] = [
            .font: UIFont.systemFont(ofSize: 36),
            .paragraphStyle: paragraphStyle
        ]

        let string = """
                     The best-laid schemes o'
                     mice an' men gang aft agley
                     """
        let attributedString = NSAttributedString(string: string,
                                                  attributes: attrs)

        let stringRect = CGRect(x: 32,
                                y: 32,
                                width: 448,
                                height: 448)
        attributedString.draw(with: stringRect,
                              options: .usesLineFragmentOrigin,
                              context: nil)

        let mouse = UIImage(named: "mouse")
        mouse?.draw(at: CGPoint(x: 300, y: 150))
    }

    imageView.image = image
}

You add that as the sixth and final case and it results in an image that looks like this:

Screenshot of image and text together.

And that’s it for today! It’s kind of cool the interesting things you can draw with just a little bit of code.

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