Is there a way to get a View
's frame after layout? I'd like to draw a line connecting two views after layout has positioned them:
It seems I need something like measure
in React.
Is there a way to get a View
's frame after layout? I'd like to draw a line connecting two views after layout has positioned them:
It seems I need something like measure
in React.
Use a GeometryReader
to get the frame of each view and use the frame to determine the points for a path between the two views.
struct ContentView : View {
var body: some View {
GeometryReader { geometry -> Text in
let frame = geometry.frame(in: CoordinateSpace.local)
return Text("\(frame.origin.x), \(frame.origin.y), \(frame.size.width), \(frame.size.height)")
}
}
}
To export the "as rendered" view coordinates, I created a set of arrays for the x,y coordinates, and saved them in the Model object. However, I had to NOT encapsulate them in @Published var, instead kept them as just "var", otherwise you get into an infinite loop of updating the coordinates and then re-rendering the view.
Using Apple's 'landmark' tutorial, the Model object was modified as follows:
import SwiftUI
import Combine
final class UserData: ObservableObject {
@Published var showFavoritesOnly = false
@Published var landmarks = landmarkData
var circleImageX = Array(repeating: 0.0, count:20)
var circleImageY = Array(repeating: 0.0, count:20)
}
Then, write to those arrays each time the CircleImage.swift is rendered using the following code, again from the 'landmark.swift' tutorial, saving the frame midpoints.
import SwiftUI
struct CircleImage: View {
@EnvironmentObject var userData: UserData
var landmark: Landmark
var landmarkIndex: Int {
userData.landmarks.firstIndex(where: {$0.id == landmark.id})!
}
var body: some View {
ZStack {
landmark.image
.clipShape(Circle())
.overlay(Circle().stroke(Color.white, lineWidth: 4))
.shadow(radius: 10)
VStack {
GeometryReader { geometry -> Text in
let frame = geometry.frame(in: CoordinateSpace.global)
self.userData.circleImageX[self.landmarkIndex] = Double(frame.midX)
return
Text("\(frame.midX)")
.foregroundColor(.red)
.bold()
.font(.title)
}
.offset(x: 0.0, y: 50.0)
GeometryReader { geometry -> Text in
let frame = geometry.frame(in: CoordinateSpace.global)
self.userData.circleImageY[self.landmarkIndex] = Double(frame.midY)
return
Text("\(frame.midY)")
.foregroundColor(.red)
.bold()
.font(.title)
}
.offset(x: 0.0, y: -50.0)
}
}
}
}`
Not only does this save the rendered coordinates, it also renders them as a Text view overlaid with the image as suggested by Jake. Naturally you can delete the Text overlay view once you're satisfied the coordinates are correct. Hope this helps
struct LineView: View {
@State private var viewFrames: [CGRect] = [.zero, .zero, .zero]
@State private var path: Path? = nil
var body: some View {
ZStack{
VStack {
HStack {
Spacer()
Text("View 1")
.foregroundColor(.white)
.background(
GeometryReader { geometry in
Color.blue.onAppear {
viewFrames[0] = geometry.frame(in: .global)
path = createPath()
}
})
Spacer()
}
HStack {
Spacer()
Text("View 2")
.foregroundColor(.white)
.background(
GeometryReader { geometry in
Color.green
.onAppear {
viewFrames[1] = geometry.frame(in: .global)
path = createPath()
}
})
}
HStack {
Text("View 3")
.foregroundColor(.white)
.background(GeometryReader { geometry in
Color.orange
.onAppear {
viewFrames[2] = geometry.frame(in: .global)
path = createPath()
}
})
Spacer()
}
}
if let path = path {
path.stroke(Color.gray, lineWidth: 3)
.ignoresSafeArea()
}
}
}
private func createPath() -> Path? {
// Check if all frames are set.
guard viewFrames.allSatisfy({ !$0.isEmpty }) else {
return nil
}
// Calculate the center points of each view.
let centerPoints = viewFrames.map {
CGPoint(x: ($0.midX), y: $0.midY)
}
// Create a path connecting the center points of each view.
var path = Path()
path.move(to: centerPoints[0])
path.addLine(to: centerPoints[1])
path.addLine(to: centerPoints[2])
return path
}
}