はじめに
こちらのコードでどのような弧が描かれるでしょうか?
struct ContentView: View {
@State private var center: CGPoint = .zero
var body: some View {
ZStack {
Path { path in
path.addArc(
center: center,
radius: 100,
startAngle: .degrees(0),
endAngle: .degrees(90),
clockwise: true
)
}
.stroke(Color.red, lineWidth: 4)
}
.background(
GeometryReader { geo in
Color.clear
.onAppear {
let frame = geo.frame(in: .global)
center = CGPoint(x: frame.midX, y: frame.midY)
}
}
)
}
}
すぐにわかった方はこの記事を読む必要はないでしょう![]()
正解はこちら
座標系
まず SwiftUI の座標系は UIKit と同じで左上が原点です。
struct ContentView: View {
var body: some View {
ZStack {
Color.red
.frame(width: 100, height: 100)
.position(.init(x: 100, y: 100))
}
}
}
なので上記コードはこうなります。
角度はこうで下が 90 度です。
なのでこちらも下に配置されるわけです。
struct ContentView: View {
@State private var center: CGPoint = .zero
var body: some View {
ZStack {
Color.red
.frame(width: 100, height: 100)
.position(position)
}.background(
GeometryReader { geo in
Color.clear
.onAppear {
let frame = geo.frame(in: .global)
center = CGPoint(x: frame.midX, y: frame.midY)
}
}
)
}
private var position: CGPoint {
let radius: CGFloat = 100
let angle = Angle(degrees: 90)
let radians = CGFloat(angle.radians)
return CGPoint(
x: center.x + radius * cos(radians),
y: center.y + radius * sin(radians)
)
}
}
clockwise
なぜ clockwise: true で反時計回りのような描画になるかというとドキュメントに記載がありました。
The clockwise parameter determines the direction in which the arc is created; the actual direction of the final path is dependent on the current transformation matrix of the graphics context. In a flipped coordinate system (the default for UIView drawing methods in iOS), specifying a clockwise arc results in a counterclockwise arc after the transformation is applied.
つまり、clockwise は Y が上の世界での「時計回り」を指していて、 SwiftUI(UIKit) のような Y が下の座標系に変換されると見た目の回転方向がひっくり返るということらしいです。
細かい理屈は忘れて clockwise: true は「反時計回り」と思って使う方が楽かもしれません。
struct ContentView: View {
@State private var center: CGPoint = .zero
var body: some View {
ZStack {
Path { path in
path.addArc(
center: center,
radius: 100,
startAngle: .degrees(0),
endAngle: .degrees(90),
clockwise: true
)
}
.stroke(Color.red, lineWidth: 4)
}
.background(
GeometryReader { geo in
Color.clear
.onAppear {
let frame = geo.frame(in: .global)
center = CGPoint(x: frame.midX, y: frame.midY)
}
}
)
}
}
なので上記はこうなるわけです。
おまけ(角度と座標変換)
おまけで角度と座標変換の話です。
画面中央を中心とした時に半径 100 の円周上の 45 度、135 度、225 度、315 度の座標を直線で繋いで正方形を描くにはどうすればいいでしょうか?
こうです。
struct ContentView: View {
@State private var center: CGPoint = .zero
private let radius: CGFloat = 100
var body: some View {
ZStack {
Path { path in
let rect = CGRect(x: center.x - radius, y: center.y - radius, width: radius * 2, height: radius * 2)
path.addEllipse(in: rect)
}
.stroke(Color.red, lineWidth: 4)
Path { path in
path.move(to: position(angle: .degrees(45)))
path.addLine(to: position(angle: .degrees(135)))
}
.stroke(Color.blue, lineWidth: 4)
Path { path in
path.move(to: position(angle: .degrees(135)))
path.addLine(to: position(angle: .degrees(225)))
}
.stroke(Color.green, lineWidth: 4)
Path { path in
path.move(to: position(angle: .degrees(225)))
path.addLine(to: position(angle: .degrees(315)))
}
.stroke(Color.yellow, lineWidth: 4)
Path { path in
path.move(to: position(angle: .degrees(315)))
path.addLine(to: position(angle: .degrees(45)))
}
.stroke(Color.purple, lineWidth: 4)
}
.background(
GeometryReader { geo in
Color.clear
.onAppear {
let frame = geo.frame(in: .global)
center = CGPoint(x: frame.midX, y: frame.midY)
}
}
)
}
private func position(angle: Angle) -> CGPoint {
let radians = CGFloat(angle.radians)
return CGPoint(
x: center.x + radius * cos(radians),
y: center.y + radius * sin(radians)
)
}
}
座標から角度を求めたいときはこうです!
private func angle(position: CGPoint) -> Angle {
let dx = position.x - center.x
let dy = position.y - center.y
var radians = atan2(dy, dx) // -π〜π, 0は右方向
if radians < 0 {
// 0 ~ 360にしたいので補正
radians += 2 * .pi
}
return Angle(radians: radians)
}
print(angle(position: position(angle: .degrees(45))).degrees) // 45.0
print(angle(position: position(angle: .degrees(135))).degrees) // 135.0
print(angle(position: position(angle: .degrees(225))).degrees) // 225.0
print(angle(position: position(angle: .degrees(315))).degrees) // 315.0
これで角度・座標変換は楽々です!
おわりに
Path を使い出すと座標やら角度がよくわからなくなりますがこれでもう楽勝です![]()
これを理解しておけば、円・時計 UI・ゲージ・グラフ・ドラッグジェスチャまで全部怖くなくなります💪




