最新消息:雨落星辰是一个专注网站SEO优化、网站SEO诊断、搜索引擎研究、网络营销推广、网站策划运营及站长类的自媒体原创博客

swift - SwiftUI Zoom & Drag: Image Does Not Fully Cover Frame After Zoom + Y-Axis Drag Not Working - Stack Overflow

programmeradmin6浏览0评论

I am implementing a draggable and zoomable image inside a rotated bounding frame in SwiftUI. The image should always fully cover the frame, even after zooming, rotating, and dragging. However, I am facing two major issues:

  1. Y-axis dragging stops working after zooming.

    • Initially, dragging in both X and Y axes works fine.
    • After zooming in or out, I can only move in the X direction, but not in Y.
  2. Image does not fully cover the frame after zooming + dragging.

    • After zooming in and dragging, the edges of the image sometimes go outside the frame.
    • I need a way to prevent dragging beyond the valid movement area while ensuring the frame is always covered.

Expected Behavior:

  1. The image should always cover the frame, even after zooming in/out.
  2. Dragging should work properly in both X and Y directions, even after zooming.
  3. The image should not move beyond valid constraints, keeping it fully covering the frame at all times.
import SwiftUI

struct RotatingDraggingRectView: View {
    @State private var rectData: RectData = RectData(
        frameSize: CGSize(width: 100, height: 160),
        framePosition: CGPoint(x: 200, y: 200),
        imageSize: CGSize(width: 200, height: 150),
        imageRotation: .zero,
        imagePosition: CGPoint(x: 200, y: 300)
    )

    @State private var imagePositionAtStartOfDrag: CGPoint?
    @State private var magnificationGestureState: CGFloat = 1

    var body: some View {
        VStack {
            GeometryReader { geometry in
                ZStack {
                    Rectangle()
                        .fill(Color.yellow)
                        .frame(width: rectData.frameSize.width, height: rectData.frameSize.height)
                        .position(rectData.framePosition)

                    Rectangle()
                        .fill(Color.blue.opacity(0.5))
                        .frame(width: rectData.imageSize.width, height: rectData.imageSize.height)
                        .rotationEffect(rectData.imageRotation)
                        .position(rectData.imagePosition)
                }
                .background(Color.red)
                .frame(width: geometry.size.width, height: geometry.size.width)
                .position(CGPoint(x: geometry.size.width / 2, y: geometry.size.height / 2))
            }
            .gesture(dragGesture().simultaneously(with: zoomGesture()))

            Slider(value: $rectData.imageRotation.degrees, in: 0...180, step: 1)
                .padding()
                .onChange(of: rectData.imageRotation) {
                    adjustRectSizeForRotation()
                }
        }
        .onAppear {
            adjustRectSizeForRotation()
        }
    }

    // MARK: - Adjust Rect Size to Fit After Rotation (Preserve Aspect Ratio)
    func adjustRectSizeForRotation() {
        let angleRadians = rectData.imageRotation.radians
        let absCos = abs(cos(angleRadians))
        let absSin = abs(sin(angleRadians))

        let requiredWidth = (rectData.frameSize.width * absCos) + (rectData.frameSize.height * absSin)
        let requiredHeight = (rectData.frameSize.width * absSin) + (rectData.frameSize.height * absCos)

        let aspectRatio = rectData.imageSize.width / rectData.imageSize.height
        let scaleFactorWidth = requiredWidth / rectData.imageSize.width
        let scaleFactorHeight = requiredHeight / rectData.imageSize.height

        let scaleFactor = max(scaleFactorWidth, scaleFactorHeight)

        rectData.imageSize.width *= scaleFactor
        rectData.imageSize.height *= scaleFactor

        rectData.imagePosition = rectData.framePosition
    }

    // MARK: - Drag Gesture (Ensures Image Covers Frame)
    func dragGesture() -> some Gesture {
        DragGesture()
            .onChanged { value in
                let xBegin: CGFloat
                let yBegin: CGFloat
                if let imagePositionAtStartOfDrag {
                    xBegin = imagePositionAtStartOfDrag.x
                    yBegin = imagePositionAtStartOfDrag.y
                } else {
                    imagePositionAtStartOfDrag = rectData.imagePosition
                    xBegin = rectData.imagePosition.x
                    yBegin = rectData.imagePosition.y
                }
                let angleRadians = rectData.imageRotation.radians
                let cosAngle = cos(angleRadians)
                let sinAngle = sin(angleRadians)
                let A = rectData.frameSize.width * abs(cosAngle)
                let B = rectData.frameSize.height * abs(sinAngle)
                let maxDragLen = rectData.imageSize.width - (A + B)
                let dxMax = maxDragLen * cosAngle
                let dyMax = maxDragLen * sinAngle
                let minX = rectData.framePosition.x - abs(dxMax / 2)
                let maxX = rectData.framePosition.x + abs(dxMax / 2)
                let minY = rectData.framePosition.y - abs(dyMax / 2)
                let maxY = rectData.framePosition.y + abs(dyMax / 2)
                let xDrag = min(maxX, max(minX, xBegin + value.translation.width))
                let yDrag = min(maxY, max(minY, yBegin + value.translation.height))
                let dxDrag = xDrag - xBegin
                let dyDrag = yDrag - yBegin
                let dx: CGFloat
                let dy: CGFloat
                if dxMax == 0 || dyMax == 0 {
                    dx = dxDrag
                    dy = dyDrag
                } else {
                    let ratio = dxMax / dyMax
                    let dxAdjusted = dyDrag * ratio
                    let dyAdjusted = dxDrag / ratio
                    if abs(dxDrag - dxAdjusted) < abs(dyDrag - dyAdjusted) {
                        dx = dxAdjusted
                        dy = dyDrag
                    } else {
                        dx = dxDrag
                        dy = dyAdjusted
                    }
                }
                let newX = xBegin + dx
                let newY = yBegin + dy
                
                rectData.imagePosition.x = newX
                rectData.imagePosition.y = newY
            }
            .onEnded { _ in
                imagePositionAtStartOfDrag = nil
            }
    }

    // MARK: - Zoom Gesture
    func zoomGesture() -> some Gesture {
        MagnifyGesture()
            .onChanged { value in
                let gestureRatio = value.magnification / magnificationGestureState
                let aspectRatio = rectData.imageSize.width / rectData.imageSize.height
                
                // Calculate rotated bounding box size
                let angleRadians = rectData.imageRotation.radians
                let cosAngle = abs(cos(angleRadians))
                let sinAngle = abs(sin(angleRadians))
                
                let rotatedFrameWidth = (rectData.frameSize.width * cosAngle) + (rectData.frameSize.height * sinAngle)
                let rotatedFrameHeight = (rectData.frameSize.width * sinAngle) + (rectData.frameSize.height * cosAngle)
                
                
                let scaleFactorWidth = rotatedFrameWidth / rectData.imageSize.width
                let scaleFactorHeight = rotatedFrameHeight / rectData.imageSize.height
                let requiredScaleFactor = max(scaleFactorWidth, scaleFactorHeight)
                
                
                let minWidth = rectData.imageSize.width * requiredScaleFactor
                
                

                var newWidth = rectData.imageSize.width * gestureRatio
                var newHeight = newWidth / aspectRatio
                
                
                newWidth = max(minWidth, newWidth)
                newHeight = newWidth / aspectRatio
                
                let imageCoversFrame = newWidth >= rotatedFrameWidth && newHeight >= rotatedFrameHeight
                
                if !imageCoversFrame {

                    let centerXAdjustment = (rectData.framePosition.x - rectData.imagePosition.x)
                    let centerYAdjustment = (rectData.framePosition.y - rectData.imagePosition.y)
                    rectData.imagePosition.x += centerXAdjustment
                    rectData.imagePosition.y += centerYAdjustment
                }
                
                rectData.imageSize.width = newWidth
                rectData.imageSize.height = newHeight
                
                magnificationGestureState = value.magnification
            }
            .onEnded { _ in
                magnificationGestureState = 1.0
            }
    }
}

// MARK: - Data Models
struct RectData {
    var frameSize: CGSize
    var framePosition: CGPoint
    var imageSize: CGSize
    var imageRotation: Angle
    var imagePosition: CGPoint
}

// MARK: - Preview
struct RotatingDraggingRectView_Previews: PreviewProvider {
    static var previews: some View {
        RotatingDraggingRectView()
    }
}

Summary of Issues:

  1. Y-axis dragging stops working after zooming.
  2. After zooming and dragging, the image sometimes does not fully cover the frame.
  3. The image moves too far outside the frame when dragging.

How can I fix these issues so that:

  • The image can drag in both X and Y directions, even after zooming?
  • The image always covers the frame fully after zooming & dragging?
  • The image does not move beyond valid bounds when dragging?

Any suggestions or improvements would be greatly appreciated!

I am implementing a draggable and zoomable image inside a rotated bounding frame in SwiftUI. The image should always fully cover the frame, even after zooming, rotating, and dragging. However, I am facing two major issues:

  1. Y-axis dragging stops working after zooming.

    • Initially, dragging in both X and Y axes works fine.
    • After zooming in or out, I can only move in the X direction, but not in Y.
  2. Image does not fully cover the frame after zooming + dragging.

    • After zooming in and dragging, the edges of the image sometimes go outside the frame.
    • I need a way to prevent dragging beyond the valid movement area while ensuring the frame is always covered.

Expected Behavior:

  1. The image should always cover the frame, even after zooming in/out.
  2. Dragging should work properly in both X and Y directions, even after zooming.
  3. The image should not move beyond valid constraints, keeping it fully covering the frame at all times.
import SwiftUI

struct RotatingDraggingRectView: View {
    @State private var rectData: RectData = RectData(
        frameSize: CGSize(width: 100, height: 160),
        framePosition: CGPoint(x: 200, y: 200),
        imageSize: CGSize(width: 200, height: 150),
        imageRotation: .zero,
        imagePosition: CGPoint(x: 200, y: 300)
    )

    @State private var imagePositionAtStartOfDrag: CGPoint?
    @State private var magnificationGestureState: CGFloat = 1

    var body: some View {
        VStack {
            GeometryReader { geometry in
                ZStack {
                    Rectangle()
                        .fill(Color.yellow)
                        .frame(width: rectData.frameSize.width, height: rectData.frameSize.height)
                        .position(rectData.framePosition)

                    Rectangle()
                        .fill(Color.blue.opacity(0.5))
                        .frame(width: rectData.imageSize.width, height: rectData.imageSize.height)
                        .rotationEffect(rectData.imageRotation)
                        .position(rectData.imagePosition)
                }
                .background(Color.red)
                .frame(width: geometry.size.width, height: geometry.size.width)
                .position(CGPoint(x: geometry.size.width / 2, y: geometry.size.height / 2))
            }
            .gesture(dragGesture().simultaneously(with: zoomGesture()))

            Slider(value: $rectData.imageRotation.degrees, in: 0...180, step: 1)
                .padding()
                .onChange(of: rectData.imageRotation) {
                    adjustRectSizeForRotation()
                }
        }
        .onAppear {
            adjustRectSizeForRotation()
        }
    }

    // MARK: - Adjust Rect Size to Fit After Rotation (Preserve Aspect Ratio)
    func adjustRectSizeForRotation() {
        let angleRadians = rectData.imageRotation.radians
        let absCos = abs(cos(angleRadians))
        let absSin = abs(sin(angleRadians))

        let requiredWidth = (rectData.frameSize.width * absCos) + (rectData.frameSize.height * absSin)
        let requiredHeight = (rectData.frameSize.width * absSin) + (rectData.frameSize.height * absCos)

        let aspectRatio = rectData.imageSize.width / rectData.imageSize.height
        let scaleFactorWidth = requiredWidth / rectData.imageSize.width
        let scaleFactorHeight = requiredHeight / rectData.imageSize.height

        let scaleFactor = max(scaleFactorWidth, scaleFactorHeight)

        rectData.imageSize.width *= scaleFactor
        rectData.imageSize.height *= scaleFactor

        rectData.imagePosition = rectData.framePosition
    }

    // MARK: - Drag Gesture (Ensures Image Covers Frame)
    func dragGesture() -> some Gesture {
        DragGesture()
            .onChanged { value in
                let xBegin: CGFloat
                let yBegin: CGFloat
                if let imagePositionAtStartOfDrag {
                    xBegin = imagePositionAtStartOfDrag.x
                    yBegin = imagePositionAtStartOfDrag.y
                } else {
                    imagePositionAtStartOfDrag = rectData.imagePosition
                    xBegin = rectData.imagePosition.x
                    yBegin = rectData.imagePosition.y
                }
                let angleRadians = rectData.imageRotation.radians
                let cosAngle = cos(angleRadians)
                let sinAngle = sin(angleRadians)
                let A = rectData.frameSize.width * abs(cosAngle)
                let B = rectData.frameSize.height * abs(sinAngle)
                let maxDragLen = rectData.imageSize.width - (A + B)
                let dxMax = maxDragLen * cosAngle
                let dyMax = maxDragLen * sinAngle
                let minX = rectData.framePosition.x - abs(dxMax / 2)
                let maxX = rectData.framePosition.x + abs(dxMax / 2)
                let minY = rectData.framePosition.y - abs(dyMax / 2)
                let maxY = rectData.framePosition.y + abs(dyMax / 2)
                let xDrag = min(maxX, max(minX, xBegin + value.translation.width))
                let yDrag = min(maxY, max(minY, yBegin + value.translation.height))
                let dxDrag = xDrag - xBegin
                let dyDrag = yDrag - yBegin
                let dx: CGFloat
                let dy: CGFloat
                if dxMax == 0 || dyMax == 0 {
                    dx = dxDrag
                    dy = dyDrag
                } else {
                    let ratio = dxMax / dyMax
                    let dxAdjusted = dyDrag * ratio
                    let dyAdjusted = dxDrag / ratio
                    if abs(dxDrag - dxAdjusted) < abs(dyDrag - dyAdjusted) {
                        dx = dxAdjusted
                        dy = dyDrag
                    } else {
                        dx = dxDrag
                        dy = dyAdjusted
                    }
                }
                let newX = xBegin + dx
                let newY = yBegin + dy
                
                rectData.imagePosition.x = newX
                rectData.imagePosition.y = newY
            }
            .onEnded { _ in
                imagePositionAtStartOfDrag = nil
            }
    }

    // MARK: - Zoom Gesture
    func zoomGesture() -> some Gesture {
        MagnifyGesture()
            .onChanged { value in
                let gestureRatio = value.magnification / magnificationGestureState
                let aspectRatio = rectData.imageSize.width / rectData.imageSize.height
                
                // Calculate rotated bounding box size
                let angleRadians = rectData.imageRotation.radians
                let cosAngle = abs(cos(angleRadians))
                let sinAngle = abs(sin(angleRadians))
                
                let rotatedFrameWidth = (rectData.frameSize.width * cosAngle) + (rectData.frameSize.height * sinAngle)
                let rotatedFrameHeight = (rectData.frameSize.width * sinAngle) + (rectData.frameSize.height * cosAngle)
                
                
                let scaleFactorWidth = rotatedFrameWidth / rectData.imageSize.width
                let scaleFactorHeight = rotatedFrameHeight / rectData.imageSize.height
                let requiredScaleFactor = max(scaleFactorWidth, scaleFactorHeight)
                
                
                let minWidth = rectData.imageSize.width * requiredScaleFactor
                
                

                var newWidth = rectData.imageSize.width * gestureRatio
                var newHeight = newWidth / aspectRatio
                
                
                newWidth = max(minWidth, newWidth)
                newHeight = newWidth / aspectRatio
                
                let imageCoversFrame = newWidth >= rotatedFrameWidth && newHeight >= rotatedFrameHeight
                
                if !imageCoversFrame {

                    let centerXAdjustment = (rectData.framePosition.x - rectData.imagePosition.x)
                    let centerYAdjustment = (rectData.framePosition.y - rectData.imagePosition.y)
                    rectData.imagePosition.x += centerXAdjustment
                    rectData.imagePosition.y += centerYAdjustment
                }
                
                rectData.imageSize.width = newWidth
                rectData.imageSize.height = newHeight
                
                magnificationGestureState = value.magnification
            }
            .onEnded { _ in
                magnificationGestureState = 1.0
            }
    }
}

// MARK: - Data Models
struct RectData {
    var frameSize: CGSize
    var framePosition: CGPoint
    var imageSize: CGSize
    var imageRotation: Angle
    var imagePosition: CGPoint
}

// MARK: - Preview
struct RotatingDraggingRectView_Previews: PreviewProvider {
    static var previews: some View {
        RotatingDraggingRectView()
    }
}

Summary of Issues:

  1. Y-axis dragging stops working after zooming.
  2. After zooming and dragging, the image sometimes does not fully cover the frame.
  3. The image moves too far outside the frame when dragging.

How can I fix these issues so that:

  • The image can drag in both X and Y directions, even after zooming?
  • The image always covers the frame fully after zooming & dragging?
  • The image does not move beyond valid bounds when dragging?

Any suggestions or improvements would be greatly appreciated!

Share Improve this question edited Mar 19 at 7:39 Hector asked Mar 17 at 11:25 HectorHector 3,8092 gold badges32 silver badges46 bronze badges 2
  • When the image is zoomed in and dragged to one side, then zoomed out, it no longer fully covers the frame.@BenzyNeez – Hector Commented Mar 18 at 10:26
  • 1 @BenzyNeez thank you for your earlier input. I've since added a bounty to this question and would value any further insights you might have – Hector Commented Mar 19 at 11:40
Add a comment  | 

1 Answer 1

Reset to default 1 +100

This problem is now considerably harder than the 1-dimensional movement that was the subject of your previous question. It's now much more of a mathematical challenge and I was finding it difficult to work out formulae for computing the bounds of the drag movement. However, an alternative way to solve is to use a Path to determine the bounds instead.

First, here is an update to the diagram that I provided in my last answer. This now shows the case of a scaled and rotated rectangle surrounding the inner frame. The values l and m are the lengths of drag that are possible in the parallel and perpendicular axes:

To make sure the area of the yellow rectangle is always fully covered by the blue rectangle when the blue rectangle is dragged, the drag movement needs to be constrained to the area shown in the center of the following diagram:

This area can be defined as a Path. It is then possible to use the function lineIntersection(_:eoFill:) to find the points that intersect a slice through the area. Using this technique, the bounds of the area can be determined and used to constrain the drag movement.

Other notes:

  • Issue 2 in your question was sometimes happening if the blue rectangle was made bigger, then moved using drag before being made smaller again. To resolve this issue, the position should be re-validated and corrected whenever the magnification is changed.

  • The Path for the middle area includes a margin of 0.5 points all around it. I found that this was necessary for ensuring that lineIntersection works when at the limits (the corners of the shape) and for when the blue rectangle has not been scaled. When the rectangle has not been scaled, the area is effectively just a straight line, so by adding a margin of 0.5 around it, the line has a width of 1 point.

  • I would suggest showing the blue rectangle as an overlay, so that its size does not impact the size of the parent ZStack. You probably want to clip the overlay too.

  • In your code, you were previously setting absolute positions on the yellow and blue rectangles. I would suggest, it is easier to use natural positioning and then apply an offset to the blue rectangle instead. This way, the movement is relative to the starting position and the position validation logic is simpler. I have modified the code to work this way.

Here is the fully updated example to show it all working. It includes visualization of the drag area, you'll probably want to remove this later.

struct RotatingDraggingRectView: View {
    @State private var rectData: RectData = RectData(
        frameSize: CGSize(width: 100, height: 160),
        imageSize: CGSize(width: 200, height: 150),
        imageRotation: .zero,
        imageOffset: .zero
    )

    @State private var imageOffsetAtStartOfDrag: CGSize?
    @State private var magnificationFactor: CGFloat = 1

    var body: some View {
        VStack {
            ZStack {
                Color.red

                Rectangle()
                    .fill(Color.yellow)
                    .frame(width: rectData.frameSize.width, height: rectData.frameSize.height)
            }
            .overlay {
                Rectangle()
                    .fill(Color.blue.opacity(0.5))
                    .frame(width: rectData.imageSize.width, height: rectData.imageSize.height)
                    .rotationEffect(rectData.imageRotation)
                    .offset(rectData.imageOffset)
                    .gesture(dragGesture().simultaneously(with: zoomGesture()))
            }
            .overlay {

                // Visualization of the bounds of movement
                GeometryReader { proxy in
                    ZStack {
                        pathForPossibleMovement(bounds: boundsForMovement)
                            .stroke(.white.opacity(0.5))
                        crosshair
                            .stroke(.white, lineWidth: 2)
                    }
                    .offset(x: proxy.size.width / 2, y: proxy.size.height / 2)
                }
                .allowsHitTesting(false)
            }
            .aspectRatio(1.0, contentMode: .fit)
            .clipped()

            Slider(value: $rectData.imageRotation.degrees, in: 0...180, step: 1)
                .padding()
                .onChange(of: rectData.imageRotation) {
                    adjustRectSizeForRotation()
                }
        }
        .onAppear {
            adjustRectSizeForRotation()
        }
    }

    private var crosshair: Path {
        Path { path in
            path.move(to: CGPoint(x: rectData.imageOffset.width, y: rectData.imageOffset.height - 10))
            path.addLine(to: CGPoint(x: rectData.imageOffset.width, y: rectData.imageOffset.height + 10))
            path.move(to: CGPoint(x: rectData.imageOffset.width - 10, y: rectData.imageOffset.height))
            path.addLine(to: CGPoint(x: rectData.imageOffset.width + 10, y: rectData.imageOffset.height))
        }
    }

    // MARK: - Adjust Rect Size to Fit After Rotation (Preserve Aspect Ratio)
    func adjustRectSizeForRotation() {
        let angleRadians = rectData.imageRotation.radians
        let absCos = abs(cos(angleRadians))
        let absSin = abs(sin(angleRadians))

        let requiredWidth = (rectData.frameSize.width * absCos) + (rectData.frameSize.height * absSin)
        let requiredHeight = (rectData.frameSize.width * absSin) + (rectData.frameSize.height * absCos)

//        let aspectRatio = rectData.imageSize.width / rectData.imageSize.height
        let scaleFactorWidth = requiredWidth / rectData.imageSize.width
        let scaleFactorHeight = requiredHeight / rectData.imageSize.height

        let scaleFactor = max(scaleFactorWidth, scaleFactorHeight)

        rectData.imageSize.width *= scaleFactor
        rectData.imageSize.height *= scaleFactor
        rectData.imageOffset = .zero
    }

    private func pathForPossibleMovement(bounds: CGSize) -> Path {
        let l = bounds.width
        let m = bounds.height
        var path = Path()
        path.addRect(CGRect(x: -(l + 1) / 2, y: -(m + 1) / 2, width: l + 1, height: m + 1))
        path = path.applying(CGAffineTransform(rotationAngle: rectData.imageRotation.radians))
        return path
    }

    private var boundsForMovement: CGSize {
        let angleRadians = rectData.imageRotation.radians
        let sinAngle = sin(angleRadians)
        let cosAngle = cos(angleRadians)
        let A = rectData.frameSize.width * abs(cosAngle)
        let B = rectData.frameSize.height * abs(sinAngle)
        let l = rectData.imageSize.width - (A + B)
        let C = rectData.frameSize.width * abs(sinAngle)
        let D = rectData.frameSize.height * abs(cosAngle)
        let m = rectData.imageSize.height - (C + D)
        return CGSize(width: l, height: m)
    }

    private func validatePosition(dragOffset: CGSize) {
        let xOffsetBegin: CGFloat
        let yOffsetBegin: CGFloat
        if let imageOffsetAtStartOfDrag {
            xOffsetBegin = imageOffsetAtStartOfDrag.width
            yOffsetBegin = imageOffsetAtStartOfDrag.height
        } else {
            imageOffsetAtStartOfDrag = rectData.imageOffset
            xOffsetBegin = rectData.imageOffset.width
            yOffsetBegin = rectData.imageOffset.height
        }
        let angleRadians = rectData.imageRotation.radians
        let sinAngle = sin(angleRadians)
        let cosAngle = cos(angleRadians)

        // Determine maximum possible drag movement in the
        // parallel and perpendicular axes
        let bounds = boundsForMovement
        let l = bounds.width
        let m = bounds.height

        // Convert to width and height in x and y axes
        let maxWidth = abs(l * cosAngle) + abs(m * sinAngle)
        let maxHeight = abs(l * sinAngle) + abs(m * cosAngle)

        // Calculate min and max offsets in x and y direction
        let dxMin: CGFloat = -(maxWidth / 2)
        let dxMax: CGFloat = (maxWidth / 2)
        let dyMin: CGFloat = -(maxHeight / 2)
        let dyMax: CGFloat = (maxHeight / 2)

        // Constrain the drag movement to min/max limits
        let dx = min(dxMax, max(dxMin, xOffsetBegin + dragOffset.width))
        let dy = min(dyMax, max(dyMin, yOffsetBegin + dragOffset.height))

        // Determine the new offset, based on the drag movement
        let newOffsetX: CGFloat
        let newOffsetY: CGFloat
        if maxWidth == 0 || maxHeight == 0 {
            newOffsetX = dx
            newOffsetY = dy
        } else {

            // Get the path that defines the region of possible movement
            let rotatedBounds = pathForPossibleMovement(bounds: bounds)

            // Compute the adjusted y when fitting to x, and vice versa
            let largeOffset = max(maxWidth, maxHeight)
            let minXLine = Path { path in
                path.move(to: CGPoint(x: largeOffset, y: dy))
                path.addLine(to: CGPoint(x: -largeOffset, y: dy))
            }
            let dxAdjustedMin = minXLine.lineIntersection(rotatedBounds).currentPoint?.x ?? dxMin

            let maxXLine = Path { path in
                path.move(to: CGPoint(x: -largeOffset, y: dy))
                path.addLine(to: CGPoint(x: largeOffset, y: dy))
            }
            let dxAdjustedMax = maxXLine.lineIntersection(rotatedBounds).currentPoint?.x ?? dxMax

            let minYLine = Path { path in
                path.move(to: CGPoint(x: dx, y: largeOffset))
                path.addLine(to: CGPoint(x: dx, y: -largeOffset))
            }
            let dyAdjustedMin = minYLine.lineIntersection(rotatedBounds).currentPoint?.y ?? dyMin

            let maxYLine = Path { path in
                path.move(to: CGPoint(x: dx, y: -largeOffset))
                path.addLine(to: CGPoint(x: dx, y: largeOffset))
            }
            let dyAdjustedMax = maxYLine.lineIntersection(rotatedBounds).currentPoint?.y ?? dyMax

            // Constrain to the adjusted bounds
            let dxAdjusted = min(dxAdjustedMax, max(dxAdjustedMin, dx))
            let dyAdjusted = min(dyAdjustedMax, max(dyAdjustedMin, dy))

            // Choose whether to adjust x or y
            if abs(dx - dxAdjusted) < abs(dy - dyAdjusted) {
                newOffsetX = dxAdjusted
                newOffsetY = dy
            } else {
                newOffsetX = dx
                newOffsetY = dyAdjusted
            }
        }
        rectData.imageOffset.width = newOffsetX
        rectData.imageOffset.height = newOffsetY
    }

    // MARK: - Drag Gesture (Ensures Image Covers Frame)
    func dragGesture() -> some Gesture {
        DragGesture()
            .onChanged { value in
                validatePosition(dragOffset: value.translation)
            }
            .onEnded { _ in
                imageOffsetAtStartOfDrag = nil
            }
    }

    // MARK: - Zoom Gesture
    func zoomGesture() -> some Gesture {
        MagnifyGesture()
            .onChanged { value in
                let gestureRatio = value.magnification / magnificationFactor
                let aspectRatio = rectData.imageSize.width / rectData.imageSize.height

                // Calculate rotated bounding box size
                let angleRadians = rectData.imageRotation.radians
                let cosAngle = abs(cos(angleRadians))
                let sinAngle = abs(sin(angleRadians))

                let rotatedFrameWidth = (rectData.frameSize.width * cosAngle) + (rectData.frameSize.height * sinAngle)
                let rotatedFrameHeight = (rectData.frameSize.width * sinAngle) + (rectData.frameSize.height * cosAngle)

                let scaleFactorWidth = rotatedFrameWidth / rectData.imageSize.width
                let scaleFactorHeight = rotatedFrameHeight / rectData.imageSize.height
                let requiredScaleFactor = max(scaleFactorWidth, scaleFactorHeight)

                let minWidth = rectData.imageSize.width * requiredScaleFactor

                var newWidth = rectData.imageSize.width * gestureRatio
                var newHeight = newWidth / aspectRatio
                newWidth = max(minWidth, newWidth)
                newHeight = newWidth / aspectRatio

                rectData.imageSize.width = newWidth
                rectData.imageSize.height = newHeight
                magnificationFactor = value.magnification
                validatePosition(dragOffset: .zero)
            }
            .onEnded { _ in
                magnificationFactor = 1.0
                imageOffsetAtStartOfDrag = nil
            }
    }
}

// MARK: - Data Models
struct RectData {
    var frameSize: CGSize
    var imageSize: CGSize
    var imageRotation: Angle
    var imageOffset: CGSize
}

发布评论

评论列表(0)

  1. 暂无评论