iosswiftuidraggesturemagnificationgesture

Update offset in Magnification Gesture


Hi everyone I'm working on a black rectangle that contains a yellow rectangle inside it.. I implemented the DragGesture and the MagnificationGesture to interact with the yellow rectangle. The drag gesture works perfectly by keeping the movement of the yellow rectangle always within the limits of the black rectangle that contains it.

My problem is when I use the MagnificationGesture because when I zoom in on the yellow rectangle I can no longer move it, it's as if the drag gesture is not taken into consideration..

I need the yellow rectangle to be able to be moved as desired even after zooming but always remaining within the limits of the black rectangle set within the DragGesture. Thanks for all

struct ContentView: View {
    @State private var offset: CGSize = .zero
    @State private var lastOffset: CGSize = .zero
    @State private var scale: CGFloat = 1.0
    @State private var lastScale: CGFloat = 1.0
    
    let imageSize: CGSize = .init(width: 150, height: 150)
    let containerSize: CGSize = .init(width: 300, height: 300)
    
    var body: some View {
        Rectangle()
            .stroke(.white, lineWidth: 2)
            .frame(containerSize)
            .background {
                GeometryReader {
                    let size = $0.size
                    Rectangle()
                        .fill(.yellow)
                        .frame(width: imageSize.width * scale, height: imageSize.height * scale)
                        .scaleEffect(scale)
                        .offset(offset)
                        .gesture(
                            MagnificationGesture()
                                .onChanged { value in
                                    let newScale = lastScale * value
                                    scale = min(max(newScale, 1), 3)
                                }
                                .onEnded { _ in
                                    lastScale = scale
                                })
                        .simultaneousGesture(
                            DragGesture()
                                .onChanged { value in
                                    let translation = value.translation
                                    let scaledImageSize: CGSize = .init(width: imageSize.width * scale, height: imageSize.height * scale)
                                    
                                    let xLimit = max(min(lastOffset.width + translation.width, size.width - scaledImageSize.width), 0)
                                    
                                    let yLimit = max(min(lastOffset.height + translation.height, size.height - scaledImageSize.height), 0)
                                    
                                    offset = CGSize(width: xLimit, height: yLimit)
                                }
                                .onEnded { value in
                                    lastOffset = offset
                                }
                        )
                }
            }
    }
}

Solution

  • It looks like you are applying the scaling twice, because you are also scaling the sizes being applied with the .frame modifier.

    Try commenting out the scaleEffect modifier:

    Rectangle()
        .fill(.yellow)
        .frame(width: imageSize.width * scale, height: imageSize.height * scale)
        // .scaleEffect(scale)
        // + other modifiers
    

    This resolves the bounds issue when dragged. However, you were asking in a comment, how to let the yellow rectangle scale in all directions while still staying within bounds? This will require updating the offset inside the magnification gesture. Something like:

    MagnificationGesture()
        .onChanged { value in
            let maxScale = min(size.width / imageSize.width, size.height / imageSize.height)
            let newScale = min(max(lastScale * value, 1), maxScale)
    
            // Adjust the offset to simulate a scaling anchor of .center
            // and to keep the scaled rectangle within bounds
            let prevAdjustmentX = imageSize.width * (scale - 1) / 2
            let newAdjustmentX = imageSize.width * (newScale - 1) / 2
            let dx = prevAdjustmentX - newAdjustmentX
            let prevAdjustmentY = imageSize.height * (scale - 1) / 2
            let newAdjustmentY = imageSize.height * (newScale - 1) / 2
            let dy = prevAdjustmentY - newAdjustmentY
            let x = min(max(0, offset.width + dx), size.width - (imageSize.width * newScale))
            let y = min(max(0, offset.height + dy), size.height - (imageSize.height * newScale))
            offset = CGSize(width: x, height: y)
            lastOffset = offset
            scale = newScale
        }
        .onEnded { _ in
            lastScale = scale
        }
    

    Animation