I am a mildly experienced UIKit/AppKit developer, but I am just getting started using SwiftUI. In UIKit it is straight forward to constrain two UIView's to have the same size and position using interface builder or programmatically using NSLayoutConstraints. Using SwiftUI's declarative approach should make this easier, but I find myself having to manually determine the frame size and position so I feel I am missing something.
As an example, say I want to create the View hierarchy shown in figure below. The main goal is to create a transparent CanvasView instance that lies over the top of an ImageView whose size and placement has been tailored to display the image with the correct aspect ratio.
Currently I can accomplish this by wrapping the contents of the ZStack inside
a GeometryReader and then do the layout math by hand and explicitly setting
the frame size and position of both the ImageView and CanvasView
struct WidgetView: View {
let uiimage = UIImage(named: "cateyes")!
var body: some View {
VStack(alignment: .leading) {
HStack {
Button("Left", action: {print("do left")})
Spacer()
Button("Right", action: {print("do right")})
}
.padding(.horizontal, 8)
ZStack(alignment: .center) {
GeometryReader { geom in
let aspect = uiimage.size.width / uiimage.size.height
let w = geom.size.width
let h = geom.size.width / aspect
let fits = h <= geom.size.height
let s = fits ? 1.0 : geom.size.height / h
let W = s*w
let H = s*h
let x0 = (geom.size.width - W)/2
ImageView(uiimage: uiimage)
.frame(width: W, height: H, alignment: .top)
.position(x: x0 + W/2, y: H/2)
CanvasView()
.frame(width: W, height: H, alignment: .top)
.position(x: x0 + W/2, y: H/2)
}
}
}
}
}
The ImageView simply contains a scaled view of the image that maintains its
aspect ratio:
struct ImageView: View {
var uiimage : UIImage!
var body: some View {
Image(uiImage: uiimage)
.resizable()
.aspectRatio(contentMode: .fit)
}
}
The CanvasView in this example merely draws an X from the corners of its view
struct CanvasView: View {
var body: some View {
GeometryReader { geometry in
let w = geometry.size.width
let h = geometry.size.height
Path { path in
path.move(to: CGPoint(x: 0,y: 0))
path.addLine(to: CGPoint(x: w, y: h))
path.move(to: CGPoint(x: 0, y: h))
path.addLine(to: CGPoint(x: w, y: 0))
}
.stroke(Color.black, lineWidth: 4)
}
}
}
This seems to work in this example, but I should not have to explicitly compute the layout. I am also concerned that the containing ZStack will not resize itself
correctly when this is done. Is there a more direct way to constrain two views to have the same frame (and other possible constraints) like we do in UIKit?
You can find the above solution on github.


To pick up on my comments, I would suggest making the
CanvasViewan.overlayover theImageView. An overlay automatically adopts the same frame as the view it is applied to (as does a background layer).Here is a revised version of your example to show it working. I also changed
UIImagetoImageand used aCanvasfor drawing the X.