💈MonthView
這個例子用到 PreferenceKey (Frames) 來記錄所有 MonthView 的 frames,然後利用這些 frames 與 currentIndex 來動態決定「圓角框線」(roundedBorder) 的位置。
程式碼

使用 Veiw Preference
- 利用自製的 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)
使用 Anchor
- 省掉用 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 
使用 Anchor + View extension
- 使用自製的 🌀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())Last updated
Was this helpful?