📐

【CGRect Quiz】負の幅/負の高さの CGSize

2021/11/07に公開

この記事について

iOS アプリ開発しているとどこかで利用するであろう CGRect
負の幅、負の高さを設定するとどのように振る舞うでしょうか?

知らなくても困らないけど、知っていると便利かも。
ということで早速 Quiz です。

Quiz

Q1

正の数 edge を与えて、次の CGRect 構造体 rect1 を作成しました。

let origin: CGPoint = ...
let edge: CGFloat = ...
let size1: CGSize = CGSize(width: edge, height: edge)
let rect1: CGRect = CGRect(origin: origin, size: size1)

rect1 はオレンジ色の矩形になりました。

このとき、以下のような負の幅/負の高さを与えた場合、
矩形 rect2 はどのように配置されますか?

let size2: CGSize = CGSize(width: -edge, height: -edge)
let rect2: CGRect = CGRect(origin: origin, size: size2)

選択肢

  1. rect2 は有効な CGRect ではない
  2. rect2 は有効な CGRect であって、rect1 を同一
  3. 上記以外

Q2

CGRect 構造体は init(origin:size:) によって、矩形の原点とサイズで初期化できますが、矩形の原点ではなく、矩形の中心で初期化できるように、次のように CGRect を拡張しました。

extension CGRect {
    /// Creates a rectangle with the specified center and size.
    init(center: CGPoint, size: CGSize) {
        let origin = CGPoint(
            x: center.x - size.width/2,
            y: center.y - size.height/2
        )
        self.init(origin: origin, size: size)
    }
}

この初期化子を用いることで、次のように構造体を作成できます。

let center: CGPoint = ...
let edge: CGFloat = ...
let size: CGSize = CGSize(width: edge, height: edge)
let rect = CGRect(center: center, size: size)

rect は水色の矩形になります。

この初期化子を用いて、次の条件を満たす2つの CGRect を作成できますか?

  • 2つの CGRect は一致する
  • 2つの CGRect の origin は異なる
let center: CGPoint = ...
let edge: CGFloat = ...
let rect1 = CGRect(
    center: center, 
    size: CGSize(width:  edge, height:  edge)
)
let rect2 = CGRect(
    center: center, 
    size: ... /* Give it an appropriate CGSize. */
)
assert(rect1 == rect2 && rect1.origin != rect2.origin)

選択肢

  1. 適切な CGSize を与えて、rect2 を作成できる
  2. 適切な CGSize は存在しない

解答

Q1

  1. 上記以外

解答の rect2 は紫色の矩形です。

解説

CGSize の width, height に負の値を設定した場合、正の場合と逆向きになります。

つまり、CGRect の原点は iOS において必ずしも矩形左上ではなく、CGSize の各値の正負で決まります。CGSize の各値が正の場合は、iOS においては左上が原点です。

補足

standardized

CGSize は負値を用いることで向きを表現できることが便利な一方で、CGRect の幅と高さは正値で、原点は左上で揃えた方が、幾何計算の際には容易な場合が多いです。

CGRect は standardized プロパティを提供していて、size の各値を正とした同一の矩形領域を返すことができます。結果として、origin も矩形領域の左上になります。

let rect3 = rect2.standardized
rect3 == rect2 // true
rect3.size.width == abs(rect2.size.width) // true
rect3.size.height == abs(rect2.size.height) // true

ちなみに Objective-C の場合、構造体はプロパティを提供できないので、 CGGeometry の関数として提供されています。

Objective-C
CGRectStandardize(rect2)
width/height

幅や高さを正値で取得したい場合は、 width, height で直接取得することもできます。

rect2.width == abs(rect2.size.width)
rect2.height == abs(rect2.size.height)

Objective-C の場合は CGGeometry の関数が提供します。

Objective-C
CGRectGetWidth(rect2)
CGRectGetHeight(rect2)

Q2

  1. 適切な CGSize を与えて、rect2 を作成できる

負の幅、または、負の高さを与えることで実現できます。

let r1 = CGRect(center: center, size: CGSize(width:  edge, height: edge))
let r2 = CGRect(center: center, size: CGSize(width: -edge, height: edge))
assert(r1 == r2 && r1.origin != r2.origin)

解説

CGSize は負の幅、負の高さも有効です。この場合、origin は左上ではなくなります。

CGRect は originsize が異なっていても、矩形領域として一致していれば == 演算子は true を返却します。

左上を原点として2つの矩形を比較したい場合は、CGRect の standardized, width/height を利用します。

サンプルコード

動作確認のためのビューは、次のように実装しました。

ContentView.swift
import UIKit

final class ContentView: UIView {
    override init(frame: CGRect) {
        super.init(frame: frame)
        initialize()
    }

    required init?(coder: NSCoder) {
        super.init(coder: coder)
        initialize()
    }

    private func initialize() {
        contentMode = .redraw
    }

    override func draw(_ rect: CGRect) {
        let origin = CGPoint(x: rect.midX, y: rect.midY)
        var edge = (min(rect.width/2, rect.height/2) * 0.8).rounded(.toNearestOrAwayFromZero)
        edge += (20-CGFloat(Int(edge)%20))

        let lineWidth = 1/UIScreen.main.nativeScale
        let m = (w: Int(rect.width) / 20, h: Int(rect.height) / 20)
        for w in 0...(Int(rect.width) / 10) {
            let color = UIColor.systemBlue
            color.setFill()
            let path = UIBezierPath(
                rect: CGRect(
                    x: CGFloat((w - m.w) * 10) + origin.x,
                    y: 0,
                    width: lineWidth,
                    height: rect.height
                )
            )
            path.fill()
        }
        for h in 0...(Int(rect.height) / 10) {
            let color = UIColor.systemBlue
            color.setFill()
            let path = UIBezierPath(
                rect: CGRect(
                    x: 0,
                    y: CGFloat((h - m.h) * 10) + origin.y,
                    width: rect.width,
                    height: lineWidth
                )
            )
            path.fill()
        }
        do {
            let color = UIColor.systemBlue
            color.setStroke()
            color.setFill()
            let lineWidth: CGFloat = 1
            var path: UIBezierPath
            do {
                path = UIBezierPath(
                    rect: CGRect(
                        x: CGFloat(((Int(rect.width) / 10)/2 - m.w) * 10) + origin.x - lineWidth/2,
                        y: 0,
                        width: lineWidth,
                        height: rect.height
                    )
                )
                path.stroke()
                path.fill()
            }
            do {
                path = UIBezierPath(
                    rect: CGRect(
                        x: 0,
                        y: CGFloat(((Int(rect.height) / 10)/2 - m.h) * 10) + origin.y - lineWidth/2,
                        width: rect.width,
                        height: lineWidth
                    )
                )
                path.stroke()
                path.fill()
            }
        }
        do {
            let color = UIColor.systemOrange
            color.setStroke()
            color.withAlphaComponent(0.5).setFill()
            let size = CGSize(width: edge, height: edge)
            let rect = CGRect(origin: origin, size: size)
            let path = UIBezierPath(rect: rect)
            path.lineWidth = 4
            path.fill()
            path.stroke()
        }
        do {
            let color = UIColor.systemPurple
            color.setStroke()
            color.withAlphaComponent(0.5).setFill()
            let size = CGSize(width: -edge, height: -edge)
            let rect = CGRect(origin: origin, size: size)
            let path = UIBezierPath(rect: rect)
            path.lineWidth = 4
            path.fill()
            path.stroke()
        }
        do {
            let color = UIColor.systemCyan
            color.setStroke()
            color.withAlphaComponent(0.5).setFill()
            let rect = CGRect(center: origin, size: CGSize(width: edge, height: edge))
            let path = UIBezierPath(rect: rect)
            path.lineWidth = 4
            path.fill()
            path.stroke()
        }
    }
}

extension CGRect {
    /// Creates a rectangle with the specified center and size.
    init(center: CGPoint, size: CGSize) {
        let origin = CGPoint(
            x: center.x - size.width/2,
            y: center.y - size.height/2
        )
        self.init(origin: origin, size: size)
    }
}

Discussion