我正在构建SwiftUI应用程序,在那里我需要在视图中旋转、放置签名和调整签名大小.我已经实现了最后两个手势,但当我激活最后一个手势时,它 destruct 了一切.最理想的情况是,如果黑色圆圈能够同时配置Angular 和大小,但两者开始相互冲突.在这个测试片段中,我已经 Select 了使用的代码,并删除了连接到签名后面的图像的代码,因为它并不重要.

主要问题是如何让.simultaneousGesture(applyRotation(width: width, height: height))正常工作.

struct NEwVIew: View {
    
    @State private var location: CGPoint = CGPoint(x: 150, y: 300)
    @GestureState private var fingerLocation: CGPoint? = nil
    @GestureState private var startLocation: CGPoint? = nil
    
    // Initialise to a size proportional to the screen dimensions.
    @State private var width: CGFloat = 100
    @State private var height: CGFloat = 100
    
    @State private var previousRotation: Double = 0.0
    @State private var knobRotation: Double = 0.0
    
    @State var rotationActive = false
    
    private func location2Degrees(location: CGPoint, midX: CGFloat, midY: CGFloat) -> CGFloat {
        let radians = location.y < midY
        ? atan2(location.x - midX, midY - location.y)
        : .pi - atan2(location.x - midX, location.y - midY)
        let degrees = (radians * 180 / .pi) - 135
        return degrees < 0 ? degrees + 360 : degrees
    }
    
    private func applyRotation(width: CGFloat, height: CGFloat) -> some Gesture {
        DragGesture()
            .onChanged { value in
                
                let midX = width / 2
                let midY = height / 2
                let startAngle = location2Degrees(location: value.startLocation, midX: midX, midY: midY)
                let endAngle = location2Degrees(location: value.location, midX: midX, midY: midY)
                let dAngle = endAngle - startAngle
                knobRotation = previousRotation + dAngle
                
            }
            .onEnded { value in
                previousRotation = knobRotation
            }
    }
    
    var simpleDrag: some Gesture {
        DragGesture()
            .onChanged { value in
                var newLocation = startLocation ?? location // 3
                newLocation.x += value.translation.width
                newLocation.y += value.translation.height
                self.location = newLocation
            }.updating($startLocation) { (value, startLocation, transaction) in
                startLocation = startLocation ?? location // 2
            }
    }
    
    var fingerDrag: some Gesture {
        DragGesture()
            .updating($fingerLocation) { (value, fingerLocation, transaction) in
                fingerLocation = value.location
            }
    }
    
    var body: some View {
        VStack {
            GeometryReader { geometry in
                
                ZStack {
                    
                    VStack {
                        ZStack {
                            if let image = loadImageFromDocumentDirectory(filename: "signature.png") {
                                
                                ZStack(alignment: .bottomTrailing) {
                                    
                                    Rectangle()
                                        .stroke(style: StrokeStyle(lineWidth: 1, dash: [5]))
                                        .fill(.blue)
                                        .frame(width: width, height: height)
                                    

                            // I've commented it because you don't have this image in app files.
                                    // Image(uiImage: image)
                                    // .resizable()
                                    // .scaledToFit()
                                    // .frame(width: width, height: height)
                                    // BLACK CIRCLE I WAS TALKING ABOUT
                                    Circle()
                                        .frame(width: 25, height: 25)
                                        .gesture(
                                            DragGesture()
                                                .onChanged { value in
                                                    // Enforce minimum dimensions.
                                                    DispatchQueue.main.async {
                                                        withAnimation {
                                                            
                                                            width = max(50, width + value.translation.width / 10)
                                                            height = width
                                                            
                                                        }
                                                    }
                                                }
                                        )
                                        .zIndex(1)
                                    
                                }
                                .rotationEffect(Angle(degrees: knobRotation))
                                // NEEDS TO WORK TOO AT THE SAME TIME WITH TWO FIRST!
                                //                                .simultaneousGesture(applyRotation(width: width, height: height))
                                
                            }
                            
                        }
                    }
                    .frame(maxWidth: width, maxHeight: height, alignment: .center)
                    .position(location)
                    .gesture(
                        simpleDrag.simultaneously(with: fingerDrag)
                    )
                }
                .frame(width: geometry.size.width, height: geometry.size.height)
            }
            
        }
        .background(Color.black.opacity(0.3))
        .overlay(
            VStack {
                Spacer()
                Button {
                    
                } label: {
                    Text("Save")
                }
            }
        )
        .ignoresSafeArea(.all)
    }
    
    
    func loadImageFromDocumentDirectory(filename: String) -> UIImage? {
        let documentsDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!
        let fileURL = documentsDirectory.appendingPathComponent(filename)
        
        do {
            let imageData = try Data(contentsOf: fileURL)
            return UIImage(data: imageData)
        } catch {
            print("Error loading image: \(error)")
            return nil
        }
    }
}

推荐答案

我不认为你需要三个手势.如果区分不同的起始位置和不同的拖动方向,只需一个拖动手势即可执行所有变换:

  • 如果拖动的起点在点上,且拖动的方向沿旋转圆,则进行旋转;
  • 否则,如果拖动的起点在点上,并且拖动的方向与旋转圆相切,则执行zoom ;
  • 否则,如果拖动的起点不在点上,则执行移动.

确定开始位置和拖拽的方向只是一个三角问题!

我不想开始对您的代码进行重大更改,但我对try 这一点很感兴趣,所以我try 创建一个可工作的示例来演示这一概念.下面是一个独立的签名图像操作面板,它使用单个拖动手势来执行所有变换.旋转部分以my answer to your other post为基础,但现在更复杂,因为还需要考虑其他变换.我希望其中可能有一些有用的部分,您可以重复使用或从中吸取灵感.

struct ContentView: View {

    enum TransformationType {
        case unknown
        case move
        case rotation
        case scale
    }

    let cornerDotSize = CGFloat(28)
    let defaultSignatureWidth = CGFloat(250)
    let defaultSignatureHeight = CGFloat(125)

    @State private var transformationType = TransformationType.unknown
    @State private var offset = CGSize.zero
    @State private var previousOffset = CGSize.zero
    @State private var degrees = CGFloat.zero
    @State private var previousRotation = CGFloat.zero
    @State private var scaleFactor = 1.0

    private func reset() {
        withAnimation {
            offset = .zero
            previousOffset = .zero
            degrees = degrees > 180 ? 360 : 0
            previousRotation = .zero
            scaleFactor = 1.0
        }
    }

    struct SignatureLine : Shape {
        func path(in rect: CGRect) -> Path {
            var path = Path()
            path.move(
                to: CGPoint(
                    x: rect.width * 0.1,
                    y: rect.height * 0.75
                )
            )
            path.addLine(
                to: CGPoint(
                    x: rect.width * 0.9,
                    y: rect.height * 0.75
                )
            )
            path.closeSubpath()
            return path
        }
    }

    private func location2Degrees(origin: CGPoint, location: CGPoint) -> CGFloat {
        let radians = location.y < origin.y
            ? atan2((location.x - origin.x) * scaleFactor, (origin.y - location.y) * scaleFactor)
            : .pi - atan2((location.x - origin.x) * scaleFactor, (location.y - origin.y) * scaleFactor)
        let degrees = (radians * 180 / .pi) - 135
        return degrees < 0 ? degrees + 360 : degrees
    }

    private var midPoint: CGPoint {
        let midX = (defaultSignatureWidth / 2) + offset.width
        let midY = (defaultSignatureHeight / 2) + offset.height
        return CGPoint(x: midX, y: midY)
    }

    /// - Returns the position of the bottom-right corner in the
    /// local coordinate space, after transformation
    private var cornerPoint: CGPoint {
        let initialAngle = atan2(defaultSignatureHeight, defaultSignatureWidth)
        let latestAngle = initialAngle + (previousRotation * .pi / 180)
        let midPoint = midPoint
        let halfDiagonalLength: CGFloat = (
                (defaultSignatureWidth * defaultSignatureWidth) +
                (defaultSignatureHeight * defaultSignatureHeight)
            )
            .squareRoot() * scaleFactor / 2
        let cornerX = midPoint.x + (cos(latestAngle) * halfDiagonalLength)
        let cornerY = midPoint.y + (sin(latestAngle) * halfDiagonalLength)
        return CGPoint(x: cornerX, y: cornerY)
    }

    private func transformationTypeForDrag(startLocation: CGPoint, dragLocation: CGPoint) -> TransformationType {
        let result: TransformationType

        // See if the start location is inside the dot
        let cornerPoint = cornerPoint
        let dx = cornerPoint.x - startLocation.x
        let dy = cornerPoint.y - startLocation.y
        let distance = ((dx * dx) + (dy * dy)).squareRoot()
        if distance <= (cornerDotSize / 2) {

            // The dot is being dragged. Calculate the difference in angles
            // between the middle point and the drag position w.r.t. the corner
            let midAngle = location2Degrees(origin: cornerPoint, location: midPoint)
            let dragAngle = location2Degrees(origin: cornerPoint, location: dragLocation)
            let dAngle = abs(midAngle - dragAngle)

            // Determine the transformation according to the angle.
            // If the angle is acute then scale, otherwise rotate
            result = (dAngle > 315 || dAngle < 45) || (dAngle > 135 && dAngle < 225)
                ? .scale
                : .rotation
        } else {

            // The start of drag is not on the dot
            result = .move
        }
        return result
    }

    private func performMove(dragTranslation: CGSize) {
        let width = previousOffset.width + dragTranslation.width
        let height = previousOffset.height + dragTranslation.height
        offset = CGSize(width: width, height: height)
    }

    private func performRotation(startLocation: CGPoint, dragLocation: CGPoint) {
        let midPoint = midPoint
        let startAngle = location2Degrees(origin: midPoint, location: startLocation)
        let endAngle = location2Degrees(origin: midPoint, location: dragLocation)
        let dAngle = endAngle - startAngle
        let combinedAngle = (previousRotation + dAngle).truncatingRemainder(dividingBy: 360)
        degrees = combinedAngle < 0 ? combinedAngle + 360 : combinedAngle
    }

    private func performScale(dragLocation: CGPoint) {
        let midPoint = midPoint
        let dX = dragLocation.x - midPoint.x
        let dY = dragLocation.y - midPoint.y
        let draggedDiagonalLength = 2 * ((dX * dX) + (dY * dY)).squareRoot()
        let unscaledDiagonalLength = (
                (defaultSignatureWidth * defaultSignatureWidth) +
                (defaultSignatureHeight * defaultSignatureHeight)
            ).squareRoot()
        let draggedScaleFactor = draggedDiagonalLength / unscaledDiagonalLength
        scaleFactor = min(1.5, max(0.5, draggedScaleFactor))
    }

    private var applyTransformation: some Gesture {
        DragGesture()
            .onChanged { value in
                if transformationType == .unknown {

                    // Determine the transformation type on first call
                    transformationType = transformationTypeForDrag(
                        startLocation: value.startLocation,
                        dragLocation: value.location
                    )
                }
                if transformationType == .move {
                    performMove(dragTranslation: value.translation)
                } else if transformationType == .rotation {
                    performRotation(startLocation: value.startLocation, dragLocation: value.location)
                } else {
                    performScale(dragLocation: value.location)
                }
            }
            .onEnded { value in
                previousRotation = degrees
                previousOffset = offset
                transformationType = .unknown
            }
    }

    @ViewBuilder
    private var backgroundDuringTransformation: some View {
        if transformationType != .unknown {
            Color.accentColor.opacity(0.1)
        }
    }

    private var signatureImage: some View {
        Image(systemName: "scribble")
            .resizable()
            .scaledToFit()
            .frame(width: defaultSignatureWidth, height: defaultSignatureHeight)
            .contentShape(Rectangle())
            .background {
                Rectangle()
                    .stroke(style: StrokeStyle(lineWidth: 1, dash: [2, 5]))
                    .foregroundColor(Color(UIColor.secondaryLabel))
            }
            .background {
                backgroundDuringTransformation
                    .animation(.easeInOut(duration: 0.2), value: transformationType)
            }
            .scaleEffect(scaleFactor)
            .overlay {

                // The dot in the bottom-right corner.
                // The overlay is applied after the scaleFactor so that
                // the dot does not get scaled, but before the rotationEffect
                // and offset modifiers so that it undergoes the same
                // rotation and shift transformations
                Circle()
                    .foregroundColor(.accentColor)
                    .frame(width: cornerDotSize, height: cornerDotSize)
                    .offset(
                        x: (defaultSignatureWidth / 2) * scaleFactor,
                        y: (defaultSignatureHeight / 2) * scaleFactor
                    )
            }
            .rotationEffect(.degrees(degrees))
            .offset(offset)
            .gesture(applyTransformation)
    }

    private var hasTransformation: Bool {
        (degrees != .zero && degrees != 360) || offset != .zero || scaleFactor != 1.0
    }

    private var resetButton: some View {
        Image(systemName: "dot.squareshape.split.2x2")
            .resizable()
            .scaledToFit()
            .foregroundColor(Color(UIColor.secondaryLabel))
            .padding(12)
            .frame(width: 44, height: 44)
            .contentShape(Rectangle())
            .accessibilityAddTraits(.isButton)
            .onTapGesture(perform: reset)
            .opacity(hasTransformation ? 1 : 0)
            .animation(.easeInOut, value: hasTransformation)
    }

    var body: some View {
        VStack(spacing: 20) {

            // The signature area
            ZStack {
                SignatureLine()
                    .stroke(style: StrokeStyle(lineWidth: 1.5, dash: [7]))
                    .foregroundColor(Color(UIColor.secondaryLabel))
                signatureImage
            }
            .overlay(alignment: .topTrailing) { resetButton }
            .frame(maxWidth: .infinity)
            .frame(height: 230)
            .clipped()
            .background(Color(UIColor.systemBackground))

            // Display the adjustments
            VStack(alignment: .leading, spacing: 10) {
                HStack {
                    Text("Offset").frame(width: 110, alignment: .leading)
                    Text("Rotation").frame(width: 110, alignment: .leading)
                    Text("Scaling").frame(width: 80, alignment: .leading)
                }
                .bold()
                HStack(alignment: .top) {
                    Text("x: \(offset.width)\ny: \(offset.height)").frame(width: 110, alignment: .leading)
                    Text("\(degrees)°").frame(width: 110, alignment: .leading)
                    Text("\(scaleFactor)").frame(width: 80, alignment: .leading)
                }
            }
            .font(.subheadline)
            .padding(.top, 20)

            Spacer()
        }
        .padding(20)
        .padding(.top, 100)
        .frame(maxWidth: .infinity, maxHeight: .infinity)
        .background(Color(UIColor.systemFill))
    }
}

Signature

Ios相关问答推荐

如果没有等待,SWIFT Async让任务抛出取消异常

flutter中实现自定义底部导航栏

如何为 Firebase Auth 实施 Firebase AppCheck?

Flutter iOS 键盘问题:. TextInputType.number 中缺少字符

如何在Combine发布者之间强制执行最小延迟

React Native IOS base64 编码图像不显示

如何根据条件快速将返回类型设为 LinearGradient 或 AngularGradient?

滚动 swiftUI 列表时,未调用单元格的任务修饰符.怎么修?

过渡动画在 iOS16 中不起作用,但在 iOS15 中起作用

在 Swift 中映射 MySQL 数据

一个 SwiftUI 视图中的多个垂直滚动视图

swiftUI中的剪辑形状无法使用旋转效果

通过MatchedGeometryEffect向上滑动视图

在 UICollectionView 上从右到左对齐

动画 UILabel 字体大小更改

iOS 10 错误:UICollectionView 接收到具有不存在索引路径的单元格的布局属性

有没有办法在 xib 文件中的视图和顶部布局指南之间添加约束?

在 iPhone 中将 UIViewController 显示为弹出窗口

如何在键盘上方添加工具栏?

有什么方法可以加粗 NSString 的一部分?