Johnlo - Car Listing App Challenge

Well CodeCrew, my first Community App Challenge.

My repo

I’ve got to the point where I can expand and collapse a card, showing the detail, but on to collapsing an expanded card when tabbing a different card? I haven’t figured out how to do that yet.

1 Like

This is nice @isourcedesign

Can you share your code for us to also check?

I answered a question from one of the participants also a few minutes ago, maybe you can also get something from this implementation.

The expand is working but I need to add functionality so that when tapped, the expanded card should collapse while the card being tapped should expand.

CarList

ContentView:

struct ContentView: View {
    
    @EnvironmentObject var model: BuyMyRideModel
    
    var body: some View {
        
        VStack {
            
            Text(Constants.appTitle)
                .bold()
                .font(.title)
                .foregroundColor(ColorManager.titleColor)
            
            // List of vehicles
            ScrollView {
                
                // Loop through the list of vehicles and add them to the screen
                ForEach(model.vehicles) { vehicle in
                    
                    CardView(vehicle: vehicle, isExpanded: model.selection.contains(vehicle))
                        .onTapGesture {
                            // Expand/collapse the card when it's tapped
                            model.expandCollapse(vehicle)
                        }
                        // Animate the expand/collapse
                        .animation(.easeInOut(duration: 0.2), value: model.selection)
                }
            }
        }
        .padding()
        .background(ColorManager.backgroundColor)
    }
}

CardView:

struct CardView: View {
    
    var vehicle: Vehicle
    let isExpanded: Bool
    
    var body: some View {
        
        HStack {
            
            // Card
            ZStack(alignment: .leading) {
                
                // Background
                Rectangle()
                    .foregroundColor(.white)
                    .cornerRadius(Constants.cornerRadius)
                
                HStack {
                    
                    // Vehicle image
                    Image(vehicle.image)
                        .resizable()
                        .aspectRatio(contentMode: .fill)
                        .frame(maxWidth: 175, minHeight: 125)
                        .clipped()
                        .cornerRadius(Constants.cornerRadius, corners: [.topLeft, .bottomLeft])
                    
                    // Vehicle info
                    VStack(alignment: .leading) {
                        
                        Text(vehicle.make + " " + vehicle.model)
                            .font(Font.system(size: Constants.headline, weight: .semibold))
                            .foregroundColor(ColorManager.titleColor)
                        
                        Text(vehicle.price)
                            .font(Font.system(size: Constants.footnote))
                            .foregroundColor(ColorManager.lightColor)
                        
                        // Rating
                        HStack {
                            
                            // The number of stars rated
                            ForEach (1..<vehicle.rating+1) { i in
                                Image(systemName: "star.fill")
                                    .font(Font.system(size: Constants.caption2))
                                    .padding(.horizontal, -5)
                            }
                            .foregroundColor(ColorManager.titleColor)
                            
                            // The number of stars not rated
                            if vehicle.rating < 5 {
                                ForEach(0..<5-vehicle.rating) { i in
                                    Image(systemName: "star")
                                        .font(Font.system(size: Constants.caption2))
                                        .padding(.horizontal, -5)
                                }
                                .foregroundColor(ColorManager.lightColor)
                            }
                        }
                        
                        // Pros and Cons
                        
                        if isExpanded {
                            
                            // Are there any Pros
                            if vehicle.pros != nil {
                                Text(Constants.pros + ":")
                                    .bold()
                                    .font(Font.system(size: Constants.footnote))
                                
                                VStack(alignment: .leading) {
                                    
                                    // Loop through the pros
                                    ForEach(vehicle.pros!, id: \.self) { item in
                                        HStack(alignment: .top) {
                                            Text(" •")
                                            Text(item)
                                        }
                                        .font(Font.system(size: Constants.footnote))
                                    }
                                }
                            }
                            
                            // Are the any Cons
                            if vehicle.cons != nil {
                                Text(Constants.cons + ":")
                                    .bold()
                                    .font(Font.system(size: Constants.footnote))
                                
                                VStack(alignment: .leading) {
                                    
                                    // Loop through the cons
                                    ForEach(vehicle.cons!, id: \.self) { item in
                                        HStack(alignment: .top) {
                                            Text(" •")
                                            Text(item)
                                        }
                                        .font(Font.system(size: Constants.footnote))
                                    }
                                }
                            }
                            
                            // There are no Pros or Cons
                            if vehicle.pros == nil && vehicle.cons == nil {
                                Text(Constants.noDetails)
                                    .font(Font.system(size: Constants.footnote))
                            }
                        }
                        
                        Spacer()
                        
                        Text(Constants.ShowDetails)
                            .bold()
                            .font(Font.system(size: Constants.subhead))
                            .foregroundColor(.blue)
                    }
                    .padding(.vertical, 5)
                    .padding(.trailing, 5)
                }
            }
        }
        .contentShape(Rectangle())
    }
}

// Extend View so we can apply radius to the individual corners of an object
extension View {
    
    func cornerRadius(_ radius: CGFloat, corners: UIRectCorner) -> some View {
        clipShape( RoundedCorner(radius: radius, corners: corners) )
    }
}

// Struct so individual corner radius can be done
struct RoundedCorner: Shape {

    var radius: CGFloat = .infinity
    var corners: UIRectCorner = .allCorners

    func path(in rect: CGRect) -> Path {
        let path = UIBezierPath(roundedRect: rect, byRoundingCorners: corners, cornerRadii: CGSize(width: radius, height: radius))
        return Path(path.cgPath)
    }
}

ViewModel:

class BuyMyRideModel: ObservableObject {
    
    @Published var vehicles = [Vehicle]()
    @Published var selection: Set<Vehicle> = [] // local data that changes dynamically
    
    init() {
        
        // Create data service instance
        let service = DataService()
        
        // Get the data
        self.vehicles = service.getLocalData()
    }
    
    // Expand/Collapse selected vehicle details
    func expandCollapse(_ vehicle: Vehicle) {
        
        // Keep track of vehicle selected
        if selection.contains(vehicle) {
            // Collapse
            selection.remove(vehicle)
        } else {
            // Expand
            selection.insert(vehicle)
        }
    }
}
1 Like

Level 2 completed but not without challenges and a lot of research and exploring what others have done. :grinning:

1 Like