简体   繁体   中英

SwiftUI on macOS: list with detail view and multiple selection

TL;DR:

I cannot have a list with a detail view and multiple selections on macOS.

In more detail:

For demonstration purposes of my issue, I made a small example project. The UI looks as follows: 在此处输入图像描述

This is the "app" when launched, with a list on top and a detail representation below. Because I am using the List's initialiser init(_:selection:rowContent:) , where selection is of type Binding<SelectionValue?>? according to Apple's documentation, I get selecting items with the keyboard arrow keys for free. 在此处输入图像描述

Here's the complete code:

import SwiftUI

@main
struct UseCurorsInLisstApp: App {
    var body: some Scene {
        WindowGroup {
            ContentView()
                .environmentObject(ViewModel())
        }
    }
}

class ViewModel: ObservableObject {
    @Published var items = [Item(), Item(), Item(), Item(), Item()]
    @Published var selectedItem: Item? = nil
}

struct Item: Identifiable, Hashable {
    let id = UUID()
}

struct ContentView: View {
    @EnvironmentObject var vm: ViewModel
    
    var body: some View {
        VStack {
            
            List(vm.items, id: \.self, selection: $vm.selectedItem) { item in
                VStack {
                    Text("Item \(item.id.uuidString)")
                    Divider()
                }
            }
            
            Divider()
            
            Group {
                if let item = vm.selectedItem {
                    Text("Detail item \(item.id.uuidString)")
                } else {
                    Text("No selection…")
                }
            }
            .frame(minHeight: 200.0, maxHeight: .infinity)
            
        }
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

Now, having had success with this so far, I figured being able to select more than one row would be useful, so I took a closer look into List(_:selection:rowContent:) , where selection is of type Binding<Set<SelectionValue>>? . To be able to have a detail view, I just made a few minor changes to

the ViewModel :

class ViewModel: ObservableObject {
    @Published var items = [Item(), Item(), Item(), Item(), Item()]
    @Published var selectedItem: Item? = nil
    
    @Published var selectedItems: Set<Item>? = nil {
        didSet {
            if selectedItems?.count == 1, let item = selectedItems?.first {
                selectedItem = item
            }
        }
    }
}

and the ContentView :

struct ContentView: View {
    @EnvironmentObject var vm: ViewModel
    
    var body: some View {
        VStack {
            
            List(vm.items, id: \.self, selection: $vm.selectedItems) { item in
                VStack {
                    Text("Item \(item.id.uuidString)")
                    Divider()
                }
            }
            
            Divider()
            
            Group {
                if vm.selectedItems?.count == 1, let item = vm.selectedItems?.first {
                    Text("Detail item \(item.id.uuidString)")
                } else {
                    Text("No or multiple selection…")
                }
            }
            .frame(minHeight: 200.0, maxHeight: .infinity)
            
        }
    }
}

The problem now is that I cannot select an item of the row any more, neither by clicking, nor by arrow keys. Is this a limitation I am running into or am I "holding it wrong"?

Use the button and insert it into the set. Keyboard selection also works with shift + (up/down arrow)

class ViewModel: ObservableObject {
    @Published var items = [Item(), Item(), Item(), Item(), Item()]
    @Published var selectedItem: Item? = nil
    
    @Published var selectedItems: Set<Item> = []
}

struct ContentView: View {
    @EnvironmentObject var vm: ViewModel
    
    var body: some View {
        VStack {
            
            List(vm.items, id: \.self, selection: $vm.selectedItems) { item in
                Button {
                    vm.selectedItem = item
                    vm.selectedItems.insert(item)
                } label: {
                    VStack {
                        Text("Item \(item.id.uuidString)")
                        Divider()
                    }
                }
                .buttonStyle(PlainButtonStyle())
            }
            
            Divider()
            
            Group {
                if let item = vm.selectedItem {
                    Text("Detail item \(item.id.uuidString)")
                } else {
                    Text("No or multiple selection…")
                }
            }
            .frame(minHeight: 200.0, maxHeight: .infinity)
            
        }
    }
}

Add remove:

Button {
    vm.selectedItem = item
    if vm.selectedItems.contains(item) {
        vm.selectedItems.remove(item)
    } else {
        vm.selectedItems.insert(item)
    }
}

Edit In simple need to give a blank default value to set. because in nil it will never append to set need initialization.

@Published var selectedItems: Set<Item> = [] {

Actually my error was pretty dumb – making the selectedItems -set optional prevents the list from working correctly. Shoutout to @Raja Kishan, who pushed me into the right direction with his proposal.

Here's the complete working code:

import SwiftUI

@main
struct UseCurorsInLisstApp: App {
    
    var body: some Scene {
        WindowGroup {
            ContentView()
                .environmentObject(ViewModel())
        }
    }
}

class ViewModel: ObservableObject {
    @Published var items = [Item(), Item(), Item(), Item(), Item()]
    @Published var selectedItems = Set<Item>()
}

struct Item: Identifiable, Hashable {
    let id = UUID()
}

struct ContentView: View {
    @EnvironmentObject var vm: ViewModel
    
    var body: some View {
        VStack {
            List(vm.items, id: \.self, selection: $vm.selectedItems) { item in
                VStack {
                    Text("Item \(item.id.uuidString)")
                    Divider()
                }
            }
            
            Divider()
            
            Group {
                if vm.selectedItems.count == 1, let item = vm.selectedItems.first {
                    Text("Detail item \(item.id.uuidString)")
                } else {
                    Text("No or multiple selection…")
                }
            }
            .frame(minHeight: 200.0, maxHeight: .infinity)
        }
    }
}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM