List style SwiftUI

1.6k views Asked by At

How to do such a list like in the picture? (edit: list header)

enter image description here

Here's what I've tried - List and Form:

NavigationView {
    VStack {
        List { // or Form here
            // Text("New") <- this also isn't it
            Section(header: Text("New")) {
                ForEach(... ... .indices, id: \.self) { index in
                    ...
                    ...
                }
            }
            
            Section(header: Text("Done")) {
                ForEach(...) { ...
                    ...
                }
            }
     }
}
            // On the pictures below - `List` on the left; `Form` on the right

enter image description here enter image description here

2

There are 2 answers

2
Asperi On BEST ANSWER

It looks like a Form with sections, the section headers can be configured (like shown below), rows content compose as you need

demo

NavigationView {
    Form {
        Section(header: Text("New").bold().font(.title).foregroundColor(.black)) {

EDIT: .textCase(.none) allows the source text capitalization "Xxxx"

Section(header: Text("New").bold().font(.title).textCase(.none).foregroundColor(.black))
0
Helperbug On

Here is a solution using the scroll view, custom section, and custom list item. Each section gets its own header, color and total. Each item gets an icon, label, and toggle. The toggle buttons add or remove an item's price from the section total.

[Custom Section with]

struct CustomListSectionView: View {
    @ObservedObject var listSection : CustomListSection

    var body: some View {
        VStack(spacing: 0) {
            headerView
                .padding(8)
            listView
                .padding()
                .background(RoundedRectangle(cornerRadius: 25.0).foregroundColor(.white))
                .padding(.horizontal)
            footerView
                .padding(8)
        }
        .background(Color.secondary)
    }
    
    var headerView : some View {
        HStack {
            Text(listSection.header)
                .font(.system(size: 24, weight: .medium, design: .rounded))
                .foregroundColor(.primary)
            Spacer()
        }
    }
    
    var listView : some View {
        VStack(spacing: 3) {
            ForEach(Array(listSection.items.enumerated()), id: \.offset) { index, item in
                CustomListItemView(color: listSection.color).environmentObject(item)
                if index < listSection.items.count - 1 {
                    Divider()
                }
            }
        }
    }

    var footerView : some View {
        HStack {
            Spacer()
            Text("Sum:")
                .font(.system(size: 18, weight: .semibold, design: .rounded))
                .foregroundColor(.secondary)
            Text(CustomListSectionView.formatter().string(from: NSNumber(value: listSection.total)) ?? "-")
                .font(.system(size: 18, weight: .medium, design: .rounded).monospacedDigit())
                .foregroundColor(.primary)
        }
    }
    
    static func formatter() -> NumberFormatter {
        let currencyFormatter = NumberFormatter()
        currencyFormatter.locale = Locale.current
        currencyFormatter.numberStyle = .currency
        currencyFormatter.usesGroupingSeparator = true
        return currencyFormatter
    }
}

struct CustomListItemView: View {
    @EnvironmentObject var listItem : CustomListItem
    let color : Color
    
    var body: some View {
        HStack {
            ZStack {
                Image(systemName: listItem.symbol)
                    .font(.headline)
                    .foregroundColor(.white.opacity(0.8))
                    .padding(6)
                    .background(Circle().foregroundColor(color))
            }
            .shadow(radius: 3)
            Text(listItem.name)
            Spacer()
            Button(action: { listItem.select.toggle() }) {
                Image(systemName: listItem.select ? "largecircle.fill.circle" : "circle")
                    .foregroundColor(.accentColor)
                    .padding(6)
            }.buttonStyle(PlainButtonStyle())
        }
    }
}

class CustomListSection: ObservableObject, Hashable {
    // hashable so id: \.self work in ForEach's
    let id = UUID()
    let header : String
    let color : Color
    @Published var items : [CustomListItem] = []
    @Published var total : Double = 0
    
    func add(_ name: String, _ symbol: String, _ amount: Double) {
        items.append(CustomListItem(name, symbol, amount, calc))
    }
    
    func calc() {
        total = items.map({ $0.price }).reduce(0, +)
    }
    
    init(_ header: String, _ color: Color) {
        self.header = header
        self.color = color
    }
    
    public func hash(into hasher: inout Hasher) {
            hasher.combine(id)
    }
    
    static func == (lhs: CustomListSection, rhs: CustomListSection) -> Bool {
        lhs.id == rhs.id
    }
}

class CustomListItem: ObservableObject, Hashable {
    @Published var select : Bool = false { didSet {
        withAnimation { calc() }
    } }
    let name : String
    let symbol : String
    @Published var amount : Double
    let calc : () -> Void

    init(_ name: String, _ symbol: String, _ amount: Double, _ calc: @escaping () -> Void) {
        self.name = name
        self.symbol = symbol
        self.amount = amount
        self.calc = calc
    }

    var price : Double { select ? amount : 0 }
    
    // hashable so id: \.self work in ForEach's
    let id = UUID()

    public func hash(into hasher: inout Hasher) {
            hasher.combine(id)
    }
    
    static func == (lhs: CustomListItem, rhs: CustomListItem) -> Bool {
        lhs.id == rhs.id
    }
}

struct CustomListSectionView_Previews: PreviewProvider {
    static var previews: some View {
        let section = CustomListSection("Section Name", .purple)
        section.add("Alpha Item", "a.circle", 1000)
        section.add("Delta Item", "d.circle", 100)
        section.add("Omega Item", "z.circle", 1)
        return CustomListSectionView(listSection: section)
    }
}