I'm trying to implement a simple view in SwiftUI that can be resized via a drag handle placed on its lower-left corner. The resizable view's dimensions are set via two @State variables controlling the width and height. These variables are mutated within a DragGesture's onChanged handler captured by the drag handle.
It's sort of working, but not in the way I expect. When I perform a drag gesture on the drag handle, the view gets resized in all dimensions. I want the resizing to work in the same way as a desktop window; dragging shouldn't resize the view in all directions.
Below is a recording from the iOS simulator showing the incorrect behaviour:
I understand that the centre of a SwiftUI view is considered its origin, and that's probably what's causing this behaviour. I'm not sure how to work around this.
Here's my code:
import SwiftUI
struct ContentView: View {
// Initialise to a size proportional to the screen dimensions.
@State private var width = UIScreen.main.bounds.size.width / 3.5
@State private var height = UIScreen.main.bounds.size.height / 1.5
var body: some View {
// This is the view that's going to be resized.
ZStack(alignment: .bottomTrailing) {
Text("Hello, world!")
.frame(width: width, height: height)
// This is the "drag handle" positioned on the lower-left corner of this stack.
Text("")
.frame(width: 30, height: 30)
.background(.red)
.gesture(
DragGesture()
.onChanged { value in
// Enforce minimum dimensions.
width = max(100, width value.translation.width)
height = max(100, height value.translation.height)
}
)
}
.frame(width: width, height: height, alignment: .center)
.border(.red, width: 5)
.background(.yellow)
.padding()
}
}
Edit: I was too liberal in what I removed for the minimal example above. The following snippet adds a position
constraint on the ZStack, which is used in another gesture (not shown) so the view can be dragged around.
I'm guessing this is problematic because position
specifies relative coordinates from the centre of the ZStack to the parent VStack, and as the ZStack is resized, it's being "moved" to maintain the position constraint.
import SwiftUI
struct ContentView: View {
// Initialise to a size proportional to the screen dimensions.
@State private var width = UIScreen.main.bounds.size.width / 3.5
@State private var height = UIScreen.main.bounds.size.height / 1.5
var body: some View {
VStack {
// This is the view that's going to be resized.
ZStack(alignment: .bottomTrailing) {
Text("Hello, world!")
.frame(width: width, height: height)
// This is the "drag handle" positioned on the lower-left corner of this stack.
Text("")
.frame(width: 30, height: 30)
.background(.red)
.gesture(
DragGesture()
.onChanged { value in
// Enforce minimum dimensions.
width = max(100, width value.translation.width)
height = max(100, height value.translation.height)
}
)
}
.frame(width: width, height: height)
.border(.red, width: 5)
.background(.yellow)
.padding()
// This position constraint causes the ZStack to grow/shrink in all dimensions
// when resized.
.position(x: 400, y: 400)
}
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
}
}
CodePudding user response:
You're right that the issue is caused by your view being center-aligned.
To fix this, you can wrap your view in a VStack
with a different alignment applied, e.g. .topLeading
if you want it to align to the top-left.
You also have to make sure this VStack
is taking up the available space of the view. Otherwise, it will shrink to the size of your resizable box, causing the view to stay center-aligned. You can achieve this with .frame(maxWidth: .infinity, maxHeight: .infinity)
.
TL;DR
Place your resizable box in a VStack
with a .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
modifier.
VStack { // <-- Wrapping VStack with alignment modifier
// This is the view that's going to be resized.
ZStack(alignment: .bottomTrailing) {
Text("Hello, world!")
.frame(width: width, height: height)
// This is the "drag handle" positioned on the lower-left corner of this stack.
Text("")
.frame(width: 30, height: 30)
.background(.red)
.gesture(
DragGesture()
.onChanged { value in
// Enforce minimum dimensions.
width = max(100, width value.translation.width)
height = max(100, height value.translation.height)
}
)
}
.frame(width: width, height: height, alignment: .topLeading)
.border(.red, width: 5)
.background(.yellow)
.padding()
}
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
Edit
With an additional position
modifier on the ZStack, try offsetting the x
and y
values by half of the width
and height
to position relative to the top-left origin of the ZStack:
.position(x: 400 width / 2, y: 400 height / 2)