35

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:

enter image description here

It seems I need something like measure in React.

Jake
  • 13,097
  • 9
  • 44
  • 73
Taylor
  • 5,871
  • 2
  • 30
  • 64
  • @Fogmeister I mean a line connecting the two views. I updated the question. – Taylor Jun 09 '19 at 21:22
  • 1
    @Fogmeister picture added :) – Taylor Jun 09 '19 at 21:25
  • Ah... I see now. Much clearer with the picture added. – Fogmeister Jun 09 '19 at 21:27
  • @Taylor deleted my answer as it wasn't that clear before the picture – Matteo Pacini Jun 09 '19 at 21:29
  • 1
    @MatteoPacini sorry for being unclear initially. – Taylor Jun 09 '19 at 21:37
  • Interesting problem though. As the views in your cods are not actually views. They are just an instruction set of how to create the views (or whatever they are) when they are added to the screen. I wonder if there's is a new way of inspecting views too. Like the equivalent of "viewDidAppear" etc... But maybe through accessing some osrt of closure and passing it into the "view"? Or something? – Fogmeister Jun 09 '19 at 21:55
  • Or maybe this is a sign that you should be using some other technology? Like SpriteKit for instance? Or just rending the views yourself without SwiftUI? Or just fall back to UIKit in this case? What is the app that you're creating? – Fogmeister Jun 09 '19 at 21:58
  • 1
    @Fogmeister It's already created http://audulus.com :) – Taylor Jun 09 '19 at 22:10
  • 2
    @Taylor Could you please add a snippet of how you've drawn a line between two views? The answer by Jake is really not that self-explanatory and is very short and I just cannot find anything that explains this well. Would be very appreciated. – Big_Chair Aug 15 '21 at 14:30

3 Answers3

27

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)")
        }
    }
}
Jake
  • 13,097
  • 9
  • 44
  • 73
  • You’re returning a frame as a _string_? – matt Jun 10 '19 at 04:24
  • I'm returning a `Text` view (i.e. SwiftUI's version of `UILabel`) that displays the frame on the screen as an example. – Jake Jun 10 '19 at 04:26
  • Would `CoordinateSpace.global` be better? – Taylor Jun 10 '19 at 05:03
  • I assumed the path being drawn would be under a local coordinate space so it made sense to get the frame in the local coordinate space as well. But it really depends on the view hierarchy I think. Global would def be useful in many scenarios. – Jake Jun 10 '19 at 05:34
  • Is GeometryReader supposed to work if you use it for example inside an HStack, will it layout the sister views first and then call the GeometryReader? And is there some way to get the laid out geometry of a daughter view? – Gusutafu Jun 29 '19 at 14:33
  • 26
    Does this approach actually work? How would you access the values you get from the GeometryReader, from _outside_ the view? That is where you need it, right? – Gusutafu Jul 02 '19 at 13:51
  • 1
    @Gusutafu state. – Ruben Martinez Jr. Apr 25 '21 at 19:43
  • 1
    When I use GeometryReader on a view the view becomes misplaced. – andrewz May 11 '22 at 16:21
4

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

grapeffx
  • 171
  • 1
  • 5
2
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
    }
    
}

ChrisGPT was on strike
  • 127,765
  • 105
  • 273
  • 257