SwiftUIのビューサイズにあわせてPathを表示する
GeometryReaderとPathでSwiftUIビューの実際のサイズにあわせて表示をおこなうコードを解説します。
「SwiftUIのPath図形表示」の続きでカーブ表示・Pathのサイズ変更と回転もサンプル付きで説明します。
※SwiftUIについて全体的なことは『SwiftUI最初の一歩』、コードの基本とビューについては『SwiftUIの文法 その1 View』を参照してください。
※この記事ではSwift言語の基本的な知識を前提にしています。コード内のキーワードや書式などの不明点は Swift5初級ガイド などを参照してください。
お知らせ
電子書籍『Swift5初級ガイド』をAppleのブックストアから出しています。サンプルは無料です。MacでもiPadでもiPhoneでも読めます。
WWDC2020で発表されたSwift 5.3に対応した第6版がダウンロード可能です。(ご購入済みの場合は無料アップデートです)
ブックストアから一度購入すると今後のアップデートは無料で読めます。
1 カーブ
Path型は Bézier curve(ベジェ曲線またはベジエ曲線)と呼ばれる、計算で作られるなめらかなカーブを描くことができます。
カーブは両端の2点とひとつまたは二つの制御点と呼ばれる点から計算されます。
数学では制御点の数は始点終点を含めて数え、3点で描く場合を「2次ベジェ曲線」、4点で描く場合を「3次ベジェ曲線」と呼ぶようです。
カーブの始点はパスの現在位置が使われます。
Pathの現在位置を設定するには move(to:) メソッドを使います。
1-1 制御点がひとつのカーブ
次の図は制御点がひとつの例です。
制御点がひとつでも二つでも制御点と始点または終点を結ぶ直線(P0-P1とP2-P1)は、始点P0または終点P1でカーブの接線となります。
このため複数のカーブや直線と連続させる場合にカーブが不連続にならないようコントロールがしやすい特徴があります。
制御点が一つのメソッド
mutating func addQuadCurve(to p: CGPoint, control cp: CGPoint)
カーブの始点は現在の点を使います。
このため描き始めは move(to:) メソッドで始点座標(図のP0)の指定が必要です。
to:引数はカーブの終点(図のP2)
control:引数はカーブの制御点(図のP1)
サンプルです。
struct SymplePath: View {
var body: some View {
VStack {
sample(points: points0)
sample(points: points1)
sample(points: points2)
}
}
func sample(points: [CGPoint]) -> some View {
ZStack {
Path { path in
path.move(to:points[0])
path.addQuadCurve(to:points[2],
control:points[1])
}
.stroke(Color.black,
style: StrokeStyle(lineWidth: lineWidth))
Path { path in
for index in points.indices {
path.addEllipse(in: CGRect(x: points[index].x-2,
y: points[index].y-2,
width: 4,
height: 4))
}
path.addLines(points)
}
.stroke(Color.green,
style: StrokeStyle(lineWidth: 1.0, lineCap: .butt))
}
}
static let xOffset: CGFloat = 30
static let yOffset: CGFloat = 80
static let bottomX: CGFloat = 170
static let sampleWidth: CGFloat = 160
let points0 = [
CGPoint(x: xOffset, y: bottomX),
CGPoint(x: xOffset, y: bottomX - 50),
CGPoint(x: xOffset + sampleWidth, y: bottomX),
]
let points1 = [
CGPoint(x: xOffset, y: bottomX),
CGPoint(x: xOffset + 0.5 * sampleWidth, y: bottomX - 50),
CGPoint(x: xOffset + sampleWidth, y: bottomX),
]
let points2 = [
CGPoint(x: xOffset, y: bottomX),
CGPoint(x: xOffset + 0.5 * sampleWidth, y: 0),
CGPoint(x: xOffset + sampleWidth, y: bottomX),
]
let lineWidth: CGFloat = 2
}
カーブの黒線は
Path { path in
path.move(to:points[0])
path.addQuadCurve(to:points[2],
control:points[1])
}
の部分で表示しています。
制御点の座標に引っ張られるイメージでカーブします。
sample(points:) メソッドはカーブを黒線で、カーブの始点終点と制御点を結ぶ直線を緑で表示します。
iPadで実行した画面です。
1-2 カーブでハート型を描く
複数組み合わせるとこのようなパスも作れます。
制御点も表示するサンプルコードです。
左右対称の4つのカーブだけで描いています。
// カーブを組み合わせて❤️を描く
struct SymplePath: View {
var body: some View {
VStack {
Path { path in
path.move(to:CGPoint(x: 60, y: 100))
path.addQuadCurve(to:CGPoint(x: 20, y: 40),
control: CGPoint(x: 20, y: 60))
path.addQuadCurve(to:CGPoint(x: 60, y: 20),
control: CGPoint(x: 20, y: 0))
path.addQuadCurve(to:CGPoint(x: 100, y: 40),
control: CGPoint(x: 100, y: 0))
path.addQuadCurve(to:CGPoint(x: 60, y: 100),
control: CGPoint(x: 100, y: 60))
path.closeSubpath()
}
.stroke(Color.red,
lineWidth: 10)
// カーブの点と制御点
ZStack {
Path { path in
path.move(to:points[0])
path.addQuadCurve(to: points[2],
control: points[1])
path.addQuadCurve(to: points[4],
control: points[3])
path.addQuadCurve(to: points[6],
control: points[5])
path.addQuadCurve(to: points[0],
control: points[7])
path.closeSubpath()
}
.stroke(Color.red,
lineWidth: 2)
Path { path in
for index in points.indices {
path.addEllipse(in: CGRect(x: points[index].x - 2,
y: points[index].y - 2,
width: 4,
height: 4))
}
path.addLines(points)
//path.closeSubpath()
}
.stroke(Color.blue,
style: StrokeStyle(lineWidth: 1.0,
lineCap: .butt))
}
}
}
let points = [
CGPoint(x: 60, y: 100),
CGPoint(x: 20, y: 60),
CGPoint(x: 20, y: 40),
CGPoint(x: 20, y: 0),
CGPoint(x: 60, y: 20),
CGPoint(x: 100, y: 0),
CGPoint(x: 100, y: 40),
CGPoint(x: 100, y: 60)
]
}
このコードをiPadで実行した画面です。
点と制御点を結ぶパスは開始点がわかるよう閉じていません。
1-3 制御点が二つのカーブ
制御点が二つのカーブ用メソッドは addCurve(to:control1:control2:) です。
mutating func addCurve(to p: CGPoint, control1 cp1: CGPoint, control2 cp2: CGPoint)
カーブの始点は現在の点を使います。
このため描き始めはmove(to:)メソッドで始点座標(下図のP0)の指定が必要です。
to:引数はカーブの終点(下図のP3)
control1:引数はカーブの一つ目の制御点(下図のP1)
control2:引数はカーブの二つ目の制御点(下図のP2)
制御点とカーブの関係の例
制御点が二つになると上図の最後のように交差するカーブも描くことができます。
この図を表示するサンプル
struct SymplePath: View {
var body: some View {
VStack {
sample(points: points0)
sample(points: points1)
sample(points: points2)
}
}
func sample(points: [CGPoint]) -> some View {
ZStack {
Path { path in
path.move(to: points[0])
path.addCurve(to: points[3],
control1: points[1],
control2: points[2])
}
.stroke(Color.black,
style: StrokeStyle(lineWidth: lineWidth))
Path { path in
for index in points.indices {
path.addEllipse(in: CGRect(x: points[index].x - 2,
y: points[index].y - 2,
width: 4,
height: 4))
}
path.addLines(points)
}
.stroke(Color.green,
style: StrokeStyle(lineWidth: 1.0,
lineCap: .butt))
}
}
static let xOffset: CGFloat = 30
static let yOffset: CGFloat = 80
static let bottomY: CGFloat = 170
static let sampleWidth: CGFloat = 160
let points0 = [
CGPoint(x: xOffset, y: bottomY),
CGPoint(x: xOffset, y: bottomY - 50),
CGPoint(x: xOffset + 0.5 * sampleWidth, y: bottomY - 50),
CGPoint(x: xOffset + sampleWidth, y: bottomY),
]
let points1 = [
CGPoint(x: xOffset, y: bottomY),
CGPoint(x: xOffset + 0.5 * sampleWidth, y: bottomY - 50),
CGPoint(x: xOffset + 0.5 * sampleWidth, y: bottomY + 50),
CGPoint(x: xOffset + sampleWidth, y: bottomY),
]
let points2 = [
CGPoint(x: xOffset, y: bottomY),
CGPoint(x: xOffset + sampleWidth + 60, y: bottomY - 100),
CGPoint(x: xOffset, y: bottomY - 100),
CGPoint(x: xOffset + sampleWidth - 50, y: bottomY),
]
let lineWidth: CGFloat = 2
}
このコードをiPadで実行した画面です。
1-4 カーブの組み合わせ例
二つのカーブを使って卵形を表示するサンプル
// 簡易卵形
struct SymplePath: View {
var body: some View {
sample()
}
func sample() -> some View {
ZStack {
Path { path in
path.move(to: points[0])
path.addCurve(to: points[3],
control1: points[1],
control2: points[2])
path.addCurve(to: points[0],
control1: points[4],
control2: points[5])
path.closeSubpath()
}
.stroke(Color.black,
style: StrokeStyle(lineWidth: lineWidth))
Path { path in
for index in points.indices {
path.addEllipse(in: CGRect(x: points[index].x - 2,
y: points[index].y - 2,
width: 4,
height: 4))
}
path.addLines(points)
path.closeSubpath()
}
.stroke(Color.green,
style: StrokeStyle(lineWidth: 1.0,
lineCap: .butt))
}
}
static let centerX: CGFloat = 180
static let topY: CGFloat = 20
static let bottomY: CGFloat = 200
let points = [
CGPoint(x: centerX, y: topY),
CGPoint(x: centerX - 40, y: topY),
CGPoint(x: centerX - 110, y: bottomY),
CGPoint(x: centerX, y: bottomY),
CGPoint(x: centerX + 110, y: bottomY),
CGPoint(x: centerX + 40, y: topY)
]
let lineWidth: CGFloat = 2
}
実行結果(緑色が制御点)
この卵形も左右対称です。
制御点が二つのカーブひとつだけで半分を描いています。
2 Pathの拡大縮小表示
Pathの輪郭情報は scale(_:anchor:) メソッドなどで拡大縮小できます。
拡大縮小しても画質に影響しないことがPathの特徴のひとつです。
画面解像度に合わせてサイズ調整が必要な部分に最適です。
// Pathのscaleメソッド
func scale(_ scale: CGFloat, anchor: UnitPoint = .center) -> ScaledShape<Path>
引数がふたつあります。
最初の引数が倍率です。(0.5なら半分、2.0なら2倍になります)
二つ目のanchor引数は拡大縮小の基準点です。
デフォルトは.centerで .bottom、 .bottomLeading、 .bottomTrailing、 .center、 .leading、 .top、 .topLeading、 .topTrailing、 .trailingなどが使えます。
scale(_:anchor:) メソッドのサンプルコード
// ★型パスの拡大縮小
struct SymplePath: View {
var body: some View {
VStack {
star(color: .green, scale: 1.0)
.border(Color.green)
star(color: .red, scale: 2.2)
.border(Color.red)
star(color: .blue, scale: 0.4)
.border(Color.blue)
}
}
// 星形を表示するパス、引数で線色とスケール指定(共通)
func star(color: Color, scale: CGFloat) -> some View {
Path { path in
path.addLines(points)
path.closeSubpath() // 5点を直線で結んだあとにパスを閉じる
}
.scale(scale, anchor: .leading)
.stroke(color,
lineWidth: 5)
}
// 星形の座標
static let topY: CGFloat = 10
static let y1: CGFloat = 80
static let y2: CGFloat = 200
static let x0: CGFloat = 10
static let x1: CGFloat = 100
static let x2: CGFloat = 200
let points = [
CGPoint(x: x0, y: y1),
CGPoint(x: x2, y: y1),
CGPoint(x: x0, y: y2),
CGPoint(x: x1, y: topY),
CGPoint(x: x2, y: y2)
]
}
iPadの実行画面
星形とビューのborderを同じ色にしています。
赤い星形がビューからはみ出し他の表示に重なっています。(表示はデバイスにより変わります)
3 Pathの回転
Pathの輪郭情報は rotation(_:anchor:) メソッドで指定角度に回転できます。
拡大縮小と同じく回転しても画質に影響しないことがPathの特徴のひとつです。
// Pathの回転メソッド
func rotation(_ angle: Angle, anchor: UnitPoint = .center) -> RotatedShape<Path>
最初の引数が角度でAngle型インスタンスで指定します。
二つ目の引数anchorは scale(_:anchor:) メソッドと同じです。(回転の中心です)
rotation(_:anchor:) メソッドのサンプルです。
// 回転のサンプル
struct SymplePath: View {
var body: some View {
VStack {
star(color: .green, angle: 0)
.border(Color.green)
star(color: .red, angle: 10) // 時計回りに10度回転
.border(Color.red)
star(color: .blue, angle: -30) // 版時計回りに30ど回転
.border(Color.blue)
}
}
func star(color: Color, angle: Double) -> some View {
Path { path in
path.addLines(points)
path.closeSubpath()
}
.rotation(.degrees(angle))
.stroke(color,
lineWidth: 5)
}
static let topY: CGFloat = 10
static let y1: CGFloat = 80
static let y2: CGFloat = 200
static let x0: CGFloat = 10
static let x1: CGFloat = 100
static let x2: CGFloat = 200
let points = [
CGPoint(x: x0, y: y1),
CGPoint(x: x2, y: y1),
CGPoint(x: x0, y: y2),
CGPoint(x: x1, y: topY),
CGPoint(x: x2, y: y2)
]
}
今後も記事を増やすつもりです。 サポートしていただけると大変はげみになります。