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
相关产品推荐
相关产品推荐

