見出し画像

SwiftUIのPath図形表示

SwiftUIビューのPath表示の使い方をキホンから解説します。

※SwiftUIについて全体的なことは『SwiftUI最初の一歩』、コードの基本とビューについては『SwiftUIの文法 その1 View』を参照してください。

※この記事ではSwift言語の基本的な知識を前提にしています。コード内のキーワードや書式などの不明点は Swift5初級ガイドなどを参照してください。

お知らせ
電子書籍『Swift5初級ガイド』をAppleのブックストアから出しています。サンプルは無料です。MacでもiPadでもiPhoneでも読めます。
WWDC2020で発表されたSwift 5.3に対応した第6版がダウンロード可能です。(ご購入済みの場合は無料アップデートです)
ブックストアから一度購入すると今後のアップデートは無料で読めます。

6宣伝store

・・・

画面はタップ(クリック)すると拡大表示します。


1 まず図形入門

最初に基本をまとめました。
座標図形といった単語に苦手意識をお持ちの方もいらっしゃるかもしれませんが、美しくプログラムで表示するのは楽しいので、ぜひお付き合いください。


1-1 二次元図形を表示する座標

この記事では平面図形(二次元図形)の表示を扱います。
二次元なので座標は x, y です。
xが横軸、yが縦軸でPathを表示する場合の原点(0, 0) はビューの左上の角です。

画像17

xがゼロなら左端、xの値が大きくなるほど右の位置を表します。
yがゼロなら上端、yの値が大きくなるほど下の位置を表します。


1-2 座標の型CGPointとサイズの型CGSize

macOSやiOSなどApple製品では座標はCGPoint(シージーポイント)型で表現します。

CGではじまる型はCore Graphics(コアグラフィクス)フレームワークで定義されています。

struct CGPoint

// プロパティ
var x: CGFloat
var y: CGFloat

xとy座標プロパティを表す数値のCGFloat(ジージーフロート)型は座標やサイズ専用の実数型です。(MacやiPadなど64bit環境ではDoubleと同等です)

CGPoint型もSwiftではインスタンスを使うので、イニシャライザは複数あり座標値にDouble、CGFloat、Int型が使えます。(ただしxとyは同じ型)

CGPoint(x: 200, y: 100)

などとできます。


大きさを表すにはCGSize(シージーサイズ)型を使います。

struct CGSize

// プロパティ
var width: CGFloat
var height: CGFloat

widthは横方向つまりx座標方向の幅です。
heightは縦方向つまりy座標方向の高さです。

こちらもイニシャライザは複数あり幅と高さ指定にDouble、CGFloat、Int型が使えます。(ただしwidthとheightは同じ型)

let s = CGSize(width: 120, height: 75)

などとできます。


1-3 長方形をあらわすCGRect型

図形の基本である長方形(矩形《くけい》と呼ぶこともあります)をあらわすにはCGRect(シージーレクト)型を使います。
長方形を意味するrectangle(レクタングル)から命名されています。

struct CGRect

// 主要プロパティ
var origin: CGPoint   // 原点
var size: CGSize      // サイズ

CGRectの原点(origin)は左上かどの座標です。
sizeプロパティは長方形の幅と高さです。

プロパティは他にもたくさんあります。
(heightなども直接得られます)

イニシャライザは4つの引数で原点x、原点y、幅、高さを設定するものと2つの引数で原点とサイズを指定するものがあります。

// CGRectのイニシャライザ
init(x: Int, y: Int, width: Int, height: Int)   // 引数はDouble、CGFloatもあり
init(origin: CGPoint, size: CGSize)

Swift の CGRectには便利なプロパティなどが追加になっています。
従来は関数などで得ていた値もプロパティで得られ便利です。

Playgroundsの各行の結果ではQuickLookで直感的な表示で確認できます。

図形型


2 Path パス

ここでは図形のPath(パス)です。

コンピュータ特にUNIXの世界では、ファイルやディレクトリの位置を表す文字列も同じ"path"(パス)ですが、もちろん別のものです。

図形のPathは輪郭情報で二次元の形状をあらわします

SwiftUIではPath型として定義されています。

struct Path

Equatable、LosslessStringConvertible、Shapeプロトコルに準拠しています。
Shape準拠なのでViewでもあり、アニメーションにも対応です。


2-1 パス入門

図形としてのパスは、輪郭の情報をもとに描く線画です。
輪郭線を描く縁取り機能(stroke)と塗りつぶし機能(fill)どちらかで図形を表示できます。

線画ですが、一筆書きには限定されません。
このため複雑なアイコンや、文字もパスを使って表現できます。

パスで描けるのは直線や円・円弧はもちろんカーブも自在です。

パスは閉じたものと、閉じていないものがあります。


2-2 点を直線で結ぶパス

最もシンプルに点を結ぶ三角形を描くコードです。

import SwiftUI
import PlaygroundSupport

struct SymplePath: View {

  var body: some View {
     Path { path in
        path.addLines([
           CGPoint(x: 100, y: 0),
           CGPoint(x: 0, y: 100),
           CGPoint(x: 200, y: 100),
           CGPoint(x: 100, y: 0)
        ])
        
     }
     .stroke()
  }
}

// playgroundで実行する場合に必要なコード
PlaygroundPage.current.setLiveView(SymplePath())

1行目2行目と最後の行は以下の全サンプルコードの実行に必要です。

iPadでの実行画面

画像8

Path形はViewでもあるのでSwiftUIのViewのbodyプロパティでこのように使えます。
Path { path in の部分はPath型イニシャライザの一つで、クロージャでパスの線画を構成しクロージャの引数で返します。(クロージャの引数pathはinoutです)

// このサンプルで使ったPath型のイニシャライザ
init(_ callback: (inout Path) -> ())

path.addLines([ の部分は複数の点を結ぶ直線をパスに追加します。
複数の点を直線で結ぶ一筆書きの形をパスインスタンスに追加するメソッドです。

mutating func addLines(_ lines: [CGPoint])

path.addLines([CGPoint(x: 100, y: 0), ...座標100, 0がパスの始点になります。
サンプルのように座標を直接並べてもよいですが、CGPoint配列のインスタンスを渡すこともできます。

.stroke() はこのパスの枠線を線幅1のデフォルト色(ライトモードなら黒)で描く指定です。

func stroke(lineWidth: CGFloat = 1) -> some Shape

strokeは引数のちがう複数のメソッドがあります。


2-3 塗りつぶしと枠線

塗りつぶしは .fill() メソッドを使います。
.fill(Color.green) とすると緑色で塗りつぶします。

struct SymplePath: View {

  var body: some View {
     VStack {
        Path { path in
           path.addLines([
              CGPoint(x: 100, y: 0),
              CGPoint(x: 0, y: 100),
              CGPoint(x: 200, y: 100),
              CGPoint(x: 100, y: 0)
           ])
        }
        .stroke()

        Path { path in
           path.addLines([
              CGPoint(x: 100, y: 0),
              CGPoint(x: 0, y: 100),
              CGPoint(x: 200, y: 100),
              CGPoint(x: 100, y: 0)
           ])
        }
        .fill(Color.green)
        
        Path { path in
           path.addLines([
              CGPoint(x: 100, y: 0),
              CGPoint(x: 0, y: 100),
              CGPoint(x: 200, y: 100),
              CGPoint(x: 100, y: 0)
           ])
        }
        // .stroke()も.fill(Color.green)も指定しないとデフォルト色で塗りつぶす

     }
  }
}

VStackで三つのPathを縦に並べています。

座標を指定したパスインスタンスの表示はViewサイズに影響されません。

iPadで実行した画面です。

画像9

.fill(Color.green) はFillStyleを省略した書き方です。
FillStyle型については 6 FillStyle を参照してください。
func fill<S>(_ content: S, style: FillStyle = FillStyle()) -> some View
 where S : ShapeStyle


2-4 いろいろなPath

Path型にはいろいろなイニシャライザがあります。
三つだけ紹介します。(詳しくはPath型のドキュメントを見てください)

init(_ rect: CGRect)   // 長方形のパスをつくる
init(ellipseIn rect: CGRect)   // 楕円形のパスをつくる
init(roundedRect rect: CGRect,
   cornerRadius: CGFloat,
   style: RoundedCornerStyle = .circular)   // 角丸長方形のパスをつくる

角丸長方形はもう一つイニシャライザがありますが割愛します。

サンプルです。

struct SymplePath: View {

  var body: some View {
     VStack {
        Path(CGRect(x: 10, y: 10, width: 200, height: 100))
        
        Path(ellipseIn: CGRect(x: 10, y: 10, width: 200, height: 100))
           .fill(Color.green)

        Path(roundedRect: CGRect(x: 10, y: 0, width: 200, height: 100),
            cornerRadius: 40)
     }

  }
}

VStackでそれぞれのPathを並べています。

画像10

.fillも.strokeも指定しない場合はデフォルトの黒で塗りつぶされます。(ライトモードの場合)


3 StrokeStyle

枠線表示の詳細はStrokeStyle型で設定できます。

struct StrokeStyle
// プロパティ 
var lineWidth: CGFloat    // 線幅
var lineCap: CGLineCap    // 端の形
var lineJoin: CGLineJoin  // 角の形
var miterLimit: CGFloat   // 角の制限
var dash: [CGFloat]       // 点線指定
var dashPhase: CGFloat    // 点線位相

イニシャライザ、各パラメータはデフォルトが定められていてすべて指定を省略すると幅1の実線(点線ではない連続した線)になります。

init(lineWidth: CGFloat = 1,     // 
   lineCap: CGLineCap = .butt,   // 
   lineJoin: CGLineJoin = .miter,// 
   miterLimit: CGFloat = 10,     // 
   dash: [CGFloat] = [CGFloat](),// 
   dashPhase: CGFloat = 0)       // 

StrokeStyle() とするとすべてデフォルトの設定(幅1の実線)で描きます。
lineWidthの指定はパスの枠線を中心線として両側に均等に太くなります。

miterLimit(接続部の表示最大値)も試しましたが、予想した形状に制御できなかったのでサンプルには含めていません。


3-1 lineCap

CGLineCap型は線端形状です。
切り落としbutt、半円round、半正方形squareを選択できます。
roundとsquareを指定すると線幅の半分だけ線を長く表示します。
それぞれのサンプルです。

画像10

サンプルコード

struct SymplePath: View {

  var body: some View {
     VStack {
        sample(lineCap: .butt)
        sample(lineCap: .round)
        sample(lineCap: .square)
     }
  }

  func sample(lineCap: CGLineCap) -> some View {
     ZStack {
        // 黒い太線
        Path { path in
           path.addLines(points)
        }
        .stroke(Color.black,
              style: StrokeStyle(lineWidth: lineWidth,
                             lineCap: lineCap))

        // パスの始点終点を結ぶ白い線
        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.white,
              style: StrokeStyle(lineWidth: 1.0,
                             lineCap: .butt))

     }
  }
  
  static let xOffset: CGFloat = 80
  static let yOffset: CGFloat = 80
  static let sampleWidth: CGFloat = 100
  let points = [
     CGPoint(x: xOffset, y: yOffset),
     CGPoint(x: xOffset + sampleWidth, y: yOffset)
  ]
  let lineWidth: CGFloat = 60
}

lineCapを引数にした sample(lineCap:)メソッドで表示しています。
一本の直線の両端の表示の違いを確認してください。

「// パスの始点終点を結ぶ白い線」部分は for index in points.indices { でパス座標に丸印を表示しています。
points.indices は points配列の範囲を返すプロパティです。

Color.blackを指定したのでライトモードで確認してください。


3-2 lineJoin

CGLineJoin型は接続部形状です。
.bevel、.miter、.roundから選びます。

画像10

サンプルコード

struct SymplePath: View {
  
  var body: some View {
     VStack {
        sample(lineJoin: .bevel)
        sample(lineJoin: .miter)
        sample(lineJoin: .round)
     }
  }
  
  func sample(lineJoin: CGLineJoin) -> some View {
     ZStack {
        // 太い直線
        Path { path in
           path.addLines(points)
        }
        .stroke(Color.black,
              style: StrokeStyle(lineWidth: lineWidth,
                             lineJoin: lineJoin))

        // パスの各点を結ぶ白い線
        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.white,
              style: StrokeStyle(lineWidth: 1.0,
                             lineCap: .butt))

     }
  }
  static let xOffset: CGFloat = 80
  static let topY: CGFloat = 50
  static let bottomX: CGFloat = 170
  static let sampleWidth: CGFloat = 100
  let points = [
     CGPoint(x: xOffset, y: bottomX),
     CGPoint(x: xOffset + 0.5 * sampleWidth, y: topY),
     CGPoint(x: xOffset + sampleWidth, y: bottomX),
  ]
  let lineWidth: CGFloat = 40
}

構造はlineCapと全く同じで、引数をlineJoinに変更し、二つの直線の角で違いを確認するサンプルです。

Color.blackを指定したのでライトモードで確認してください。


3-3 dash

点線を配列で設定します。

dashの指定サンプルは「SwiftUI UI部品カタログ 後編」の 11-8 点線 を参照してください。


3-4 dashPhase

点線をどこから描き始めるかの指定です。
緑の丸がパスで設定した点座標です。

点線はdash配列の合計長さで表示を繰り返します。
dashPhaseは最初の描き始めを長さで指定します。
このためdashPhaseで指定する数値の範囲はゼロから最大dash配列の合計長さまでとなります。
(dashPhaseをdash配列の合計長さに設定するとゼロにした場合と同じ表示になります)

画像10

サンプルコード

struct SymplePath: View {
  
  var body: some View {
     VStack {
        sample()
        sample(dashPhase: 10)
        sample(dashPhase: 20)
     }
  }

  //
  
  func sample(dashPhase: CGFloat = 0) -> some View {
     ZStack {
        // 太い点線
        Path { path in
           path.addLines(points)
        }
        .stroke(Color.black,
              style: StrokeStyle(lineWidth: lineWidth,
                             dash: [10, 10, 40, 10],
                             dashPhase: dashPhase ))

        // パスの各点を結ぶ緑の線
        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 = 80
  static let topY: CGFloat = 50
  static let bottomX: CGFloat = 170
  static let sampleWidth: CGFloat = 100
  let points = [
     CGPoint(x: xOffset, y: bottomX),
     CGPoint(x: xOffset + 0.5 * sampleWidth, y: topY),
     CGPoint(x: xOffset + sampleWidth, y: bottomX),
  ]
  let lineWidth: CGFloat = 20
}

基本はlineJoinと同じです。
点線の表示しない部分と区別するため、パスの座標と中央線を緑にしています。


4 閉じたパス

パスは closeSubpath() メソッドで閉じることができます。

最初のサンプル(2-2 点を直線で結ぶ)では3点を結ぶ連続する3直線のパスで三角形を表示しました。
p0とp1、p1とp2、p2とp0を結ぶ三つのパスを指定していました。

p0とp1、p1とp2を結んだあとにcloseSubpath() するとp2から開始点であるp0を結び閉じたパスになります


4-1 閉じたパスと閉じていないパスの違い

三角形の例では塗りつぶす場合、閉じたパスと閉じていないパスでは違いはありません

閉じたパスでは輪郭線の表示がかわります。

閉じることで最後の点と始点を直線で結ぶパスになります。
サンプルで確認してください。

もうひとつ線幅が細い場合は目立ちませんが、角の表示が違います。
閉じていない場合は始点と終点にlineCapを表示します。
閉じたパスでは端がなくなりlineCapは使われません。
かわりにlineJoinを表示します。

次のサンプルで確認しましょう。

// 閉じたパス
struct SymplePath: View {

  var body: some View {
     VStack {
        HStack {
           triangle2(close: false)
              .stroke(lineColor,
                    style: StrokeStyle(lineWidth: lineWidth,
                                   lineJoin: .miter))
           triangle2(close: false)
        }

        HStack {
           triangle2(close: true)
              .stroke(lineColor,
                    style: StrokeStyle(lineWidth: lineWidth,
                                   lineJoin: .miter))
           triangle2(close: true)
        }

        HStack {
           triangle()
              .stroke(lineColor,
                    style: StrokeStyle(lineWidth: lineWidth,
                                   lineJoin: .miter))
           triangle()
        }

     }
  }

  let taHeight: CGFloat = 60
  let taWidth: CGFloat = 120
  let orignX: CGFloat = 50
  let orignY: CGFloat = 100
  let lineWidth: CGFloat = 30
  let lineColor = Color.green
  
  /// 閉じたパスの確認 3点を結ぶ2直線のパス
  /// - Parameter close: trueにすると閉じたパスで表示
  /// - Returns: Pathインスタンス
  func triangle2(close: Bool) -> Path {
     Path { path in
        path.addLines([
           CGPoint(x: orignX + 0.5 * taWidth, y: orignY),
           CGPoint(x: orignX, y: orignY + taHeight),
           CGPoint(x: orignX + taWidth, y: orignY + taHeight)
        ])
        // パスを閉じると始点まで直線で結ぶ
        if close {
           path.closeSubpath()
        }
     }
  }
  
  /// 3点を結ぶ3直線の閉じていないパス
  /// - Returns: Pathインスタンス
  func triangle() -> Path {
     Path { path in
        path.addLines([
           CGPoint(x: orignX + 0.5 * taWidth, y: orignY),
           CGPoint(x: orignX, y: orignY + taHeight),
           CGPoint(x: orignX + taWidth, y: orignY + taHeight),
           CGPoint(x: orignX + 0.5 * taWidth, y: orignY)
        ])
     }
  }

}

strokeメソッドで枠線表示をしないパスは、デフォルト色で塗りつぶしされます。
二辺のみで閉じていない場合と、閉じた場合、開始点まで結んだ場合で塗りつぶしに違いは見られません。

iPadでの実行画面
上から❶2直線による閉じていないパス、❷2直線による閉じたパス、❸3直線による閉じていないパス です。

画像11


5 円弧 Arc

円弧は円周の一部だけを切り取った図形です。

ここから先は

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

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