[SwiftUI] 糸かけ曼荼羅アプリを作る(1:円周上に 図形を配置する)

SwiftUI

     

TAGS:

⌛️ 2 min.
糸かけ曼荼羅のデザイン確認できるようなアプリを作っていきます。

環境&対象

以下の環境で動作確認を行なっています。

  • macOS Big Sur 11.3
  • Xcode 12.5
  • iOS 14.5

糸かけ曼荼羅アプリ

糸かけ曼荼羅

糸かけ曼荼羅の説明ページは、こちら

「釘が打ってある板の上に、糸をかけていき、綺麗な模様を作り出すことができる」ものです。

模様を確認したい

一定の間隔で、糸をかけていくのですが、その間隔を変えることで、作り出される模様も変わってきます。

どんな模様が作られるかは、その間隔の数字だけでは想像することは難しいです。

作ってみるまでどんな模様が作られるかがわからず、進めていくことで 徐々にわかってくるのも面白そうですが、
事前に確認したくなるときもありそうです。

模様自体は、数値計算で簡単に求められそうなので、アプリで予想図を作ってみることにします。

例えば、以下のような模様ができることになります。

MandaraExample
MandaraExample

実際の作品は、上記の協会の Web ページでもみることができます。

糸かけ曼荼羅アプリ 仕様

以下のような仕様で進めていきます。

  • MVVM で作る(ViewModel が必要か微妙ですが、とりあえず)
  • テストは、SnapshotTest を取り入れる
  • Path を使って自分で作図する
  • 少しづつ作図できるようにしながら作る

糸かけ曼荼羅アプリ 設計

モデル

板に配置される釘の数は、色々とバリエーションがあるようです。釘の数を設定できるようにします。

また、かけていく糸の色を変えることで作り出される模様も見栄えが変わるので、それぞれの糸の色も設定できるようにします。

糸をかけていく間隔も設定できるようにしますし、複数本の糸を個別に設定できるようにしてみます。

ちなみに、上記の情報が その曼荼羅を再現するための設計情報に相当するものになり、モデルの保持する情報でもあります。

  • 釘数
  • かける糸の本数
  • かける糸のそれぞれの情報
    • かける間隔

上記に、アプリ向けの情報を追加して、以下のようなモデルを作ります。


// (1)
class MandaraModel {
    // (2)
    let boardSize: CGSize = CGSize(width: 400, height: 400)
    // (3)
    let radius:CGFloat = 180.0
    let center = CGPoint(x: 200.0, y: 200.0)
    // (4)
    let pinNum = 48
    // (5)
    var strings:[MandaraString] = []
}

class MandaraString {
    // (6)
    let distance: Int
    // (7)
    let color: Color
    init(_ distance:Int,_ color:Color) {
        self.distance = distance
        self.color = color
    }
}
コード解説
  1. アプリ動作中には、MandaraModel は1インスタンスを共有するので、class で定義しています
  2. 表示領域を決める情報です(将来的になくすかもしれませんが、プロトタイピングしやすいように入れました)
  3. 半径と中心の情報です
  4. 釘の数
  5. 糸情報の配列
  6. 糸をかける間隔(糸ごとに保持します)
  7. 糸の色(糸ごとに保持します)

必要なメソッド等は、ViewModel や View を進めて 必要になった時に追加することにします。

ビュー

以下の要素を ZStack を使うことで重ね合わせて表示するようにします。

  • 板と板状に配置された釘で1ビュー
  • 糸ごとに 1ビュー

モデル実装

すでにほぼ完成していますが、モデルのライフサイクルを アプリと同じようにするために、App を継承した struct で @StateObject を使って 保持します。

# アプリ名を ItokakeMandara として プロジェクトを作成しています。


//
//  ItokakeMandaraApp.swift
//
//  Created by : Tomoaki Yagishita on 2021/05/20
//  © 2021  SmallDeskSoftware
//

import SwiftUI

@main
struct ItokakeMandaraApp: App {
    // (1)
    @StateObject var model = MandaraModel()
    var body: some Scene {
        WindowGroup {
            ContentView()
                // (2)
                .environmentObject(model)
        }
    }
}
コード解説
  1. MandaraModel は、class として定義されています。class で宣言したデータを @StateObject 変数とすることで、App と同じライフサイクルを持てるようになります。
  2. .environmentObject として登録することで、ContentView 配下のビューから、@EnvironmentObject で参照できるようになり、引数として下位のビューに渡さなくて良くなります。

ビュー実装(ContentView)

まずは、(板と)釘を表示するためのビューを作成します。ContentView に (板と)釘を表すビューを追加します。


struct ContentView: View {
    @EnvironmentObject var model: MandaraModel
    var body: some View {
        HStack {
            ZStack {
                // (1)
                Color.white
                // (2)
                MandaraBoard()
            }
            // (3)
            .frame(width: model.boardSize.width, height: model.boardSize.height)
        }
        .padding()
    }
}
  • 背景を白に
  • MnadaraBoard で 釘を描画予定
  • とりあえず、サイズは、固定で作成

ビュー実装(MandaraBoard)

板と釘を描画するビューを実装していきます。

  • とりあえず、板として表示するものはない(将来的に、釘番号等を表示すると良さげ)
  • 釘は円周上に配置される
  • 配置間隔は、釘の数から算出可能
  • 実際の釘の位置は、半径・釘の数・板のサイズ から算出可能なので、Model のメソッドとして追加する
    ビュー依存の情報を算出するので、本来は ViewModel に実装するのが良さげです。

モデルに 釘の位置算出のメソッド追加(pinPos)

普通に三角関数を使って計算します。以下は、注意する点。

  • SwiftUI では、時計の3時が0° (厳密には、回転方向に気をつける点がありますが、スキップしてます)
  • 糸かけ曼荼羅では、時計の12時が0°

つまり、SwiftUIでの角度 = -90° + 糸かけ曼荼羅の角度 という関係。(実際には、radian で計算します)


class MandaraModel: ObservableObject {
    // snip 
    func pinPos(_ index:Int) -> CGPoint {
        let itoAngle = CGFloat(index) / CGFloat(pinNum) * 2.0 * CGFloat(Double.pi)
        let angle = -0.5 * CGFloat(Double.pi) + itoAngle
        let x = cos(angle) * radius + center.x
        let y = sin(angle) * radius + center.y
        return CGPoint(x: x, y: y)
    }
}

MandaraBoard

上記の pinPos メソッドを使って、MandaraBoard は、以下のようになります。


struct MandaraBoard: View {
    // (1)
    @EnvironmentObject var model: MandaraModel
    var body: some View {
        // (2)
        ForEach(0..<model.pinNum) { index in
            // (3)
            Circle()
                .fill(Color.red)
                .frame(width:3, height:3)
                // (4)
                .position(x: model.pinPos(index).x,
                          y: model.pinPos(index).y)
        }
    }
}
コード解説
  1. EnvironmentObject 経由で Model にアクセスします
  2. 釘の数分、釘を描画します
  3. Circle をサイズ指定で描画することで 釘に見立てています。(色は外部から指定可能にする方が便利かもしれません)
  4. 先ほど作成した pinPos メソッドで算出した座標に、.position モディファイアを使って配置します。

完成した MandaraBoard view

上記のコードで、以下のようなビューが表示されるようになります。

MandaraBoard
MandaraBoard

次回以降の予定です。

  • 釘の数を外部から指定可能に
  • SnapshotTesting の導入
  • かけた糸を描画するビューの作成
  • SnapshotTesting の適用範囲拡張
  • 糸の色や、かける間隔を外部から指定可能に
  • …その他…

やったこと

やったこと
  • モデルを @StateObject を使って、アプリと同じライフサイクルで 保持するようにした
  • Circle を 座標指定で描画した

説明は以上です。
不明な点やおかしな点ありましたら、こちらまで。

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です