Search code examples
swiftuirectanglesswiftui-list

Variable Rectangle() dimension based on cell size to draw a timeline


I am trying to build a List that I want to look like a timeline. Each cell will represent a milestone. Down the left hand side of the table, I want the cells to be 'connected', by a line (the timeline). I have tried various things to get it to display as I want but I have settled with basic geometric shapes , i.e Circle() and Rectangle().

This is sample code to highlight the problem:

import SwiftUI

struct ContentView: View {
    
    var body: some View {
        let roles: [String] = ["CEO", "CFO", "Managing Director and Chairman of the supervisory board", "Systems Analyst", "Supply Chain Expert"]
        NavigationView{
            VStack{
                List {
                    ForEach(0..<5) { toto in
                        NavigationLink(
                            destination: dummyView()
                        ) {
                            HStack(alignment: .top, spacing: 0) {
                                VStack(alignment: .center, spacing: 0){
                                    Rectangle()
                                        .frame(width: 1, height: 30, alignment: .center)
                                    Circle()
                                        .frame(width: 10, height: 10)
                                    Rectangle()
                                        .frame(width: 1, height: 20, alignment: .center)
                                    Circle()
                                        .frame(width: 30, height: 30)
                                        .overlay(
                                            Image(systemName: "gear")
                                                .foregroundColor(.gray)
                                                .font(.system(size: 30, weight: .light , design: .rounded))
                                                .frame(width: 30, height: 30)
                                        )
  //THIS IS THE RECTANGLE OBJECT FOR WHICH I WANT THE HEIGHT TO BE VARIABLE
                                    Rectangle()
                                        .frame(width: 1, height: 40, alignment: .center)
                                        .foregroundColor(.green)
                                }
                                .frame(width: 32, height: 80, alignment: .center)
                                .foregroundColor(.green)
                                
                                
                                VStack(alignment: .leading, spacing: 0, content: {
                                    Text("Dummy operation text that will be in the top of the cell")
                                        .font(.subheadline)
                                        .multilineTextAlignment(.leading)
                                        .lineLimit(1)
                                    Label {
                                        Text("March 6, 2021")
                                            .font(.caption2)
                                    } icon: {
                                        Image(systemName: "calendar.badge.clock")
                                    }
                                    
                                    HStack{
                                        
                                        HStack{
                                            Image(systemName: "flag.fill")
                                            Text("In Progress")
                                                .font(.system(size: 12))
                                        }
                                        .padding(.horizontal, 4)
                                        .padding(.vertical, 3)
                                        .foregroundColor(.blue)
                                        .background(Color.white)
                                        .cornerRadius(5, antialiased: true)
                                        
                                        HStack{
                                            Image(systemName: "person.fill")
                                            Text(roles[toto])
                                                .font(.system(size: 12))
                                            
                                        }
                                        .padding(.horizontal, 4)
                                        .padding(.vertical, 3)
                                        .foregroundColor(.green)
                                        .background(Color.white)
                                        .cornerRadius(5, antialiased: true)
                                        
                                        
                                        HStack{
                                            Image(systemName: "deskclock")
                                            Text("in 2 Months")
                                                .font(.system(size: 12))
                                        }
                                        .padding(.horizontal, 4)
                                        .padding(.vertical, 3)
                                        .foregroundColor(.red
                                            
                                        )
                                        .background(
                                            Color.white
                                        )
                                        .cornerRadius(5, antialiased: true)
                                    }
                                    
                                })
                            }.listRowInsets(.init(top: 0, leading: 0, bottom: 0, trailing: 0))
                        }
                    }
                }
            }
        }    
    }
}



struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

struct dummyView: View {
    var body: some View {
        Text("Hello, World!")
    }
}

struct dummyView_Previews: PreviewProvider {
    static var previews: some View {
        dummyView()
    }
}

but as you can see in the enclosed picture, there are unwanted gaps

enter image description here

So other content in the cell is making the height of the entire cell 'unpredictable' and break the line.

Is there a way to determine the height of the cell and extend the dimensions of the Rectangle, so that it extends to the full height of the cell?

Is there a better approach you recommend for trying to build such a timeline ?

PS: I have tried playing around with .frame and .infinity but that does work. Many thanks.


Solution

  • Why not just draw the line based on the size of the row. See Creating custom paths with SwiftUI. Remember, everything is a view.

    First, you need to decompose what you are doing into subviews. You have too many moving parts in one view to get it correct. Also, I would avoid setting specific padding amounts as that will mess you up when you change devices. You want a simple, smart view that is generic enough to handle different devices.

    I would have a row view that has a geometry reader so it knows its own height. You could then draw the line so that it spanned the full height of the row, regardless of the height. Something along the lines of this:

    struct ListRow: View {
        var body: some View {
            GeometryReader { geometry in
                ZStack {
                    HStack {
                        Spacer()
                        Text("Hello, World!")
                        Spacer()
                    }
                    VerticalLine(geometry: geometry)
                }
            }
        }
    }
    

    and

    struct VerticalLine: View {
        
        let geometry: GeometryProxy
        
        var body: some View {
            Path { path in
                path.move(to: CGPoint(x: 20, y: -30))
                path.addLine(to: CGPoint(x: 20, y: geometry.size.height+30))
            }
            .stroke(Color.green, lineWidth: 4)
        }
    }