[SwiftUI] 糸かけ曼荼羅アプリを作る(3: かけた糸を描画する)

SwiftUI

糸かけ曼荼羅のボードを表示できたので、釘にかけて糸を描画するビューを作成します

環境&対象

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

  • macOS Big Sur 11.3
  • Xcode 12.5
  • iOS 14.5

釘にかけた糸 ビュー

糸かけ曼荼羅では、糸を 一定の距離をあけて 釘にかけていきます。

通常、一定の距離として、素数が選択されます。

糸かけ曼荼羅での糸の掛け方

# SwiftUI でも Swift のトピックでもありませんが、作る上で理解が必要です・・・

# 以降では、糸をかけていく間隔を 距離 と呼びます。

”素数を距離とする”というのがポイントです。

通常 比較的大きい素数が選ばれるので、"釘の数" と "距離" は、公約数をもちません(厳密には、1以外をもちません)

ですので、糸を最初にかけた釘に再度戻ってくるのは、他の全ての釘に糸をかけた後になります。(かつ、他の釘に2回かける前に最初の釘に戻ってきます)

例えば、48本の釘がある状態で、素数 31 を距離として選択するとします。

釘に、0から時計回りで47まで番号を振り、0から糸をかけ始めるとします。

最初に、0番の釘、次に、31番の釘 続いて 62番の釘。62番目の釘はありませんが、釘は円周上に配置されていて、48番目の釘は、0番目の釘のことです。ですので、62番の釘は、14番の釘に該当します。14番の釘の次は、45番の釘、その次は、・・・・

このように進めていって、最初の釘(0番の釘)に戻ってくるまで、"釘の本数" 回糸をかけると その糸は かけ終わりです。

終了条件としては、"最初の釘に戻ってくる" or "釘の本数+1 回 糸をかける" のいずれかになります。

糸をかけるたびに終了判定をおこなうよりも、特定回数を行う方がプログラム的には容易になるので、後者を採用することにしました。

複数の糸

通常は、複数本の糸を それぞれ別の距離を使って、かけていきます。
糸ごとに、異なる距離でかけられますし、同時に 別の色を使われることが多いようです。

ということで、以下のような定義で MandaraString が定義されているわけです。


public class MandaraString {
    let distance: Int
    let color: Color
    public init(_ distance:Int,_ color:Color) {
        self.distance = distance
        self.color = color
    }
}

糸かけビュー (Itokake)

テスト

SnapshotTesting を使うテストコードでは、結果はイメージに集約されているので、事前にテストコードを書く意味は薄い気がします。

ですが、テストコードを実際に書くことで API を使うシーンを想像でき、適切な/使いやすい API 定義に役立つので、機能実装前に、テストコードを作成していきます。

ということで定義した テストコードは、以下です。


final class Test_ItoKake: XCTestCase {
    func test_ItoKake_ViewDrawing_31() throws {
        // (1)
        let sut = Itokake(MandaraString(31, .red)).environmentObject(MandaraModel(48))
        // (2)
        assertSnapshot(matching: sut.asVC(), as: .image)
    }
}
コード解説
  1. テスト対象は Itokake というビューで MandaraString を引数として受け取ります。
    MandaraBoard と同様に、MandaraModel が environmentObject として登録されていることを前提とします。
  2. 間隔を31、色を赤としたときのイメージを assertSnapshot しています

Itokake ビュー

おおよその インターフェースが定義できたので、Itokake ビューを作っていきます。

いわゆる線分を描画していくことになります。 SwiftUI では、Path という View protocol に準拠した要素を使用します。(厳密には、Path は、Shape に準拠した protocol で、Shape が View に準拠した protocol となっています。結果として、Path は、View protocol に準拠する形です。)


struct Itokake: View {
    // (1)
    @EnvironmentObject var model: MandaraModel
    // (2)
    let ito: MandaraString
    // (3)
    public init(_ ito: MandaraString) {
        self.ito = ito
    }
    
    var body: some View {
        // (4)
        Path { path in
            // (5)
            path.move(to: model.pinPos(0))
            // (6)
            for index in 1...model.pinNum {
                // (7)
                path.addLine(to: model.pinPos(index * ito.distance))
            }
        }
        .stroke(lineWidth: 0.5)
        // (8)
        .fill(ito.color)
    }
}
コード解説
  1. environmentObject で MandaraModel を参照します
  2. 描画に必要な属性を持つ MandaraString を保持します
  3. initializer は、MandaraString を受け取り、内部変数にセットします
  4. Path を使って、線分の組み合わせで描画します
  5. 線分の最初の位置を move(to:) を使って指定します。位置情報は、model.pinPos で取得しています。
  6. 終了条件として釘の本数分の描画という設計にしましたので、釘の本数分繰り返し描画していきます
  7. addLine(to:) を使うことで、現在の位置から指定された位置までへの線分を描画します
  8. MandaraString で指定されている色で描画します

テストコードを動かす

前回同様 事前にテストイメージを用意していないので、最初に動作させると確実に Failed になります。作成されたスナップショットは、以下のようになっています。

test_ItoKake_ViewDrawing_31.1.png
test_ItoKake_ViewDrawing_31.1.png

確認が少し大変でしたが、ただしく 31 おきの釘に糸がかけられていることを確認しました。

アプリに組み込む

MandaraString を設定するための UI までつくると大変なので、まずは、 MandaraBoard に重ねて表示するようにしてみます。


public struct ContentView: View {
    @EnvironmentObject var model: MandaraModel

    public var body: some View {
        VStack {
            HStack {
                Text("Pin num")
                Picker(selection: $model.pinNum, label: Text("Pin #")) {
                    Text("16")
                        .tag(16)
                    Text("48")
                        .tag(48)
                    Text("64")
                        .tag(64)
                }
                .pickerStyle(SegmentedPickerStyle())
            }

            ZStack {
                Color.white
                MandaraBoard()
                // (1)      (2)
                Itokake(MandaraString(31, .red))
            }
            .frame(width: model.boardSize.width, height: model.boardSize.height)
        }
        .padding()
    }
}
コード解説
  1. 直接 Itokake ビューを配置しています
  2. Itokake ビューに必要な MandaraString も直接値を指定して作成しています

完成したアプリは、以下のような動作です。

次は、この Itokake ビューを UI 上で設定できるようにして、重ねて表示していきます。

やったこと
  • Path の move(to:), addLine(to:) を使用して図形を描画した
  • 糸かけ曼荼羅の糸かけルールを理解した 🙂

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

コメントを残す

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