LayoutをカスタムしてViewを円形に配置する

Layoutプロトコルに準拠した構造体を独自に定義すれば、
Viewのコレクションの配置をカスタムすることができる。

これを使って、Viewを円形に配置し、時計の文字盤を作ってみる。

# `placeSubviews`の挙動を理解する

まずは`Layout`に準拠した、`RadialLayout`を定義して、
ただのテキストを囲ってみる
```swift
struct Clock: View {
    var body: some View {
        RadialLayout {
            Text("12")
        }
    }
}

struct RadialLayout: Layout {
    func sizeThatFits(proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) -> CGSize {
        proposal.replacingUnspecifiedDimensions()
    }
    
    func placeSubviews(in bounds: CGRect, proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) {
        
    }
}
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2648522/6b794881-bd0f-2eb2-7578-596a98d7d1bc.png)

`bounds`は`RadialLayout`が占有する画面の領域。この場合だと画面全体。
`subviews`は`RadialLayout`に渡されたViewの配列。この場合だと`Text("12")`が入った配列。

placeメソッドで座標を指定すれば、その要素は指定した座標に描画される。
```swift
struct Clock: View {
    var body: some View {
        RadialLayout {
            Text("12")
        }
    }
}

struct RadialLayout: Layout {
    func sizeThatFits(proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) -> CGSize {
        proposal.replacingUnspecifiedDimensions()
    }
    
    func placeSubviews(in bounds: CGRect, proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) {
        subviews[0].place(at: CGPoint(x: 50, y: 50), proposal: .unspecified)
    }
}
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2648522/a7467c50-6093-a0ea-e83b-da063ae5a40c.png)

画面の中心を取得するには、以下のようにする。
(わかりやすいように、文字も大きく太くした)
```swift
struct Clock: View {
    var body: some View {
        RadialLayout {
            Text("12")
                .font(.system(.title, design: .rounded)).bold()
        }
    }
}

struct RadialLayout: Layout {
    func sizeThatFits(proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) -> CGSize {
        proposal.replacingUnspecifiedDimensions()
    }
    
    func placeSubviews(in bounds: CGRect, proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) {
        subviews[0].place(at: CGPoint(x: bounds.midX, y: bounds.midY), proposal: .unspecified)
    }
}
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2648522/9245c4b6-9dd2-0c47-7d75-54910dd761a8.png)

画面の中心は取得できたが、これでは要素の左上が画面の中心に配置されただけ。
要素の中心を画面の中心に配置するには、`anchor`を指定する。
```swift
struct Clock: View {
    var body: some View {
        RadialLayout {
            Text("12")
                .font(.system(.title, design: .rounded)).bold()
        }
    }
}

struct RadialLayout: Layout {
    func sizeThatFits(proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) -> CGSize {
        proposal.replacingUnspecifiedDimensions()
    }
    
    func placeSubviews(in bounds: CGRect, proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) {
        subviews[0].place(at: CGPoint(x: bounds.midX, y: bounds.midY), anchor: .center, proposal: .unspecified)
    }
}
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2648522/7f217798-5172-3b4b-d21c-b4e522ee9750.png)

`CGPoint`の`applying`メソッドを使えば、軸を回転させることができる。
（0度)
```swift
struct Clock: View {
    var body: some View {
        RadialLayout {
            Text("12")
                .font(.system(.title, design: .rounded)).bold()
        }
    }
}

struct RadialLayout: Layout {
    func sizeThatFits(proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) -> CGSize {
        proposal.replacingUnspecifiedDimensions()
    }
    
    func placeSubviews(in bounds: CGRect, proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) {
        let angle = Angle.degrees(0.0).radians
        
        var point = CGPoint(x: 0, y: -100)
            .applying(CGAffineTransform(rotationAngle: angle))
        
        point.x += bounds.midX
        point.y += bounds.midY
        
        subviews[0].place(at: point, anchor: .center, proposal: .unspecified)
    }
}
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2648522/a8f64dd6-9e90-5d69-509d-8a08d2f8c55c.png)


（90度）
```swift
struct Clock: View {
    var body: some View {
        RadialLayout {
            Text("12")
                .font(.system(.title, design: .rounded)).bold()
        }
    }
}

struct RadialLayout: Layout {
    func sizeThatFits(proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) -> CGSize {
        proposal.replacingUnspecifiedDimensions()
    }
    
    func placeSubviews(in bounds: CGRect, proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) {
        let angle = Angle.degrees(90.0).radians
        
        var point = CGPoint(x: 0, y: -100)
            .applying(CGAffineTransform(rotationAngle: angle))
        
        point.x += bounds.midX
        point.y += bounds.midY
        
        subviews[0].place(at: point, anchor: .center, proposal: .unspecified)
    }
}
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2648522/e7d0a950-d440-1485-4e89-4702978af37e.png)

あとはこれを動的にやればOK

# 動的に配置する
1~12のテキストを`RadialLayoutに渡す`
subviewをfor文で回して、indexによって角度を変える。
```swift
struct Clock: View {
    let numbers = [12, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]
    
    var body: some View {
        RadialLayout {
            ForEach(numbers, id: \.self) { item in
                Text("\(item)")
                    .font(.system(.title, design: .rounded)).bold()
            }
        }
    }
}

struct RadialLayout: Layout {
    func sizeThatFits(proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) -> CGSize {
        proposal.replacingUnspecifiedDimensions()
    }
    
    func placeSubviews(in bounds: CGRect, proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) {
        for (index, subview) in subviews.enumerated() {
            let radius = bounds.width / 3.0
            let angle = Angle.degrees(360.0 / Double(subviews.count)).radians
            
            var point = CGPoint(x: 0, y: -radius)
                .applying(CGAffineTransform(rotationAngle: angle * Double(index)))
            
            point.x += bounds.midX
            point.y += bounds.midY
            
            subview.place(at: point, anchor: .center, proposal: .unspecified)
        }
    }
}
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2648522/94fc9b97-7225-4a77-d33a-df45f2f9b491.png)


# いろいろ付け加えていい感じにする
```swift
struct Clock: View {
    let numbers = [12, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]
    
    var body: some View {
        ZStack {
            RadialLayout {
                ForEach(numbers, id: \.self) { item in
                    Text("\(item)")
                        .font(.system(.title, design: .rounded)).bold()
                }
            }
            .frame(width: 240)
            
            RadialLayout {
                ForEach(numbers, id: \.self) { item in
                    Text("\(item * 5)")
                        .font(.system(.caption, design: .rounded))
                }
            }
            .frame(width: 360)
            
            Circle()
                .strokeBorder(style: StrokeStyle(lineWidth: 10, dash: [1, 10]))
                .frame(width: 220)
        }
    }
}

struct RadialLayout: Layout {
    func sizeThatFits(proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) -> CGSize {
        proposal.replacingUnspecifiedDimensions()
    }
    
    func placeSubviews(in bounds: CGRect, proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) {
        for (index, subview) in subviews.enumerated() {
            let radius = bounds.width / 3.0
            let angle = Angle.degrees(360.0 / Double(subviews.count)).radians
            
            var point = CGPoint(x: 0, y: -radius)
                .applying(CGAffineTransform(rotationAngle: angle * Double(index)))
            
            point.x += bounds.midX
            point.y += bounds.midY
            
            subview.place(at: point, anchor: .center, proposal: .unspecified)
        }
    }
}
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2648522/62d0e637-7339-edf3-41d3-f0ed08d0a8e2.png)