Search code examples
iosswiftswiftui

UIViewRepresentable react to changes in view frame


I have a UIView that draws some things on a CALayer using paths. For the correct size of the drawing it depends on the view's frame.

Now I'm trying to use this view in SwiftUI and for this I'm using UIViewRepresentable. My first problem was that I could not get the correct frame for the view however I worked around this by wrapping my representable view in a GeometryReader which in imo is not the best but it works. Now the problem is that when my view changes its size (for example the screen is rotated) I don't know how to receive a notification so I can update my drawing.

As far as I understand, the updateUIView(_:) method is not called unless some state of the view hierarchy changes and it doesn't seem to be called on rotation.

struct MyRepresentable: UIViewRepresentable {
    var geometry: GeometryProxy

    func makeUIView(_ view: UIView, context: Context) -> UIView {
         let view = UIView()
         let size = geometry.size
         view.frame = CGRect(x: 0, y: 0, width: size.width, height: size.height)
         context.coordinator.view = view
         context.coordinator.draw()
         return view
    }

    func updateUIView(_ view: UIView, context: Context) {
         let size = geometry.size
         context.coordinator.view.frame = CGRect(x: 0, y: 0, width: size.width, height: size.height)
         context.coordinator.update()
    }
}

Edit:

The draw method looks something like this:

// locations is [CLLocation]
let points = locations.map { $0.cgPoint }
let path = CGMutablePath()
    
path.move(to: points[0])
for index in 1..<points.count {
   path.addLine(to: points[index])
}
    
var transform = getTransformation(for: path)
let transformedPath = path.copy(using: &transform)
    
let newLayer = CAShapeLayer()
newLayer.path = transformedPath
newLayer.strokeColor = UIColor.white.cgColor
newLayer.fillColor = UIColor.clear.cgColor
newLayer.lineWidth = 6

self.view.layer.addSublayer(newLayer)

Solution

  • I would create a custom UIView and do the drawing in its draw method.

    class MyView: UIView {
        var locations: [CLLocation] = []
    
        override func draw(_ rect: CGRect) {
            // put your drawing logic here...
            // you can access self.bounds here!
    
            let points = locations.map { $0.cgPoint }
            let path = CGMutablePath()
                
            path.move(to: points[0])
            for index in 1..<points.count {
               path.addLine(to: points[index])
            }
                
            var transform = getTransformation(for: path)
            let transformedPath = UIBezierPath(cgPath: path.copy(using: &transform))
            UIColor.white.setStroke()
            transformedPath.lineWidth = 6
            transformedPath.stroke()
        }
    }
    
    struct MyRepresentable: UIViewRepresentable {
    
        func makeUIView(_ view: MyView, context: Context) -> MyView {
             let view = MyView()
             // this is so that 'draw' is called every time the frame of the view changes
             view.contentMode = .redraw
    
             return view
        }
    
        func updateUIView(_ view: MyView, context: Context) {
             view.locations = someUpdatedLocations
        }
    }
    

    Also consider using a SwiftUI Canvas to do the drawing. The size of the Canvas is given as one of its closure parameters, and you can port it to UIKit using a UIHostingController.

    struct MyView: View {
        let locations: [CLLocation]
        
        var body: some View {
            Canvas { gc, size in
                // use 'size' here to do your drawing
            }
        }
    }