/*
Inspecting the View Tree – Part 2: AnchorPreferences
https://swiftui-lab.com/communicating-with-the-view-tree-part-2/
When using the Anchor<Value> as an index to the GeometryProxy, you get the represented CGRect or CGPoint value. And as a plus, you get it already translated to the coordinate space of the GeometryReader view.
*/
import SwiftUI
import PlaygroundSupport
// ⭐️ 收集與處理所有的 MonthView's frame
typealias FrameAnchors = AllValues<Anchor<CGRect>>
// 🌅 MonthView
struct MonthView: View {
@Binding var currentIndex: Int // 接收與更新當前月份 (⭐️ @Binding)
let index : Int // 自己的月份
// 月份名稱
let monthNames = [
"ㄧ月", "二月", "三月","四月", "五月", "六月",
"七月", "八月", "九月","十月", "十一月", "十二月"
]
// init
init(index: Int, current: Binding<Int>) {
self._currentIndex = current
self.index = index
}
// view body
var body: some View {
Text(monthNames[index])
.padding(.horizontal, 12)
.padding(.vertical, 4)
// ---------------------------------------------------------------
// ⭐️ 將自己的 frame 加到 FrameAnchors
// .anchorPreference(key: FrameAnchors.self, value: .bounds) { [$0] }
.register(to: FrameAnchors.self)
// ---------------------------------------------------------------
.animation(.default)
// ⭐️ 當按到時,主動更新 currentIndex
.onTapGesture { self.currentIndex = self.index }
}
}
// 🌅 YearView
struct YearView: View {
// ⭐️ 當前月份
@State private var currentIndex = 0
// ------------------------------------------------------
// ⭐️ 由於使用 Anchor<CGRect> 來管理 MonthView 的 frames,
// 所以 YearView 並不需要多一個 @State 變數來負責更新的工作。
// ------------------------------------------------------
// ⭐️ 當前月份的「圓角框線」
func roundedBorder(anchors: FrameAnchors.Value) -> some View {
// ⭐️ 利用此輔助函數做計算,並傳回「圓角框線」給 GeometryReader 用
func makeView(with geo: GeometryProxy) -> some View {
// ------------------------------------------
// ⭐️ 將 anchor 轉為這個座標系統的 frame (CGRect)
let anchor = anchors[currentIndex] // Anchor<CGRect>
let rect = geo[anchor] // CGRect
// ------------------------------------------
let rounded = RoundedRectangle(cornerRadius: 4)
// 傳回「圓角框線」
return rounded
.fill(Color.yellow.opacity(0.2))
.overlay(rounded.stroke(Color.pink, lineWidth: 3))
// ⭐️ 設定尺寸
.frame(rect.size) // 🌀View + frame
// ⭐️ 設定位移 (注意:要配合 ZStack 的「對齊方式:.topLeading」才有效)
.offset(x: rect.minX, y: rect.minY)
.animation(.default)
}
return GeometryReader { makeView(with: $0) }
}
// view body
var body: some View {
// 📦 StackForEach
HStackForEach(0..<4, spacing: 16) { j in // 一年四季
VStackForEach(0..<3, spacing: 8) { i in // 每季三月
// 🌅 MonthView
MonthView(index: i + 3*j, current: self.$currentIndex)
.border(Color.gray.opacity(0.1))
}
}// HStackForEach (container)
// --------------------------------------------------
// ⭐️ 利用 FrameAnchors 的資料,在這個座標系統中畫「圓角框線」
.overlay(with: FrameAnchors.self) {
self.roundedBorder(anchors: $0) }
// --------------------------------------------------
.padding()
.background(Color.gray)
.animation(.spring())
}// body
}
struct ContentView: View {
let r: CGFloat = 1
// view body
var body: some View {
YearView() // 🌅 YearView
.shadow(color: .black, radius: r, x: r, y: r)
}
}
// live view
PlaygroundPage.current.setLiveView(ContentView())