ios 如何在SwiftUI中的图像上绘制线条

oknwwptz  于 2023-11-19  发布在  iOS
关注(0)|答案(2)|浏览(179)

我想使用SwiftUI在图像上交互式地画一条线。
我遇到了几个问题。我开始在图像上使用DragGesture。然而,图像上的点偏离了Path想要绘制的位置。这主要是通过在覆盖层中绘制来解决的-这将与图像的大小和位置相同。
下面的解决方案还以像素和屏幕单位获取图像大小,以帮助进行坐标转换。

d4so4syb

d4so4syb1#

我建议你用DragGesture来记录点,用Canvas来画线。
您描述的路径上的点与捕获它们的位置不对应的问题是使用不同坐标空间的症状。您需要在捕获点的同一坐标空间中绘制线。例如:

  • DragGesture连接到Image,并在Image上绘制重叠线
  • 或者,将DragGesture连接到包含ImageZStack,并在ZStack内部的层中绘制线。

下面是一个简单的实现,它在连续拖动手势的端点之间绘制直线。它使用了上述两种方法中的第二种,使用ZStack

struct ContentView: View {

    @State private var points = [CGPoint]()
    @State private var startOfDrag = CGPoint.zero

    var body: some View {
        ZStack {
            Image(systemName: "ladybug")
                .resizable()
                .scaledToFit()
            if points.count > 1 {
                Canvas { ctx, size in
                    var path = Path()
                    path.addLines(points)
                    ctx.stroke(path, with: .color(.red), lineWidth: 5)
                }
            }
        }
        .gesture(
            DragGesture(minimumDistance: 0)
                .onChanged { val in
                    if points.isEmpty || startOfDrag != val.startLocation {
                        startOfDrag = val.startLocation
                        points.append(startOfDrag)
                    } else if val.translation != .zero {
                        if points.count > 1 {
                            points.removeLast()
                        }
                        points.append(val.location)
                    }
                }
        )
        .frame(width: 300, height: 300)
    }
}

字符串


的数据
这一解决办法可以以各种方式加以阐述,例如:

  • 收集所有的点从一个拖动运动,以建立一个更详细的路径(见Create a pencil effect for drawing on SwiftUi
  • 使用GeometryReader获取拖动区域的大小,并将这些点转换为相对UnitPoint位置,以便它们可以用于在已调整大小的图像上绘制相同的线
  • 当最后一个点靠近第一个点时,允许路径闭合
  • 提供一个撤消按钮,允许删除路径的最后一行。
nhaq1z21

nhaq1z212#


的数据
下面的代码让我们在图像的顶部绘制一条直线路径,如上图所示。直线的点被归一化到图像,这样即使在图像大小发生变化(例如设备旋转后可能发生的变化)后,也可以正确地重新绘制直线。
涉及的技巧包括在覆盖层中绘制线条。代码还显示了以像素为单位的图像大小和以点为单位的图像视图大小。这些大小在不同视图之间转换时很有用。

import SwiftUI

struct ContentView: View {
    @State private var line: [CGPoint] = []     // Line is an array of points

    @State private var pixelSize = PixelSize()
    @State private var viewSize = CGSize.zero

    @State private var startNewSegment = true
    @State private var pixelPath = ""

    var body: some View {
        VStack {
            lineView() // Magic happens here
            buttons()
            info()
        }
        .padding()
        .onAppear {
            // Comment out next line when no longer needed for debugging
            line = [CGPoint(x: 0, y: 0), CGPoint(x: 0.25, y: 0.25), CGPoint(x: 0.5, y: 0)]
        }
    }

    @ViewBuilder
    func lineView() -> some View {
        let largeConfig = UIImage.SymbolConfiguration(pointSize: 100, weight: .bold, scale: .large)
        let image = UIImage(systemName: "bolt.square", withConfiguration: largeConfig)! // Your image here

        Image(uiImage: image)
            .resizable()
            .aspectRatio(contentMode: .fit)
            .saveSize(in: $viewSize) // NOTE: this is the screen size for the image!
            .overlay { // Draw line in overlay.  You really want to do this.
                if line.count > 1 {
                    // Replace "Path" with "Canvas"
                    Canvas { ctx, size in
                        var path = Path()
                        path.addLines(line.map { screenPoint($0)})
                        ctx.stroke(path, with: .color(.blue), lineWidth: 5)
                    }

                    // Path { path in
                    //     path.move(to: screenPoint(line[0]))
                    //     for i in 1..<line.count {
                    //         path.addLine(to: screenPoint(line[i]))
                    //     }
                    // }
                    // .stroke(.blue, lineWidth: 5)
                }
            }
            .onAppear {
                pixelSize = image.pixelSize
            }
        // Build up line by adding points in straight line segments.
        // Allow point to be added by tap
            .onTapGesture { location in
                line.append(limitPoint(location))
            }
        // Or allow new point to be added from drag
            .gesture(
                DragGesture()
                    .onChanged { value in
                        if line.count < 1 {
                            // If no points, add "startLocation" point (more accurate than simply location for first point)
                            line.append(limitPoint(value.startLocation))
                        } else if line.count < 2 || startNewSegment {
                            // Add point at current position
                            line.append(limitPoint(value.location))
                            startNewSegment = false
                        } else {
                            // Note: Now in mode where we are replacing the last point
                            line.removeLast()
                            line.append(limitPoint(value.location))
                            startNewSegment = false
                        }
                    }
                    .onEnded { value in
                        line.removeLast()
                        line.append(limitPoint(value.location))
                        startNewSegment = true
                    }
            )
    }

    func screenPoint(_ point: CGPoint) -> CGPoint {
        // Convert 0->1 to view's coordinates
        let vw = viewSize.width
        let vh = viewSize.height
        let nextX = min(1, max(0, point.x)) * vw
        let nextY = min(1, max(0, point.y)) * vh
        return CGPoint(x: nextX, y: nextY)
    }

    func limitPoint(_ point: CGPoint) -> CGPoint {
        // Convert view coordinate to normalized 0->1 range
        let vw = max(viewSize.width, 1)
        let vh = max(viewSize.height, 1)

        // Keep in bounds - even if dragging outside of bounds
        let nextX = min(1, max(0, point.x / vw))
        let nextY = min(1, max(0, point.y / vh))
        return CGPoint(x: nextX, y: nextY)
    }

    @ViewBuilder
    func buttons() -> some View {
        HStack {
            Button {
                line.removeAll()
                pixelPath = ""
            } label: {
                Text("Clear")
                    .padding()
            }
            Button {
                // Show line points in "Pixel" units
                let vw = viewSize.width
                let vh = viewSize.height
                if vw > 0 && vh > 0 {
                    let pixelWidth = CGFloat(pixelSize.width - 1)
                    let pixelHeight = CGFloat(pixelSize.height - 1)
                    let pixelPoints = line.map { CGPoint(x: pixelWidth * $0.x, y: pixelHeight * $0.y)}
                    pixelPath = "\(pixelPoints)"
                }
            } label: {
                Text("Pixel Path")
                    .padding()
            }
        }
    }

    @ViewBuilder
    func info() -> some View {
        Text("Image WxL: \(pixelSize.width) x \(pixelSize.height)")
        Text("View  WxL: \(viewSize.width) x \(viewSize.height)")
        Text("Line points: \(line.count)")
        if pixelPath != "" {
            Text("Pixel Path: \(pixelPath)")
        }
    }
}

// Auxiliary definitions and functions

struct PixelSize {
    var width: Int = 0
    var height: Int = 0
}

extension UIImage {
    var pixelSize: PixelSize {
        if let cgImage = cgImage {
            return PixelSize(width: cgImage.width, height: cgImage.height)
        }
        return PixelSize()
    }
}

// SizeCalculator from: https://stackoverflow.com/questions/57577462/get-width-of-a-view-using-in-swiftui

struct SizeCalculator: ViewModifier {

    @Binding var size: CGSize

    func body(content: Content) -> some View {
        content
            .background(
                GeometryReader { proxy in
                    Color.clear // we just want the reader to get triggered, so let's use an empty color
                        .onAppear {
                            size = proxy.size
                        }
                        .onChange(of: proxy.size) { // Added to handle device rotation
                            size = proxy.size
                        }
                }
            )
    }
}

extension View {
    func saveSize(in size: Binding<CGSize>) -> some View {
        modifier(SizeCalculator(size: size))
    }
}

字符串

相关问题