Home > Mobile >  Adjusting the frame of a SwiftUI view with a DragGesture resizes it in all dimensions
Adjusting the frame of a SwiftUI view with a DragGesture resizes it in all dimensions

Time:04-18

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:

Resizing the view

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)
  • Related