Search code examples
arraysswiftswiftuiforeachprotocols

Is it possible to have 2 arrays in the same ForEach in SwiftUI?


Is it possible to have 2 structs/arrays in the same ForEach (for the Option 3)? If so, how? All my attempts were unsuccessful.

import Foundation
import SwiftUI

struct Car: Vehicle, Identifiable {
    let id = UUID()
    let name: String
}

struct Bike: Vehicle, Identifiable {
    let id = UUID()
    let name: String
}

let cars = [Car(name: "Renault"), Car(name: "Peugeot")]

let bikes = [Bike(name: "Yamaha"), Bike(name: "Honda")]

protocol Vehicle {
    var name: String { get }
}

struct ParentView: View {
    var body: some View {
        // Option 1 OK
        ForEach(cars.sorted { $0.name < $1.name }) { car in
            ChildView(vehicle: car)
        }

        // Option 2 OK
        ForEach(bikes.sorted { $0.name < $1.name }) { bike in
            ChildView(vehicle: bike)
        }

        // Option 3 NOT OK
        ForEach((cars, bikes).sorted { $0.name < $1.name }) { vehicle in
            ChildView(vehicle: vehicle)
        }
    }
}

struct ChildView: View {
    let vehicle: Vehicle

    var body: some View {
        Text(vehicle.name)
    }
}

Thanks, in advance!


Solution

  • Here is an alternative solution for the case when you don't want to or simply cannot change a protocol just to adhere to some UI requirements.

    We want to use any type that conforms to Vehicle and yet be able to efficiently use List/ForEach by also conforming to Identifiable. For this solution we can create a wrapper type for this

    struct VehicleUI: Identifiable {
        let id = UUID()
        let vehicle: any Vehicle
    }
    

    And then we can map and join the types directly in the view or some model class

    (cars + bikes)
        .map(VehicleUI.init)
        .sorted(using: KeyPathComparator(\.vehicle.name, order: .forward))
    

    Example:

    struct ParentView: View {
        var vehicles: [VehicleUI]  {
            (cars + bikes)
                .map(VehicleUI.init)
                .sorted(using: KeyPathComparator(\.vehicle.name, order: .forward))
        }
        var body: some View {
            List {
                ForEach(vehicles) { vehicle in
                    ChildView(vehicle: vehicle.vehicle)
                }
            }
        }
    }