【问题标题】:SwiftUI - Drawing (curved) paths between viewsSwiftUI - 在视图之间绘制(弯曲)路径
【发布时间】:2021-10-17 21:55:13
【问题描述】:

简单来说,在我的 Android 应用中,我有一个屏幕,它可以绘制圆圈,然后用曲线将它们连接起来。

我正在尝试在 SwiftUI 中重新创建它。

我发现这个问题似乎与我正在寻找的问题非常相似,但不幸的是答案非常简短,即使在阅读了大约 10 个不同的博客和 5 个视频之后,我仍然没有完全理解它。
@987654322 @


所以基本逻辑是我以某种方式使用GeometryReader 来获取我创建的每个Circle.midX.midY 坐标,然后在它们之间绘制Paths我唯一的问题是在创建圆后获取这些坐标。

我如何将路径添加到屏幕,在 ZStack 前面,Circles 在后面,路径作为一个自定义形状?


更多信息:
在 Android 上,最终结果如下所示:

基本上,我有一个 Challenge 对象,它有一个名称和一些详细文本,我将它们布置成这样,因此它在视觉上代表了用户的“旅程”。

所以我真正需要的是知道如何布置一些圆圈/图像(上面有文字),然后画线连接它们。每个这样的挑战圈都需要可点击才能打开详细视图。

【问题讨论】:

  • 能否提供圈子布局的代码?
  • @George 我自己还不确定最好的方法是什么。如果我只有圆圈,我会将每个圆圈放在 HStack 中,然后将它们全部放在 VStack 中,但我认为这会使画线变得不可能。
  • @George 我认为唯一现实的方法是将它们全部绘制在一个 GeometryReader 块中,并根据屏幕宽度百分比放置它们(我在 Android 上就是这样做的)。
  • 是否可以共享一个最小的有效载荷,作为绘制圆(和/或线)的模型?
  • @Alladinian 对不起,我不太明白,你是要我提供更多关于我打算如何布局的信息吗?或者这是一个建议?在这种情况下,我对如何最有效地实施这一点完全持开放态度。最终的结果基本上是带有文字的圆圈,并且它们可以点击进入详细页面。

标签: ios swift swiftui line geometryreader


【解决方案1】:

GeometryReader 为您提供容器视图的信息,您可以获取geometry.size 之类的大小,然后计算中间点等

GeometryReader 内部的布局是 ZStack,所以所有项目都将是一个在彼此之上

绘制曲线的简单方法是Path { path in },在这个块内你可以添加线条/曲线到路径,而不是stoke()

您可以通过两种方式绘制圆圈:第一种是再次使用Path,添加圆角矩形和fill()

另一种选择是放置 Circle() 并添加偏移量

第一种方式是蓝色的,第二种方式是半径较小的绿色。我随机选择曲线控制点只是为了给你一个想法

let circleRelativeCenters = [
    CGPoint(x: 0.8, y: 0.2),
    CGPoint(x: 0.2, y: 0.5),
    CGPoint(x: 0.8, y: 0.8),
]

var body: some View {
    GeometryReader { geometry in
        let normalizedCenters = circleRelativeCenters
            .map { center in
                CGPoint(
                    x: center.x * geometry.size.width,
                    y: center.y * geometry.size.height
                )
            }
        Path { path in
            var prevPoint = CGPoint(x: normalizedCenters[0].x / 4, y: normalizedCenters[0].y / 2)
            path.move(to: prevPoint)
            normalizedCenters.forEach { center in
                    path.addQuadCurve(
                        to: center,
                        control: .init(
                            x: (center.x + prevPoint.x) / 2,
                            y: (center.y - prevPoint.y) / 2)
                    )
                    prevPoint = center
            }
        }.stroke(lineWidth: 3).foregroundColor(.blue).background(Color.yellow)
        Path { path in
            let circleDiamter = geometry.size.width / 5
            let circleFrameSize = CGSize(width: circleDiamter, height: circleDiamter)
            let circleCornerSize = CGSize(width: circleDiamter / 2, height: circleDiamter / 2)
            normalizedCenters.forEach { center in
                path.addRoundedRect(
                    in: CGRect(
                        origin: CGPoint(
                            x: center.x - circleFrameSize.width / 2,
                            y: center.y - circleFrameSize.width / 2
                        ), size: circleFrameSize
                    ),
                    cornerSize: circleCornerSize
                )
            }
        }.fill()
        ForEach(normalizedCenters.indices, id: \.self) { i in
            let center = normalizedCenters[i]
            let circleDiamter = geometry.size.width / 6
            let circleFrameSize = CGSize(width: circleDiamter, height: circleDiamter)
            Circle()
                .frame(size: circleFrameSize)
                .offset(
                    x: center.x - circleFrameSize.width / 2,
                    y: center.y - circleFrameSize.width / 2
                )
        }.foregroundColor(.green)
    }.frame(maxWidth: .infinity, maxHeight: .infinity).foregroundColor(.blue).background(Color.yellow)
}

结果:


Path { path in 内部我可以使用forEach,因为它不再是视图构建器的范围。

如果你需要为你的修饰符做一些计算,你可以使用下一个技巧:

func circles(geometry: GeometryProxy) -> some View {
    var points = [CGPoint]()
    var prevPoint: CGPoint?
    (0...5).forEach { i in
        let point: CGPoint
        if let prevPoint = prevPoint {
            point = CGPoint(x: prevPoint.x + 1, y: prevPoint.y)
        } else {
            point = .zero
            
        }
        points.append(point)
        prevPoint = point
    }
    return ForEach(points.indices, id: \.self) { i in
        let point = points[i]
        Circle()
            .offset(
                x: point.x,
                y: point.y
            )
    }
}

然后你可以像circles(geometry: geometry).foregroundColor(.green)那样在body里面使用它

【讨论】:

  • 非常感谢您的努力!这绝对是与我尝试做的不同的逻辑方法(首先绘制圆圈然后获取它们的坐标)。您基本上是在手动设置“坐标”然后对其进行规范化。我想我可以在ForEach 中完成这项工作,方法是设置起点,然后在循环中添加它+对其进行规范化。
  • 我会尝试并在几个小时后报告以接受您的回答!
  • 我几乎让它工作了,但我被困在这个可能很明显的问题上(我对 SwiftUI 还是有点陌生​​):使用ForEach 我设法画线然后画圆。然后我想增加下一次循环运行的值,比如startPoint = nextPoint但是我当然不能在ForEach 中运行任何这样的代码。您巧妙地使用了数组的.forEach,但这意味着我必须事先声明每个坐标。但我需要它是动态的,并在绘制前一个点之后计算每个点。你有什么建议吗?
  • 好吧,我设法做了一些正确的事情,但它在代码方面很丑陋。我想我在某种程度上滥用了 SwiftUI。我可能必须走你的路线并在init() 中运行func(),它会为我预先计算所有点,以便我可以在GeometryReader 的开头将其映射。我会看看如何改进它,也许我会把它作为一个单独的答案分享。不管怎样,谢谢你,你真的帮了我很多!
  • @Big_Chair 我已经添加了一个解释和一个可以解决您问题的示例
【解决方案2】:

我正在使用preferenceKey (CirclePointsKey) 的reduce function 将所有坐标存储到点数组中。带有几何阅读器的覆盖将读取每个球的每个中间位置的位置。我将视图容器框架命名为 ballContainer 只是为了获得正确的相对位置。

这不完全是您发布的曲线,但您可以根据需要更改“path.addCurve”中的参数。

只有当至少有 2 个preferenceKey 试图设置一个新值时才会调用reduce 函数。通常用于设置一个新值,但在这种情况下,我将附加每个值。

struct CirclePointsKey: PreferenceKey {
    typealias Value = [CGPoint]

    static var defaultValue: [CGPoint] = []

    static func reduce(value: inout [CGPoint], nextValue: () -> [CGPoint]) {
        value.append(contentsOf: nextValue())
    }
}

struct ExampleView: View {
    @State var points: [CGPoint] = []

    var body: some View {
        ZStack {
            ScrollView {
                ZStack {
                    Path { (path: inout Path) in

                        if let firstPoint = points.first {
                            path.move(to: firstPoint)
                            var lastPoint: CGPoint = firstPoint
                            for point in points.dropFirst() {
//                                path.addLine(to: point)
                                let isGoingRight = point.x < lastPoint.x
                                path.addCurve(to: point, control1: CGPoint(x: isGoingRight ? point.x : lastPoint.x,
                                                                           y: !isGoingRight ? point.y : lastPoint.y),
                                              control2: CGPoint(x: point.x, y: point.y))
                                lastPoint = point
                            }
                        }
                    }
                    .stroke(lineWidth: 2)
                    .foregroundColor(.white.opacity(0.5))

                    VStack {
                        VStack(spacing: 30) {
                            ForEach(Array(0...4).indices) { index in
                                ball
                                    .overlay(GeometryReader { geometry in
                                        Color.clear
                                            .preference(key: CirclePointsKey.self,
                                                        value: [CGPoint(x: geometry.frame(in: .named("ballContainer")).midX,
                                                                        y: geometry.frame(in: .named("ballContainer")).midY)])
                                    })
                                    .frame(maxWidth: .infinity,
                                           alignment: index.isMultiple(of: 2) ? .leading : .trailing)
                            }
                        }
                        .coordinateSpace(name: "ballContainer")
                        .onPreferenceChange(CirclePointsKey.self) { data in
                            points = data
                        }
                        Text("points:\n \(String(describing: points))")
                        Spacer()
                    }
                }
            }
        }
        .foregroundColor(.white)
        .background(Color.black.ignoresSafeArea())
    }

    @ViewBuilder
    var ball: some View {
        Circle()
            .fill(Color.gray)
            .frame(width: 70, height: 70, alignment: .center)
            .padding(10)
            .overlay(Circle()
                .strokeBorder(style: StrokeStyle(lineWidth: 2, dash: [5, 10, 10, 5]))
                .foregroundColor(.white)
                .padding(7)
            )
            .shadow(color: .white.opacity(0.7), radius: 10, x: 0.0, y: 0.0)
    }
}

【讨论】:

  • 啊,我读到了这种方法here,但我无法真正理解它——现在我明白了。我决定仍然接受菲利普的回答,因为我的头脑更容易理解,但我非常感谢你的不同观点!
猜你喜欢
  • 2016-02-01
  • 2013-11-29
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2023-04-10
  • 2015-01-16
  • 2013-07-08
  • 1970-01-01
相关资源
最近更新 更多