Melodius
Melodius

Reputation: 2795

Can't get SwiftUI List selection to work with custom struct

I'm trying to make a List of favorite newspapers. In edit mode the list displays all available newspapers from which the user can select his favorites. After selecting favorites the list displays only the favorites. Here is my code:

 struct Newspaper: Hashable {
     let name: String
 }
 
 struct ContentView: View {
     @State var editMode: EditMode = .inactive
     @State private var selection = Set<Newspaper>()
     var favorites: [Newspaper] {
         selection.sorted(by: ({ $0.name < $1.name }))
     }
     
     let newspapers = [
         Newspaper(name: "New York Times"),
         Newspaper(name: "Washington Post")
     ]
     
     var body: some View {
         NavigationView {
             List(editMode == .inactive ? favorites : newspapers, id: \.name, selection: $selection) { aliasItem in
                 Text(aliasItem.name)
             }
             .toolbar {
                 EditButton()
             }
             .environment(\.editMode, self.$editMode)
         }
     }
 }

The problem is that the list enters edit mode, but the selection widgets don't appear. If I replace Newspaper with just an array of String (and modify the rest of the code accordingly), then the selection widgets do appear and the list works as expected. Can anyone explain what the problem is?

I originally tried using an Identifiable Newspaper like this:

 struct Newspaper: Codable, Identifiable, Equatable, Hashable {
     var id: String { alias + publicationName }
     let alias: String
     let publicationName: String
 }

Since this didn't work, I tested the simpler version above to try to pinpoint the problem.

Since I need to save the favorites, the Newspaper has to be Codable and thus can't use UUID as they are read from disk and the complete Newspapers array is fetched from a server. That's why I have the id as a computed property.

Yrb:s answer provided the solution to the problem: the type of the selection Set has to be the same type as the id you are using in your Identifiable struct and not the type that you are displaying in the List.

So in my case (with the Identifiable Newspaper version) the selection Set has to be of type Set<String> and not Set<Newspaper> since the id of Newspaper is a String.

Upvotes: 7

Views: 2176

Answers (2)

padeso
padeso

Reputation: 464

You can use a struct for a List's selection, but you have to tell SwiftUI about it, so it doesn’t use the struct’s ID. The original code works with an appropriate tag added, specifically:

Text(aliasItem.name)
    .tag(aliasItem)

Upvotes: 3

Yrb
Yrb

Reputation: 9705

Your issue stems from the fact that List's selection mode uses the id: property to track your selection. Since you are declaring your List as List(..., id: \.name, ...), your selection var needs to be of type String. If you change it to List(..., id: \.self, ...), it will work, but using self in a list like that brings it own problems. In keeping with best practice, and forgetting the selection for a moment, you should be using an Identifiable struct. List should then identify the elements by the id parameter on the struct. (I used a UUID)

Working up to the selection, that means you need to define it as @State private var selection = Set<UUID>(). That leaves dealing with your favorites computed variable. Instead of returning an array of your selection, you simply filter the newspapers array for those element contained in selection. In the end, that leaves you with this:

struct Newspaper: Identifiable, Comparable {
    let id = UUID()
    let name: String
    
    static func < (lhs: Newspaper, rhs: Newspaper) -> Bool {
        lhs.name < rhs.name
    }
}

 struct ContentView: View {
    @State var editMode: EditMode = .inactive
    @State private var selection = Set<UUID>()
    var favorites: [Newspaper] {
        newspapers.filter { selection.contains($0.id) }
    }
    
    let newspapers = [
        Newspaper(name: "New York Times"),
        Newspaper(name: "Washington Post")
    ]
    
    var body: some View {
        NavigationView {
            VStack {
                List(editMode == .inactive ? favorites.sorted() : newspapers, selection: $selection) { aliasItem in
                    Text(aliasItem.name)
                }
                .toolbar {
                    EditButton()
                }
                .environment(\.editMode, self.$editMode)
                Text(selection.count.description)
            }
        }
    }
}

EDIT:

In your comment, you said that Newspaper needs to be 'Codable', and implied that there is no id parameter in the server response. The below Newspaper is Codable, but will not expect an id in the server response, but will simply add its own constant id. It is a very bad idea to have a computed id. id should never change and it should be unique. UUID gives you that.

struct Newspaper: Identifiable, Comparable, Codable {
    let id = UUID()
    let name: String
    
    enum CodingKeys:String,CodingKey {
        case name
    }

    static func < (lhs: Newspaper, rhs: Newspaper) -> Bool {
        lhs.name < rhs.name
    }
}

Upvotes: 6

Related Questions