【CGRect Quiz】負の幅/負の高さの CGSize
この記事について
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)
選択肢
-
rect2
は有効な CGRect ではない -
rect2
は有効な CGRect であって、rect1
を同一 - 上記以外
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)
選択肢
- 適切な CGSize を与えて、
rect2
を作成できる - 適切な CGSize は存在しない
解答
Q1
- 上記以外
解答の 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 の関数として提供されています。
CGRectStandardize(rect2)
width/height
幅や高さを正値で取得したい場合は、 width, height で直接取得することもできます。
rect2.width == abs(rect2.size.width)
rect2.height == abs(rect2.size.height)
Objective-C の場合は CGGeometry の関数が提供します。
CGRectGetWidth(rect2)
CGRectGetHeight(rect2)
Q2
- 適切な 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 は origin
や size
が異なっていても、矩形領域として一致していれば ==
演算子は 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