Skip to content

Instantly share code, notes, and snippets.

@igorcferreira
Created January 15, 2024 20:14
Show Gist options
  • Save igorcferreira/06c8d3367ede677454d267fd2ef04dd3 to your computer and use it in GitHub Desktop.
Save igorcferreira/06c8d3367ede677454d267fd2ef04dd3 to your computer and use it in GitHub Desktop.
Fetching view frame, including on List, using GeometryReader
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()
}
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)
}}
}
}
@igorcferreira
Copy link
Author

Here is a demonstration of the ContentView.swift above in a project:

Screen.Recording.2024-01-15.at.17.14.54.mov

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment