import SwiftUI import Photos import PhotosUI // MARK: - 背景图片布局偏好键 struct BackgroundImageFramePreferenceKey: PreferenceKey { static var defaultValue: CGRect = .zero static func reduce(value: inout CGRect, nextValue: () -> CGRect) { value = nextValue() } } struct ImageComposerView: View { @EnvironmentObject var languageManager: LanguageManager let qrCodeImage: UIImage let backgroundImage: UIImage @Environment(\.dismiss) private var dismiss @State private var qrCodePosition = CGPoint.zero // 将在 onAppear 中设置为图片中心 @State private var qrCodeScale: CGFloat = 1.0 @State private var qrCodeRotation: Double = 0.0 @State private var qrCodeRotationLast: Double = 0.0 @State private var isDragging = false @State private var isScaling = false @State private var isRotating = false @State private var showingSaveSheet = false @State private var composedImage: UIImage? @State private var isComposing = false @State private var isSelected = true // 二维码选择状态 @State private var actualEditingAreaSize: CGSize = .zero // 实际的编辑区域大小 @GestureState private var fingerLocation: CGPoint? = nil @GestureState private var startLocation: CGPoint? = nil // 手势状态 @State private var dragOffset = CGSize.zero @State private var scaleOffset: CGFloat = 1.0 @State private var rotationOffset: Double = 0.0 var simpleDrag: some Gesture { DragGesture() .onChanged { value in var newLocation = startLocation ?? qrCodePosition newLocation.x += value.translation.width newLocation.y += value.translation.height // 约束位置到背景图边界 qrCodePosition = constrainPositionToImage(newLocation) } .updating($startLocation) { (value, startLocation, _) in startLocation = startLocation ?? qrCodePosition } } var fingerDrag: some Gesture { DragGesture() .updating($fingerLocation) { (value, fingerLocation, _) in fingerLocation = value.location } } //Editor State enum EditorState { case idle case dragging case scaling case rotating } @State private var editorState: EditorState = .idle // 防卡死状态管理 @State private var lastGestureTime = Date() var body: some View { NavigationView { editingArea .navigationTitle("add_to_picture_title".localized) .navigationBarTitleDisplayMode(.inline) .toolbar { ToolbarItem(placement: .navigationBarLeading) { Button(action: { dismiss() }) { Image(systemName: "chevron.left") } } ToolbarItem(placement: .navigationBarTrailing) { Button("save".localized) { composeAndSave() } .padding(.horizontal, 16) .padding(.vertical, 8) .cornerRadius(8) .font(.system(size: 16, weight: .semibold)) } } } .sheet(isPresented: $showingSaveSheet) { if let composedImage = composedImage { ShareSheet(activityItems: [composedImage]) } } .onAppear { // 设置二维码初始位置为图片中心并应用约束 let imageSize = getImageDisplaySize() let centerPosition = CGPoint(x: imageSize.width / 2, y: imageSize.height / 2) qrCodePosition = constrainPositionToImage(centerPosition) // 启动轻量级防卡死检查 startLightweightAntiStuckCheck() } } // MARK: - 编辑区域 private var editingArea: some View { GeometryReader { geometry in ZStack { // 背景图片 Image(uiImage: backgroundImage) .resizable() .aspectRatio(contentMode: .fit) .clipped() // 二维码图层 qrCodeLayer // 重置按钮 VStack { Spacer() HStack { Spacer() resetButton .padding(.trailing, 20) .padding(.bottom, 20) } } } .background(Color(.systemGray6)) .onAppear { // 保存实际的编辑区域大小 DispatchQueue.main.async { self.actualEditingAreaSize = geometry.size } } .onChange(of: geometry.size) { newSize in // 当编辑区域大小改变时更新 DispatchQueue.main.async { self.actualEditingAreaSize = newSize } } .clipped() } } // MARK: - 重置按钮 private var resetButton: some View { Button(action: resetQRCode) { Image(systemName: "arrow.clockwise") .font(.system(size: 18, weight: .medium)) .foregroundColor(.white) .padding(12) .background(Color.black.opacity(0.6)) .clipShape(Circle()) .shadow(color: .black.opacity(0.3), radius: 4, x: 0, y: 2) } } // MARK: - 二维码图层 private var qrCodeLayer: some View { ZStack { ZStack { // 选择框边框 RoundedRectangle(cornerRadius: 4) .stroke(Color.teal, lineWidth: 2) .frame(width: 140 * qrCodeScale, height: 140 * qrCodeScale) // 二维码图片 Image(uiImage: qrCodeImage) .resizable() .aspectRatio(contentMode: .fit) .frame(width: 100 * qrCodeScale, height: 100 * qrCodeScale) // 操作图标 operationIcon } .rotationEffect(.degrees(qrCodeRotation)) .position(qrCodePosition) .gesture(simpleDrag.simultaneously(with: fingerDrag)) if let fingerLocation = fingerLocation { Circle() .stroke(Color.teal, lineWidth: 2) .frame(width: 20, height: 20) .position(fingerLocation) } } } // MARK: - 操作图标 private var operationIcon: some View { Image(systemName: "arrow.up.left.and.arrow.down.right") .font(.system(size: 16, weight: .medium)) .foregroundColor(.white) .padding(6) .background(Color.teal) .clipShape(Circle()) .scaleEffect(qrCodeScale) .shadow(color: .black.opacity(0.2), radius: 2, x: 0, y: 1) .offset(x: 68 * qrCodeScale, y: 68 * qrCodeScale) .gesture( DragGesture() .onChanged { value in // 更新最后手势时间,防止卡死 lastGestureTime = Date() // 使用简单的水平移动计算旋转 let rotationSensitivity: Double = 0.8 let rotationDelta = Double(value.translation.width) * rotationSensitivity // 累积旋转角度 qrCodeRotation = qrCodeRotationLast + rotationDelta // 限制旋转角度在合理范围内 if qrCodeRotation > 360 { qrCodeRotation -= 360 } else if qrCodeRotation < -360 { qrCodeRotation += 360 } } .onEnded { _ in // 保存当前旋转角度作为下次的起始角度 qrCodeRotationLast = qrCodeRotation // 重置状态,防止卡死 DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { if Date().timeIntervalSince(lastGestureTime) > 0.5 { // 如果超过0.5秒没有新的手势,重置状态 } } } ) } // MARK: - 约束位置到图片边界 private func constrainPositionToImage(_ position: CGPoint) -> CGPoint { let qrCodeSize = CGSize(width: 100 * qrCodeScale, height: 100 * qrCodeScale) let halfWidth = qrCodeSize.width / 2 let halfHeight = qrCodeSize.height / 2 // 使用实际的编辑区域大小作为边界,而不是计算的图片显示尺寸 let boundarySize = actualEditingAreaSize != .zero ? actualEditingAreaSize : UIScreen.main.bounds.size // 计算边界 - 确保二维码不会超出编辑区域 let minX = halfWidth let maxX = boundarySize.width - halfWidth let minY = halfHeight let maxY = boundarySize.height - halfHeight // 约束位置 let constrainedX = max(minX, min(maxX, position.x)) let constrainedY = max(minY, min(maxY, position.y)) return CGPoint(x: constrainedX, y: constrainedY) } // MARK: - 获取图片显示尺寸 private func getImageDisplaySize() -> CGSize { // 使用实际的编辑区域大小,如果还没有获取到则使用屏幕大小作为后备 let availableSize = actualEditingAreaSize != .zero ? actualEditingAreaSize : UIScreen.main.bounds.size let imageAspectRatio = backgroundImage.size.width / backgroundImage.size.height let availableAspectRatio = availableSize.width / availableSize.height var displaySize: CGSize if imageAspectRatio > availableAspectRatio { // 图片更宽,以宽度为准 displaySize = CGSize( width: availableSize.width, height: availableSize.width / imageAspectRatio ) } else { // 图片更高,以高度为准 displaySize = CGSize( width: availableSize.height * imageAspectRatio, height: availableSize.height ) } return displaySize } // MARK: - 操作手势处理 private func handleOperationGesture(_ value: DragGesture.Value) { let translation = value.translation // 设置最小移动阈值,避免微小移动触发操作 let minMovementThreshold: CGFloat = 3.0 if abs(translation.width) < minMovementThreshold && abs(translation.height) < minMovementThreshold { return } // 判断主要操作方向 if abs(translation.width) > abs(translation.height) { // 水平移动 - 旋转操作 isRotating = true isScaling = false // 使用基于中心点的角度计算算法 let center = CGPoint(x: 100 * qrCodeScale / 2, y: 100 * qrCodeScale / 2) let currentTouch = CGPoint(x: translation.width, y: translation.height) let previousTouch = CGPoint.zero // 对于实时旋转,前一个位置为原点 // 计算相对于中心点的角度 let angle1 = atan2(previousTouch.y - center.y, previousTouch.x - center.x) let angle2 = atan2(currentTouch.y - center.y, currentTouch.x - center.x) let angleDelta = (angle2 - angle1) * 180 / .pi // 转换为角度 qrCodeRotation += angleDelta // 保持角度在 0-360 范围内 qrCodeRotation = qrCodeRotation.truncatingRemainder(dividingBy: 360) // 转换为 -180 到 180 范围 if qrCodeRotation > 180 { qrCodeRotation -= 360 } else if qrCodeRotation < -180 { qrCodeRotation += 360 } } else { // 垂直移动 - 缩放操作 isScaling = true isRotating = false // 根据x、y的直接移动距离计算缩放 let scaleSensitivity: CGFloat = 0.005 // 缩放灵敏度 let distance = sqrt(translation.width * translation.width + translation.height * translation.height) // 根据移动方向确定缩放方向 // 向上移动缩小,向下移动放大 let scaleDirection = translation.height > 0 ? 1.0 : -1.0 let scaleDelta = distance * scaleDirection * scaleSensitivity // 计算新的缩放值 let newScale = qrCodeScale - scaleDelta // 限制缩放范围在 0.3 到 2.5 之间 qrCodeScale = max(0.3, min(2.5, newScale)) } } // MARK: - 重置二维码位置 private func resetQRCode() { // 立即重置所有操作状态,防止卡死 isScaling = false isRotating = false isDragging = false withAnimation(.easeInOut(duration: 0.3)) { // 重置到图片中心并应用约束 let imageSize = getImageDisplaySize() let centerPosition = CGPoint(x: imageSize.width / 2, y: imageSize.height / 2) qrCodePosition = constrainPositionToImage(centerPosition) qrCodeScale = 1.0 qrCodeRotation = 0.0 isSelected = true // 确保重置后二维码被选中 } } // MARK: - 轻量级防卡死检查 private func startLightweightAntiStuckCheck() { Timer.scheduledTimer(withTimeInterval: 3.0, repeats: true) { _ in // 如果超过2秒没有手势操作,且状态仍然为true,则重置状态 if Date().timeIntervalSince(lastGestureTime) > 2.0 { if isScaling || isRotating || isDragging { DispatchQueue.main.async { isScaling = false isRotating = false isDragging = false } } } } } // MARK: - 合成并保存 private func composeAndSave() { isComposing = true DispatchQueue.global(qos: .userInitiated).async { let composedImage = composeImages(background: backgroundImage, qrCode: qrCodeImage) DispatchQueue.main.async { self.composedImage = composedImage self.isComposing = false self.showingSaveSheet = true } } } // MARK: - 图片合成 private func composeImages(background: UIImage, qrCode: UIImage) -> UIImage { let renderer = UIGraphicsImageRenderer(size: background.size) return renderer.image { context in // 绘制背景图片 background.draw(in: CGRect(origin: .zero, size: background.size)) // 计算二维码在背景图片中的实际位置和大小 let qrCodeSize = CGSize(width: 100 * qrCodeScale, height: 100 * qrCodeScale) // 将屏幕坐标转换为图片坐标 let imageRect = CGRect(origin: .zero, size: background.size) let screenRect = UIScreen.main.bounds // 计算缩放比例 let scaleX = background.size.width / screenRect.width let scaleY = background.size.height / screenRect.height let scale = min(scaleX, scaleY) // 计算二维码在图片中的位置 let qrCodeX = qrCodePosition.x * scale let qrCodeY = qrCodePosition.y * scale let qrCodeWidth = qrCodeSize.width * scale let qrCodeHeight = qrCodeSize.height * scale // 创建二维码绘制区域 let qrCodeRect = CGRect( x: qrCodeX - qrCodeWidth / 2, y: qrCodeY - qrCodeHeight / 2, width: qrCodeWidth, height: qrCodeHeight ) // 保存当前图形状态 context.cgContext.saveGState() // 设置旋转 context.cgContext.translateBy(x: qrCodeX, y: qrCodeY) context.cgContext.rotate(by: CGFloat(qrCodeRotation * .pi / 180)) context.cgContext.translateBy(x: -qrCodeX, y: -qrCodeY) // 绘制二维码 qrCode.draw(in: qrCodeRect) // 恢复图形状态 context.cgContext.restoreGState() } } } #Preview { let sampleQRCode = UIImage(systemName: "qrcode") ?? UIImage() let sampleBackground = UIImage(systemName: "photo") ?? UIImage() return ImageComposerView(qrCodeImage: sampleQRCode, backgroundImage: sampleBackground) .environmentObject(LanguageManager.shared) }