2
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

はじめに

こちらのコードでどのような弧が描かれるでしょうか?

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)
                    }
            }
        )
    }
}

すぐにわかった方はこの記事を読む必要はないでしょう:zipper_mouth:

正解はこちら

arc.png

座標系

まず SwiftUI の座標系は UIKit と同じで左上が原点です。

struct ContentView: View {
    
    var body: some View {
        ZStack {
            Color.red
                .frame(width: 100, height: 100)
                .position(.init(x: 100, y: 100))
        }
    }
}

なので上記コードはこうなります。

position.png

角度はこうで下が 90 度です。

kakudo.jpg

なのでこちらも下に配置されるわけです。

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)
        )
    }
}

90.png

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)
                    }
            }
        )
    }
}

なので上記はこうなるわけです。

arc.png

おまけ(角度と座標変換)

おまけで角度と座標変換の話です。

画面中央を中心とした時に半径 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)
        )
    }
}

suquare.png

座標から角度を求めたいときはこうです!

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 を使い出すと座標やら角度がよくわからなくなりますがこれでもう楽勝です:v:

これを理解しておけば、円・時計 UI・ゲージ・グラフ・ドラッグジェスチャまで全部怖くなくなります💪

2
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?