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))
}
}
2条答案
按热度按时间d4so4syb1#
我建议你用
DragGesture
来记录点,用Canvas
来画线。您描述的路径上的点与捕获它们的位置不对应的问题是使用不同坐标空间的症状。您需要在捕获点的同一坐标空间中绘制线。例如:
DragGesture
连接到Image
,并在Image
上绘制重叠线DragGesture
连接到包含Image
的ZStack
,并在ZStack
内部的层中绘制线。下面是一个简单的实现,它在连续拖动手势的端点之间绘制直线。它使用了上述两种方法中的第二种,使用
ZStack
:字符串
的数据
这一解决办法可以以各种方式加以阐述,例如:
GeometryReader
获取拖动区域的大小,并将这些点转换为相对UnitPoint
位置,以便它们可以用于在已调整大小的图像上绘制相同的线nhaq1z212#
的数据
下面的代码让我们在图像的顶部绘制一条直线路径,如上图所示。直线的点被归一化到图像,这样即使在图像大小发生变化(例如设备旋转后可能发生的变化)后,也可以正确地重新绘制直线。
涉及的技巧包括在覆盖层中绘制线条。代码还显示了以像素为单位的图像大小和以点为单位的图像视图大小。这些大小在不同视图之间转换时很有用。
字符串