swiftswiftuiswiftui-list

How can I connect two Dots with a line in SwiftUi?


This is my code:

ScrollView {
    LazyVStack {
        ForEach(0 ..< 4, id: \.self) { _ in // Index verwenden
            HStack {
                ConnectedCircles()
                CardView_beige {
                    HStack {
                        CardView_todo(title: "08:10 - 8:15", subtitle: "Mac Book sauber machen!", textlong: "15 min")
                    }
                }
                .padding()
            }
        }
    }
    .background(Color.white.ignoresSafeArea())
    .clipShape(
        .rect(
            topLeadingRadius: 25,
            bottomLeadingRadius: 0,
            bottomTrailingRadius: 0,
            topTrailingRadius: 25
        )
    )
}

struct VerticalLine: Shape {
    func path(in rect: CGRect) -> Path {
        var path = Path()
        path.move(to: CGPoint(x: rect.midX, y: rect.minY))
        path.addLine(to: CGPoint(x: rect.midX, y: rect.maxY))
        return path
    }
}

struct ConnectedCircles: View {
    var body: some View {
        VStack(spacing: 0) {
            VerticalLine()
                .stroke(Color.blue, lineWidth: 4)
                .frame(width: 20, height: 10)
            Circle()
                .strokeBorder(Color.blue, lineWidth: 4)
                .frame(width: 30, height: 30)

            VerticalLine()
                .stroke(Color.blue, lineWidth: 4)
                .frame(width: 20, height: 50)
        }
    }
}

These are my CardViews:

struct CardView_todo: View {
    var title: String
    var subtitle: String
    var textlong: String
    var time: String?
    
    var body: some View {
        VStack {
            VStack {
                HStack {
                    Text(title)
                        .foregroundStyle(.black)
                        .padding(2)
                    Spacer()
                }
                
                HStack {
                    Text(subtitle)
                        .fontWeight(.bold)
                        .foregroundStyle(.black)
                        
                    Spacer()
                }
                .padding(.leading, 2)
                
                HStack {
                    Text(textlong)
                        .foregroundStyle(.black)
                        .padding(2)
                    Spacer()
                }
            }
        }
    }
}

struct CardView_beige<Content: View>: View {
    let content: Content

    init(@ViewBuilder content: () -> Content) {
        self.content = content()
    }

    var body: some View {
        content
            .padding()
            .background(Color(#colorLiteral(red: 0.9490196078, green: 0.9019607843, blue: 0.8470588235, alpha: 1))) // Beige Hintergrundfarbe
            .cornerRadius(10)
    }
}

I'm trying to make a list in SwiftUI where two points are connected by what is actually a dotted line.

I have managed to create a circle and a line, but I can't get the lines to connect, and I can't get the first circle of the list to have a line going upwards, for example.


Solution

  • The dashed line makes this problem a little complicated. If you try to draw each link separately then you can sometimes see that the sizes of the dashes are different where the sections come together.

    It works better if you have one continuous dashed line going from top to bottom. The circles can then simply be drawn over it. This can be achieved with just the VerticalLine shape and basic Circle shapes, ConnectedCircles is not needed.

    Like this:

    EDIT: Implementation changed from an HStack with negative padding on the main content to a ZStack with leading padding on the main content (it looks the same, but I think it's a bit cleaner).

    EDIT2: Updated in response to comment to hide the dashed line above the first circle and below the last.

    struct ContentView: View {
        let nCards = 4
        var body: some View {
            ScrollView {
                ZStack(alignment: .leading) {
    
                    // The dashed line from top to bottom
                    VerticalLine()
                        .stroke(style: .init(lineWidth: 4, dash: [7, 5]))
                        .frame(width: 60)
                        .foregroundColor(.blue)
    
                    LazyVStack {
                        ForEach(0..<nCards, id: \.self) { index in // Index verwenden
                            HStack(spacing: 0) {
                                Circle()
                                    .stroke(lineWidth: 4)
                                    .frame(width: 30, height: 30)
                                    .foregroundColor(.blue)
                                    .background(.white)
    
                                CardView_beige {
                                    CardView_todo(title: "08:10 - 8:15", subtitle: "Mac Book sauber machen!", textlong: "15 min")
                                }
                                .padding()
                            }
                            // Align the circles with the dashed line
                            .padding(.leading, 15)
    
                            // Cover the dashed line above the first circle
                            // and below the last
                            .background {
                                if index == 0 {
                                    VStack {
                                        Color.white
                                        Color.clear
                                    }
                                } else if index == nCards - 1 {
                                    VStack {
                                        Color.clear
                                        Color.white
                                    }
                                }
                            }
                        }
                    }
                }
                .background(Color.white.ignoresSafeArea())
                .clipShape(
                    .rect(
                        topLeadingRadius: 25,
                        bottomLeadingRadius: 0,
                        bottomTrailingRadius: 0,
                        topTrailingRadius: 25
                    )
                )
            }
        }
    }
    

    Screenshot

    Hope it's close to what you were wanting.