You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

SwiftUI视图集成捏合缩放旋转手势与现有拖拽功能的实现方案

集成双指捏合缩放旋转到SwiftUI可编辑文本视图

核心解决思路

  • 将MagnificationGesture与RotationGesture组合为复合手势,实现双指缩放+旋转的联动操作
  • 利用simultaneousGesture让复合手势与现有单指拖拽手势和平共存,系统会自动根据触摸点数区分操作类型
  • 复用现有状态变量(lastScale/lastRotation),保证拖拽调整箭头与双指操作的状态一致性

完整修改代码

以下是集成双指手势后的完整代码,关键修改已标注:

struct SizePreferenceKey: PreferenceKey {
    static var defaultValue: CGSize = .zero
    static func reduce(value: inout CGSize, nextValue: () -> CGSize) {
        value = nextValue()
    }
}

struct ResizableTextView: View {
    let text: String
    let isEdittable: Bool
    
    /// Called when the edit (pen) button is tapped.
    var onEdit: (() -> Void)?
    
    /// Called when the close (delete) button is tapped.
    var onDelete: (() -> Void)?
    
    var onTap: (() -> Void)?
    
    // MARK: - Transformation State Variables
    @State private var scale: CGFloat = 1.0
    @State private var rotation: Angle = .zero

    // "Committed" transformation values from previous gestures.
    @State private var lastScale: CGFloat = 1.0
    @State private var lastRotation: Angle = .zero

    // For tracking the resize gesture’s starting values.
    @State private var initialDistance: CGFloat = 0
    @State private var initialAngle: Angle = .zero
    @State private var isDragging: Bool = false

    // The intrinsic size of the text (including padding), measured before any transforms.
    @State private var textSize: CGSize = .zero

    // MARK: - Position (Drag-to-move) State Variables
    @State private var positionOffset: CGSize = .zero
    @State private var lastPositionOffset: CGSize = .zero

    var body: some View {
        GeometryReader { geometry in
            // The container's center is used as the anchor point for the text.
            let containerCenter = CGPoint(x: geometry.size.width / 2,
                                          y: geometry.size.height / 2)
            
            ZStack {
                // Main text view with measured size.
                Text(text)
                    .padding(20)
                    .background(
                        GeometryReader { proxy in
                            Color.clear
                                .preference(key: SizePreferenceKey.self, value: proxy.size)
                        }
                    )
                    .onPreferenceChange(SizePreferenceKey.self) { newSize in
                        self.textSize = newSize
                    }
                    // Apply the current transformations.
                    .scaleEffect(scale)
                    .rotationEffect(rotation)
                    // Draw a border around the text.
                    .overlay(
                        Rectangle()
                            .stroke(Color.gray, lineWidth: isEdittable ? 1 : 0)
                            .scaleEffect(scale)
                            .rotationEffect(rotation)
                    )
                    // Position the text at the container center.
                    .position(containerCenter)
                
                // Calculate half of the scaled text dimensions.
                let halfWidth = (textSize.width * scale) / 2
                let halfHeight = (textSize.height * scale) / 2
                let angleRad = rotation.radians
                
                // MARK: - Compute Control Positions
                
                // Bottom-right (Resize control):
                let arrowOffsetX = halfWidth * CGFloat(cos(angleRad)) - halfHeight * CGFloat(sin(angleRad))
                let arrowOffsetY = halfWidth * CGFloat(sin(angleRad)) + halfHeight * CGFloat(cos(angleRad))
                let arrowPosition = CGPoint(x: containerCenter.x + arrowOffsetX,
                                            y: containerCenter.y + arrowOffsetY)
                
                // Top-right (Edit/Pen control):
                let penOffsetX = halfWidth * CGFloat(cos(angleRad)) + halfHeight * CGFloat(sin(angleRad))
                let penOffsetY = halfWidth * CGFloat(sin(angleRad)) - halfHeight * CGFloat(cos(angleRad))
                let penPosition = CGPoint(x: containerCenter.x + penOffsetX,
                                          y: containerCenter.y + penOffsetY)
                
                // Top-left (Close/Delete control):
                let closeOffsetX = -halfWidth * CGFloat(cos(angleRad)) + halfHeight * CGFloat(sin(angleRad))
                let closeOffsetY = -halfWidth * CGFloat(sin(angleRad)) - halfHeight * CGFloat(cos(angleRad))
                let closePosition = CGPoint(x: containerCenter.x + closeOffsetX,
                                            y: containerCenter.y + closeOffsetY)
                
                // MARK: - Add Controls Using Helper Functions
                if isEdittable {
                    closeButton(at: closePosition)
                    editButton(at: penPosition)
                    resizeButton(at: arrowPosition, containerCenter: containerCenter)
                }
            }
            // Apply the offset to the entire structure.
            .offset(x: positionOffset.width, y: positionOffset.height)
            // Attach a drag gesture to reposition the whole structure.
            .gesture(
                DragGesture()
                    .onChanged { value in
                        positionOffset = CGSize(width: lastPositionOffset.width + value.translation.width,
                                                height: lastPositionOffset.height + value.translation.height)
                    }
                    .onEnded { _ in
                        lastPositionOffset = positionOffset
                    }
            )
            // MARK: - 新增双指缩放旋转复合手势
            .simultaneousGesture(
                MagnificationGesture()
                    .simultaneously(with: RotationGesture())
                    .onChanged { magValue, rotValue in
                        // 处理缩放逻辑,复用原有边界限制
                        let scaleFactor = magValue.magnitude
                        var newScale = lastScale * scaleFactor
                        newScale = min(max(newScale, 0.1), 5.0)
                        scale = newScale
                        
                        // 处理旋转逻辑
                        let angleDelta = rotValue.rotation
                        rotation = lastRotation + angleDelta
                    }
                    .onEnded { _ in
                        // 提交状态,与拖拽调整逻辑保持一致
                        lastScale = scale
                        lastRotation = rotation
                    }
            )
            .onTapGesture {
                onTap?()
            }
            // Ensure the ZStack fills the container.
            .frame(width: geometry.size.width, height: geometry.size.height)
        }
        // Set an arbitrary height for the view (adjust as needed).
        .frame(height: 300)
    }
    
    // MARK: - Helper Functions for Control Buttons
    
    private func closeButton(at position: CGPoint) -> some View {
        Image("iconClose")
            .resizable()
            .scaledToFit()
            .frame(width: 16, height: 16)
            .foregroundStyle(.black)
            .rotationEffect(.zero)
            .padding(4)
            .background(
                Circle()
                    .fill(Color.white)
                    .frame(width: 28, height: 28)
                    .shadow(radius: 5)
            )
            .position(position)
            .onTapGesture {
                onDelete?()
            }
    }
    
    private func editButton(at position: CGPoint) -> some View {
        Image("iconPen")
            .resizable()
            .scaledToFit()
            .frame(width: 16, height: 16)
            .foregroundStyle(.black)
            .rotationEffect(.zero)
            .padding(4)
            .background(
                Circle()
                    .fill(Color.white)
                    .frame(width: 28, height: 28)
                    .shadow(radius: 5)
            )
            .position(position)
            .onTapGesture {
                onEdit?()
            }
    }
    
    private func resizeButton(at position: CGPoint, containerCenter: CGPoint) -> some View {
        Image("iconResize")
            .resizable()
            .scaledToFit()
            .frame(width: 16, height: 16)
            .foregroundStyle(.black)
            .rotationEffect(Angle(degrees: 80))
            .padding(4)
            .background(
                Circle()
                    .fill(Color.white)
                    .frame(width: 28, height: 28)
                    .shadow(radius: 5)
            )
            .position(position)
            .gesture(
                DragGesture()
                    .onChanged { value in
                        let currentLocation = value.location
                        // Compute the vector from the container's center to the current drag location.
                        let dx = currentLocation.x - containerCenter.x
                        let dy = currentLocation.y - containerCenter.y
                        let currentDistance = sqrt(dx * dx + dy * dy)
                        let currentAngle = Angle(radians: atan2(dy, dx))
                        
                        if !isDragging {
                            isDragging = true
                            initialDistance = currentDistance
                            initialAngle = currentAngle
                        } else {
                            // Adjust scale based on the change in distance.
                            let scaleFactor = currentDistance / initialDistance
                            var newScale = lastScale * scaleFactor
                            newScale = min(max(newScale, 0.1), 5.0)
                            scale = newScale
                            
                            // Adjust rotation based on the change in angle.
                            let angleDelta = currentAngle - initialAngle
                            rotation = lastRotation + angleDelta
                        }
                    }
                    .onEnded { _ in
                        isDragging = false
                        lastScale = scale
                        lastRotation = rotation
                    }
            )
    }
}

关键说明

  • 手势冲突解决:通过simultaneousGesture绑定复合手势,系统会自动识别单指/双指操作,单指触发移动拖拽,双指触发缩放旋转,两者互不干扰
  • 状态同步:双指操作的状态更新逻辑完全复用原有拖拽调整的lastScale和lastRotation变量,确保两种操作方式的状态无缝衔接
  • 行为一致性:保留了原有缩放的边界限制(0.1到5.0),保证双指缩放和拖拽调整的行为统一
  • 复合手势联动:使用simultaneously(with:)将缩放与旋转手势绑定,双指操作时两者同步响应,符合用户直觉

内容的提问来源于stack exchange,提问作者Codebane the Swiftbreaker

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.14 13:47:03