Sponsor Link
環境&対象
- 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)
}
}
- テスト対象は Itokake というビューで MandaraString を引数として受け取ります。
MandaraBoard と同様に、MandaraModel が environmentObject として登録されていることを前提とします。 - 間隔を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)
}
}
- environmentObject で MandaraModel を参照します
- 描画に必要な属性を持つ MandaraString を保持します
- initializer は、MandaraString を受け取り、内部変数にセットします
- Path を使って、線分の組み合わせで描画します
- 線分の最初の位置を move(to:) を使って指定します。位置情報は、model.pinPos で取得しています。
- 終了条件として釘の本数分の描画という設計にしましたので、釘の本数分繰り返し描画していきます
- addLine(to:) を使うことで、現在の位置から指定された位置までへの線分を描画します
- MandaraString で指定されている色で描画します
テストコードを動かす
前回同様 事前にテストイメージを用意していないので、最初に動作させると確実に Failed になります。作成されたスナップショットは、以下のようになっています。
確認が少し大変でしたが、ただしく 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()
}
}
- 直接 Itokake ビューを配置しています
- Itokake ビューに必要な MandaraString も直接値を指定して作成しています
完成したアプリは、以下のような動作です。
次は、この Itokake ビューを UI 上で設定できるようにして、重ねて表示していきます。
- Path の move(to:), addLine(to:) を使用して図形を描画した
- 糸かけ曼荼羅の糸かけルールを理解した 🙂
説明は以上です。
不明な点やおかしな点ありましたら、こちらまで。
Sponsor Link