Created
January 15, 2024 20:14
-
-
Save igorcferreira/06c8d3367ede677454d267fd2ef04dd3 to your computer and use it in GitHub Desktop.
Fetching view frame, including on List, using GeometryReader
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
struct ListCellView: View { | |
struct Selection: Equatable { | |
let label: String | |
let frame: CGRect | |
} | |
@State private var cellFrame: CGRect = .zero | |
let label: String | |
let action: (Selection) async -> Void | |
var body: some View { | |
VStack { | |
Text("Cell: \(label)") | |
.frame(maxWidth: .infinity, alignment: .leading) | |
Text("Origin: \(cellFrame.origin.x, specifier: "%.0f") x \(cellFrame.origin.y, specifier: "%.0f")") | |
.font(.footnote) | |
.frame(maxWidth: .infinity, alignment: .leading) | |
Text("Size: \(cellFrame.size.width, specifier: "%.0f") x \(cellFrame.size.height, specifier: "%.0f")") | |
.font(.footnote) | |
.frame(maxWidth: .infinity, alignment: .leading) | |
} | |
.padding() | |
.background(.separator) | |
.fetchFrame { cellFrame = $0 } | |
.onTapGesture { Task { | |
await action(Selection(label: label, frame: cellFrame)) | |
}} | |
} | |
} | |
struct ContentView: View { | |
@State private var selection: ListCellView.Selection? = nil | |
@State private var fullViewFrame: CGRect = .zero | |
@ViewBuilder | |
var content: some View { | |
List(0..<100) { position in | |
ListCellView(label: "\(position)") { selection in | |
self.selection = selection | |
} | |
} | |
} | |
@ViewBuilder | |
func text(for selection: ListCellView.Selection) -> some View { | |
//Text being configured as a view that is independent from the placement | |
Text(selection.label) | |
.frame(maxWidth: .infinity, maxHeight: .infinity) | |
.font(.title) | |
.background(.black) | |
.foregroundStyle(.white) | |
} | |
@ViewBuilder | |
var animationRectangle: some View { | |
if let selection { | |
text(for: selection) | |
//Position the view | |
.frame( | |
width: selection.frame.width, | |
height: selection.frame.height | |
) | |
.position( | |
x: selection.frame.midX, | |
y: selection.frame.midY | |
) | |
//Animate change | |
.animation(.easeInOut, value: selection) | |
//Dismiss | |
.onTapGesture { self.selection = nil } | |
} | |
} | |
var body: some View { | |
content | |
.safeAreaPadding(.top) | |
.padding(.top) | |
.overlay { animationRectangle } | |
.ignoresSafeArea() | |
.fetchFrame { fullViewFrame = $0 } | |
} | |
} | |
#Preview { | |
ContentView() | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
extension View { | |
/// This method uses GeometryReader as an overlay to compute the view frame without interfering in the actual frame calculation. | |
/// Allowing, for example, the measure of Views of a List. | |
/// | |
/// - Parameter coordinateSpace: The desired coordinate space that the frame will be calculated | |
/// - Parameter update: Method that will receive the calculated frame | |
/// - Returns: View with an overlay | |
func fetchFrame( | |
in coordinateSpace: CoordinateSpace = .global, | |
update: @escaping (CGRect) async -> Void | |
) -> some View { | |
//GeometryReader used as overlay to avoid | |
//breaking inner view frame calculation | |
overlay { GeometryReader { proxy in | |
let proxyFrame = proxy.frame(in: coordinateSpace) | |
//Update as a task to avoid clogging the main thread | |
let _ = Task { await update(proxyFrame) } | |
//Random view to ocupy the full space and allow measure | |
Rectangle().foregroundStyle(.clear) | |
}} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Here is a demonstration of the
ContentView.swift
above in a project:Screen.Recording.2024-01-15.at.17.14.54.mov