Reputation: 2795
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
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
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