💈MonthView
這個例子用到 PreferenceKey (Frames) 來記錄所有 MonthView 的 frames,然後利用這些 frames 與 currentIndex 來動態決定「圓角框線」(roundedBorder) 的位置。
Last updated
這個例子用到 PreferenceKey (Frames) 來記錄所有 MonthView 的 frames,然後利用這些 frames 與 currentIndex 來動態決定「圓角框線」(roundedBorder) 的位置。
Last updated
利用自製的 View extension .registerFrame(to: key, in: space) 來收集所有 MonthView 的 frame,然後再由 parent view 的 .onPreferenceChange() 來更新 YearView 的 @State 變數 frames
。
import SwiftUI
import PlaygroundSupport
// 月份名稱
let monthNames = [
"ㄧ月", "二月", "三月","四月", "五月", "六月",
"七月", "八月", "九月","十月", "十一月", "十二月"
]
// ⭐️ 收集與處理所有的 MonthView's frame
typealias Frames = AllValues<CGRect> // 📦 AllValues<T>
// live view
struct ContentView: View {
let r: CGFloat = 1
// view body
var body: some View {
YearView() // 🌅 YearView
.shadow(color: .black, radius: r, x: r, y: r)
}
}
PlaygroundPage.current.setLiveView(ContentView())
// 🌅 YearView
struct YearView: View {
// ⭐️ 當前月份
@State private var currentIndex = 0
// ⭐️ 準備接收來自 Frames 的資料,用於更新 MonthView
@State private var frames = [CGRect](repeating: .zero, count: 12)
// ⭐️ 當前月份的「圓角框線」
var roundedBorder: some View {
// ⭐️ 當前月份的 frame
let rect = frames[currentIndex]
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)
}
// view body
var body: some View {
// ⭐️ 對齊「左上角」:
ZStack(alignment: .topLeading) {
// 📦 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)
// ⭐️ 在這裡定義座標系統 "container"
.coordinateSpace(name: "container")
// ⭐️ current month view's border
roundedBorder
}// ZStack
.padding()
.background(Color.gray)
.animation(.spring())
// ⭐️ 根據收集來的 Frames,更新 self.frames
.onPreferenceChange(Frames.self) { self.frames = $0 }
}// body
}
// 🌅 MonthView
struct MonthView: View {
@Binding var currentIndex: Int // 接收與更新當前月份 (⭐️ @Binding)
let index : Int // 自己的月份
// 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 加到 Frames
.appendFrame(to: Frames.self, in: .named("container")) // 🌀View + ref
.animation(.default)
// ⭐️ 當按到時,主動更新 currentIndex
.onTapGesture { self.currentIndex = self.index }
}
}
// 初版: 2020.10.15
import SwiftUI
import PlaygroundSupport
// 月份名稱
let monthNames = [
"ㄧ月", "二月", "三月","四月", "五月", "六月",
"七月", "八月", "九月","十月", "十一月", "十二月"
]
// ⭐️ 收集與處理所有的 MonthView's frame
typealias Frames = AllValues<CGRect>
// 🌅 MonthView
struct MonthView: View {
@Binding var currentIndex: Int // 接收與更新當前月份 (⭐️ @Binding)
let index : Int // 自己的月份
// 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 加到 Frames
.appendFrame(to: Frames.self, in: .named("container")) // 🌀View + ref
.animation(.default)
// ⭐️ 當按到時,主動更新 currentIndex
.onTapGesture { self.currentIndex = self.index }
}
}
// 🌅 YearView
struct YearView: View {
// ⭐️ 當前月份
@State private var currentIndex = 0
// ⭐️ 準備接收來自 Frames 的資料,用於更新 MonthView
@State private var frames = [CGRect](repeating: .zero, count: 12)
// ⭐️ 當前月份的「圓角框線」
var roundedBorder: some View {
// ⭐️ 當前月份的 frame
let rect = frames[currentIndex]
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)
}
// view body
var body: some View {
// ⭐️ 對齊「左上角」:
ZStack(alignment: .topLeading) {
// 📦 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)
// ⭐️ 在這裡定義座標系統 "container"
.coordinateSpace(name: "container")
// ⭐️ current month view's border
roundedBorder
}// ZStack
.padding()
.background(Color.gray)
.animation(.spring())
// ⭐️ 根據收集來的 Frames,更新 self.frames
.onPreferenceChange(Frames.self) { self.frames = $0 }
}// 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())
🅿️ PreferenceKey (Frames = AllValues<CGRect>
)
🌀View + pref (appendFrame
)
省掉用 geo.frame(in: space) 換算座標。
省掉用 .coordinateSpace(name:) 定義座標系統。
省掉用 @State 變數來管理畫面更新。
import SwiftUI
import PlaygroundSupport
// ⭐️ 收集與處理所有的 MonthView's frame
typealias FrameAnchors = AllValues<Anchor<CGRect>>
// live view
struct ContentView: View {
let r: CGFloat = 1
// view body
var body: some View {
YearView() // 🌅 YearView
.shadow(color: .black, radius: r, x: r, y: r)
}
}
PlaygroundPage.current.setLiveView(ContentView())
// 🌅 YearView
struct YearView: View {
// ⭐️ 當前月份
@State private var currentIndex = 0
// ------------------------------------------------------
// ⭐️ 由於使用 Anchor<CGRect> 來管理 MonthView 的 frames,
// 所以 YearView 並不需要多一個 @State 變數來負責更新的工作。
// ------------------------------------------------------
// ⭐️ 當前月份的「圓角框線」
func roundedBorder(anchors: FrameAnchors.Value) -> some View {
// ⭐️ 當前月份的 anchor
let anchor = anchors[currentIndex] // Anchor<CGRect>
let rounded = RoundedRectangle(cornerRadius: 4)
// ⭐️ 利用此輔助函數做計算,並傳回「圓角框線」給 GeometryReader 用
func makeView(with geo: GeometryProxy) -> some View {
// -------------------------------------------
// ⭐️ 將 anchor 轉為這個座標系統的 frame (CGRect)
let rect = geo[anchor]
// -------------------------------------------
// 傳回「圓角框線」
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 的資料,在這個座標系統中畫「圓角框線」
.overlayPreferenceValue(FrameAnchors.self) { (anchors) in
self.roundedBorder(anchors: anchors) }
// ------------------------------------------------------
.padding()
.background(Color.gray)
.animation(.spring())
}// body
}
// 🌅 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] }
// ---------------------------------------------------------------
.animation(.default)
// ⭐️ 當按到時,主動更新 currentIndex
.onTapGesture { self.currentIndex = self.index }
}
}
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] }
// ---------------------------------------------------------------
.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 {
// ⭐️ 當前月份的 anchor
let anchor = anchors[currentIndex] // Anchor<CGRect>
let rounded = RoundedRectangle(cornerRadius: 4)
// ⭐️ 利用此輔助函數做計算,並傳回「圓角框線」給 GeometryReader 用
func makeView(with geo: GeometryProxy) -> some View {
// ⭐️ 將 anchor 轉為這個座標系統的 frame (CGRect)
let rect = geo[anchor]
// 傳回「圓角框線」
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 的資料,在這個座標系統中畫「圓角框線」
.overlayPreferenceValue(FrameAnchors.self) { (anchors) in
self.roundedBorder(anchors: anchors) }
// ------------------------------------------------------
.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())
📦 PreferenceKeys
使用自製的 🌀View + preference,可以讓語法更簡潔易懂。
import SwiftUI
import PlaygroundSupport
// ⭐️ 收集與處理所有的 MonthView's frame
typealias FrameAnchors = AllValues<Anchor<CGRect>>
// live view
struct ContentView: View {
let r: CGFloat = 1
// view body
var body: some View {
YearView() // 🌅 YearView
.shadow(color: .black, radius: r, x: r, y: r)
}
}
PlaygroundPage.current.setLiveView(ContentView())
// 🌅 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
.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) { // 🌀View + preference
self.roundedBorder(anchors: $0) }
// --------------------------------------------------
.padding()
.background(Color.gray)
.animation(.spring())
}// body
}
// 🌅 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
.register(to: FrameAnchors.self) // 🌀View + preference
// ---------------------------------
.animation(.default)
// ⭐️ 當按到時,主動更新 currentIndex
.onTapGesture { self.currentIndex = self.index }
}
}
/*
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())