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 NSLayoutConstraint
s. 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 CanvasView
an .overlay
over the ImageView
. 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 UIImage
to Image
and used a Canvas
for drawing the X.
struct WidgetView: View {
let image = Image("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)
ImageView(image: image)
.overlay { CanvasView() }
Spacer()
}
}
}
struct ImageView: View {
let image: Image
var body: some View {
image
.resizable()
.scaledToFit()
}
}
struct CanvasView: View {
var body: some View {
Canvas { context, size in
let w = size.width
let h = size.height
var path = Path()
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))
context.stroke(path, with: .color(.black), style: .init(lineWidth: 4))
}
}
}