Japan

To save an iOS Simulator screenshot on the Mac Desktop,
File → New Screen Shot

The above screenshots are from the iPhone XR simulator. They are 828 × 1792 pixels, which is the size of the iPhone XR screen. See the tech specs. I reduced them on this web page to 414 × 896 pixels,

We printed a line of text in the draw(_:) method of the view in the Hello, World! app. Now let’s draw the flag of Japan. The first version of the app draws the circle in the upper left corner of the view. The second version draws it in the center. Nothing will happen when you touch or shake it.

Source code in Japan.zip

  1. AppDelegate.swift: unchanged.
  2. ViewController.swift: unchanged.
  3. JapanView.swift: added the init that takes an NSCoder, and draw(_:).
  4. Main.storyboard: where the 375 × 667 dimesions come from. (You can verify this with the Size Inspector.) Changed the class of the view controller’s UIView to my class JapanView.
  5. LaunchScreen.storyboard: unchanged.
  6. Info.plist: unchanged.

Create the project

Same as the previous project, except that the product name is Japan instead of Hello.

Output from print

View → Debug Area → Activate Console
The output shows that the JapanView has not yet received its correct dimensions when its init method is executed. But the correct dimensions are there when the JapanView’s draw(_:) method is executed.

On iPhone XR, the screen is 828 × 1792 pixels. The view is 414 × 896 points. Therefore 1 point = 2 pixels. See Points vs. Pixels.

init(coder:) frame  = (0.0, 0.0, 375.0, 667.0)
init(coder:) bounds = (0.0, 0.0, 375.0, 667.0)
draw(_:)     frame  = (0.0, 0.0, 414.0, 896.0)
draw(_:)     bounds = (0.0, 0.0, 414.0, 896.0)

Center the circle in the view

We put the upper left corner of the circle at the upper left corner of the JapanView.

But we want to center the circle in the JapanView.

There are three ways to do this.

  1. Move the circle by brute force. In the draw(_:) method of class JapanView, change the rectangle from
    		let r: CGRect = CGRect(
    			x: bounds.minX,
    			y: bounds.minY,
    			width: 2 * radius,
    			height: 2 * radius);
    
    to
    		let r: CGRect = CGRect(
    			x: bounds.minX + bounds.width / 2 - radius,
    			y: bounds.minY + bounds.height / 2 - radius,
    			width: 2 * radius,
    			height: 2 * radius);
    
    This will move the circle half the width of the big white JapanView to the right, and half the width of the big white JapanView down.

  2. Move the origin. The origin of a view is the point whose coördinates are (0, 0). The origin is initially the upper left corner of the view. To prove it, insert the following statements at the start of the draw(_:) method of class JapanView. To get the northwest arrow ↖ (Unicode "\u{2196}"),
    Edit → Emoji & Symbols
    		let point: CGPoint = CGPoint(x: 0.0, y: 0.0);
    		let font: UIFont = UIFont.systemFont(ofSize: 32);
    		let attributes: [NSAttributedString.Key: Any] = [NSAttributedString.Key.font: font];  //a dictionary
    		"↖ Origin".draw(at: point, withAttributes: attributes);
    
    The following diagram shows the origin at the upper left corner of the JapanView on an iPhone XR in portrait orientation. The width and height are measured in points (pairs of pixels) so the dimensions of the screen are 828 × 1792 pixels.

    Let’s move the origin to the center of the JapanView.

    Of course, we do not mention the above numbers in the program. Instead, insert the following statements into the viewDidLoad method of the view controller in the file ViewController.swift, immediately after the call to super.viewDidLoad. The new statements can not be inserted into the view’s init(coder:) method—that would be too early because the view’s bounds are not yet correct then. The new statements can not be inserted into the draw(_:) method—that would be too late.
    		//Keep the width and height of the view the same,
    		//but let the center of the view be the origin.
    
    		let w: CGFloat = view.bounds.width;
    		let h: CGFloat = view.bounds.height;
    		view.bounds = CGRect(x: -w / 2, y: -h / 2, width: w, height: h);
    
    We can now simplify the rectangle in draw(_:) to
    		let r: CGRect = CGRect(
    			x: -radius,
    			y: -radius,
    			width: 2 * radius,
    			height: 2 * radius);
    

  3. Keep the origin at the upper left corner of the view, but automatically translate (move) everything we draw 207 points to the right and 414 points down. Remove the code we just inserted into viewDidLoad, but let the rectangle around the circle in draw(_:) remain as
    		let r: CGRect = CGRect(
    			x: -radius,
    			y: -radius,
    			width: 2 * radius,
    			height: 2 * radius);
    
    Insert the following transformation into draw(_:). Since it mentions the constant c, it must be inserted after you create c. To have an effect on the circle, it must be inserted before the call to addEllipse(in:).
    		c.translateBy(x: bounds.width / 2, y: bounds.height / 2);
    
    The following is a roundabout way to do exactly the same translation. Instead of putting the horizontal and vertical distances directly into the CTM, we can put them into the constant translate. Then (possibly at a later time), we put translate into the CTM.
    		let translate: CGAffineTransform = CGAffineTransform(
    			translationX: bounds.width / 2,
    			y: bounds.height / 2);
    
    		c.concatenate(translate);
    

Another transformation: scale

Insert the following statement immediately after the translateBy(x:y:) in draw(_:).

		//Stretch all subsequent drawing vertically by a factor of 1.5.
		c.scaleBy(x: 1, y: 1.5);

What goes wrong if we do the scale before the translate?

I usually do my favorite pair of transformations before I add any shapes to the path.

		//Put the origin at the center of view.
		//Make the X axis point to the right, the Y axis point up:

		c.translateBy(x: bounds.width / 2, y: bounds.height / 2);
		c.scaleBy(x: 1, y: -1);

A third transformation: rotate

To demonstrate rotation, we replace the circle with a square.

	override func draw(_ rect: CGRect) {
		// Drawing code
		let side: CGFloat = bounds.width / 2;	//in pixels

		//specify lower left corner of rectangle because of the following scale
		let r: CGRect = CGRect(
			x: -side / 2,
			y: -side / 2,
			width: side,
			height: side);

		let c: CGContext = UIGraphicsGetCurrentContext()!;
		c.beginPath();	//unnecessary here: the path is already empty
		c.translateBy(x: bounds.width / 2, y: bounds.height / 2); //origin at center
		c.scaleBy(x: 1, y: -1);                                   //Y axis points up
		c.addRect(r);
		c.setFillColor(red: 1.0, green: 0.0, blue: 0.0, alpha: 1.0); //red, opaque
		c.fillPath();
	}

Now rotate the drawing by 15° counterclockwise. Insert the following statement immediately after the above call to scaleBy(x:y:). Without the scale, the 15 would have had to be –15. GLKMathDegreesToRadians multiplies its argument by π/180 to convert degrees to radians. The return value of this function must be converted from Float to the CGFloat expected by rotate(by:).

		c.rotate(by: CGFloat(GLKMathDegreesToRadians(15)));

At the top of the file JapanView.swift, after the existing import UIKit:

import GLKit;	//needed for GLKMathDegreesToRadians

What goes wrong if we do the rotate before the translate and scale? First of all, the rotation becomes clockwise.

A path containing two shapes

import GLKit;	//needed for GLKMathDegreesToRadians
	override func draw(_ rect: CGRect) {
		//Fill the Red Cross.
		let minimum: CGFloat = min(bounds.width, bounds.height);
		let longSide: CGFloat = minimum * 15 / 16;
		let shortSide: CGFloat = longSide / 3;

		let c: CGContext = UIGraphicsGetCurrentContext()!;
		c.beginPath();

		c.translateBy(x: bounds.width / 2, y: bounds.height / 2); //origin at center of view
		c.scaleBy(x: 1, y: -1);                                   //make Y axis point up

		let r: CGRect = CGRect(x: -longSide / 2, y: -shortSide / 2, width: longSide, height: shortSide);
		c.addRect(r);	//the horizontal bar
		c.rotate(by: CGFloat(GLKMathDegreesToRadians(90)));
		c.addRect(r);	//the vertical bar

		c.setFillColor(red: 1.0, green: 0.0, blue: 0.0, alpha: 1.0);
		c.fillPath();
	}

A path is limited to one color. If you want two colors, you’ll have to split the path into two paths.

	override func draw(_ rect: CGRect) {
		//Fill the horizontal bar with red, the vertical with blue.
		let minimum: CGFloat = min(bounds.width, bounds.height);
		let longSide: CGFloat = minimum * 15 / 16;
		let shortSide: CGFloat = longSide / 3;
		let r: CGRect = CGRect(x: -longSide / 2, y: -shortSide / 2, width: longSide, height: shortSide);

		let c: CGContext = UIGraphicsGetCurrentContext()!;
		c.translateBy(x: bounds.width / 2, y: bounds.height / 2); //origin at center of view
		c.scaleBy(x: 1, y: -1);                                   //make Y axis point up

		c.beginPath();
		c.addRect(r);
		c.setFillColor(red: 1.0, green: 0.0, blue: 0.0, alpha: 0.5); //semi-transparent red
		c.fillPath();

		c.beginPath();
		c.rotate(by: CGFloat(GLKMathDegreesToRadians(90)));
		c.addRect(r);
		c.setFillColor(red: 0.0, green: 0.0, blue: 1.0, alpha: 0.5); //semi-transparent blue
		c.fillPath();
	}

A path containing three shapes
(four, if you count the move(to:)

	override func draw(_ rect: CGRect) {
		//Fill a right triangle.
		let minimum: CGFloat = min(bounds.width, bounds.height);
		let length: CGFloat = minimum * 5 / 8;       //of side

		let c: CGContext = UIGraphicsGetCurrentContext()!;

		//origin at right angle
		c.translateBy(
			x: (bounds.width + length) / 2,
			y: (bounds.height + length) / 2);
		c.scaleBy(x: 1, y: -1);

		c.beginPath();
		c.move(to: CGPoint(x: 0.0, y: 0.0));        //lower right vertex (the right angle)
		c.addLine(to: CGPoint(x: 0.0, y: length));  //upper right vertex
		c.addLine(to: CGPoint(x: -length, y: 0.0)); //lower left vertex
		c.closePath();                              //back to starting point

		c.setFillColor(red: 1.0, green: 0.0, blue: 0.0, alpha: 1.0);
		c.fillPath();
	}

Fill vs. stroke

Change the above fillPath() to strokePath(), and change setFillColor(red:green:blue:alpha:) to setStrokeColor(red:green:blue:alpha:). Blue ink would be nice. Now that we’re stroking, set the line width to 10 before calling strokePath(). You could also set the line join and line cap; see Parameters That Affect Stroking.

Fill and stroke

fillPath() and strokePath() erase the current path from the computer’s memory as they draw it on the screen. To draw the same path more than once, you must store the path in a variable of type CGMutablePath. It’s like a rubber stamp that can be stamped more than once. There is no need to call CGPathRelease in Swift.

	override func draw(_ rect: CGRect) {
		//Fill and stroke the same right triangle.
		let minimum: CGFloat = min(bounds.width, bounds.height);
		let length: CGFloat = minimum * 5 / 8;	//of side

		let p: CGMutablePath = CGMutablePath();   //right triangle
		p.move(to: CGPoint(x: 0.0, y: 0.0));      //lower right vertex (the right angle)
		p.addLine(to: CGPoint(x: 0, y: length));  //upper right vertex
		p.addLine(to: CGPoint(x: -length, y: 0)); //lower left vertex
		p.closeSubpath();

		let c: CGContext = UIGraphicsGetCurrentContext()!;
		//Origin at right angle.
		c.translateBy(
			x: (bounds.width + length) / 2,
			y: (bounds.height + length) / 2);
		c.scaleBy(x: 1, y: -1);

		c.beginPath();
		c.addPath(p);
		c.setFillColor(red: 1.0, green: 0.0, blue: 0.0, alpha: 1.0); //red
		c.fillPath();

		c.beginPath();
		c.addPath(p);
		c.setLineWidth(10.0);
		c.setStrokeColor(red: 0.0, green: 0.0, blue: 1.0, alpha: 1.0); //blue
		c.strokePath();
	}