Search code examples
swiftswiftuiswiftui-listhashableidentifiable

SwiftUI - using ForEach with a Binding array that does not conform to identifiable / hashable


I have the a codable object as follows:

struct IncidentResponse: Codable {
    let incident: IncidentDetails?
}

struct IncidentDetails: Codable, Identifiable {
    let id: String?
    let reason: IncidentReasonResponse?
    let message: String?
    let startedAt: String?
    let endedAt: String?
}

struct IncidentReasonResponse: Codable, Identifiable {
    let id: String?
    let name: String?
    let code: String?
    let inOp: Bool?
}

Here is an example of an incident response when called from the API:

{
  "incident": {
    "id": "610aebad8c719475517e9736",
    "user": null,
    "reason": {
      "name": "No aircraft",
      "code": "no-aircraft",
      "inOp": true
    },
    "message": "test this",
    "startedAt": "2021-08-04T19:34:05+0000",
    "endedAt": null
  }
}

In SwiftUI, I am trying to display a list of these. So I have an array of these IncidentResponse objects named existingIncidents and then the following:

var body: some View {
    List {
        Section(header: Text("Existing incidents")) {
            if let existingIncidents = self.existingIncidents {
                ForEach(existingIncidents) { incident in
                    
                    VStack(alignment: .leading) {
                        HStack {
                            Image.General.incident
                                .foregroundColor(Constants.iconColor)
                            Text(incident.incident?.reason?.name ?? "")
                                .foregroundColor(Constants.textColor)
                                .bold()
                        }
                        Spacer()
                        HStack {
                            Image.General.clock
                                .foregroundColor(Constants.iconColor)
                            Text(incident.incident?.startedAt ?? "No date")
                                .foregroundColor(Constants.textColor)
                        }
                        Spacer()
                        HStack {
                            Image.General.message
                                .foregroundColor(Constants.iconColor)
                            Text(incident.incident?.message ?? "No message")
                                .foregroundColor(Constants.textColor)
                        }
                    }
                }
            }
        }
    }
    .listStyle(PlainListStyle())

However, I am unable to use existingIncidents as it is as it does not conform to Identifiable or Hashable (so I can't use the id: /.self workaround) ...

How can I get around this?

I tried to add a UUID into IncidentResponse like this:

struct IncidentResponse: Codable {
    let incident: IncidentDetails?
    var id = UUID().uuidString
}

However, this is then stopping the object from decoding properly from the API.


Solution

  • Option 1:

    Give your IncidentResponse an ID and then tell it to not try to decode the value:

    struct IncidentResponse: Codable, Identifiable {
        var id = UUID()
        let incident: IncidentDetails?
        
        enum CodingKeys: String, CodingKey {
            case incident
        }
    }
    

    Option 2:

    Make incident and id non-optional and then use this to get the id:

    ForEach(existingIncidents, id: \.incident.id) { incident in
    

    I'll also note that IncidentResponse seems to be a somewhat meaningless wrapper at this point. When you do your decoding and store the values to existingIncidents (which you haven't shown), you could probably store them just as [IncidentDetails] instead. In that case, you just have to make the id property non-optional on IncidentDetails and declare it as Identifiable. For example:

    struct IncidentDetails: Codable, Identifiable {
        let id: String
        let reason: IncidentReasonResponse?
        let message: String?
        let startedAt: String?
        let endedAt: String?
    }
    
    //....
    
    let existingIncidentsWrappers : [IncidentResponse] = //...
    let existingIncidents : [IncidentDetails] = existingIncidentsWrappers.compactMap { $0.incident }