見出し画像

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版がダウンロード可能です。(ご購入済みの場合は無料アップデートです)
ブックストアから一度購入すると今後のアップデートは無料で読めます。

6宣伝store



1 カーブ

Path型は Bézier curveベジェ曲線またはベジエ曲線)と呼ばれる、計算で作られるなめらかなカーブを描くことができます

カーブは両端の2点とひとつまたは二つの制御点と呼ばれる点から計算されます。

数学では制御点の数は始点終点を含めて数え、3点で描く場合を「2次ベジェ曲線」、4点で描く場合を「3次ベジェ曲線」と呼ぶようです。

カーブの始点はパスの現在位置が使われます。
Pathの現在位置を設定するには move(to:) メソッドを使います。


1-1 制御点がひとつのカーブ

次の図は制御点がひとつの例です。

画像6

制御点がひとつでも二つでも制御点と始点または終点を結ぶ直線(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で実行した画面です。

画像7


1-2 カーブでハート型を描く

複数組み合わせるとこのようなパスも作れます。

画像4

制御点も表示するサンプルコードです。
左右対称の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で実行した画面です。

画像8

点と制御点を結ぶパスは開始点がわかるよう閉じていません。


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)

制御点とカーブの関係の例

画像5

制御点が二つになると上図の最後のように交差するカーブも描くことができます。

この図を表示するサンプル

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で実行した画面です。

画像9


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
}

実行結果(緑色が制御点)

画像6

この卵形も左右対称です。
制御点が二つのカーブひとつだけで半分を描いています。


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の実行画面

画像10

星形とビューの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)
  ]
}

ここから先は

8,984字 / 10画像 / 1ファイル
この記事のみ ¥ 500

今後も記事を増やすつもりです。 サポートしていただけると大変はげみになります。