英文:
How do I create selection state for items in a SwiftUI LazyVGrid, so it behaves like a list selection
问题
我正在在导航视图的详细信息部分组合一个网格视图。我希望能够在用户点击它们时为单个网格项设置选择状态,类似于列表的工作方式。我不希望多选,因此切换不起作用。我还希望自定义选择样式。
import SwiftUI
struct Tutorials: Identifiable, Hashable {
    var id = UUID()
    let name: String
    let icon: String
}
struct ImageItems: Identifiable, Hashable {
    var id = UUID()
    let name: String
    let icon: String
}
let allLibraries = [Tutorials(name: "所有图书馆项目", icon: "house")]
let lessons = [Tutorials(name: "浴室", icon: "bathtub"),
               Tutorials(name: "卧室", icon: "bed.double"),
               Tutorials(name: "餐厅", icon: "table.furniture"),
               Tutorials(name: "门窗", icon: "door.left.hand.open"),
               Tutorials(name: "厨房", icon: "cooktop"),
               Tutorials(name: "照明", icon: "lamp.table"),
               Tutorials(name: "客厅", icon: "sofa"),
               Tutorials(name: "杂项", icon: "ellipsis.circle"),
               Tutorials(name: "办公与学习", icon: "printer"),
               Tutorials(name: "楼梯", icon: "stairs"),
               Tutorials(name: "实用室", icon: "washer")]
let videos = [Tutorials(name: "装饰", icon: "paintbrush"),
              Tutorials(name: "家具", icon: "chair"),
              Tutorials(name: "杂项", icon: "ellipsis.circle"),
              Tutorials(name: "植物与树木", icon: "tree"),
              Tutorials(name: "池塘与游泳池", icon: "figure.open.water.swim"),
              Tutorials(name: "结构", icon: "door.garage.closed")]
let imageGrid = [ImageItems(name: "装饰", icon: "paintbrush"),
                 ImageItems(name: "家具", icon: "chair"),
                 ImageItems(name: "杂项", icon: "ellipsis.circle"),
                 ImageItems(name: "植物与树木", icon: "tree"),
                 ImageItems(name: "池塘与游泳池", icon: "figure.open.water.swim"),
                 ImageItems(name: "结构", icon: "door.garage.closed"),
                 ImageItems(name: "家具", icon: "chair"),
                 ImageItems(name: "杂项", icon: "ellipsis.circle"),
                 ImageItems(name: "植物与树木", icon: "tree"),
                 ImageItems(name: "池塘与水池", icon: "figure.open.water")]
struct MyDisclosureStyle: DisclosureGroupStyle {
    func makeBody(configuration: Configuration) -> some View {
        Button {
            withAnimation {
                configuration.isExpanded.toggle()
            }
        } label: {
            VStack {
                Spacer()
                    .frame(height: 15)
                HStack(alignment: .firstTextBaseline) {
                    configuration.label
                    Spacer()
                    Image(systemName: "chevron.right")
                        .rotationEffect(.degrees(configuration.isExpanded ? 90 : 0))
                }
                .padding(.top, 0)
                .padding(.bottom, 0)
                .contentShape(Rectangle())
                .onTapGesture {
                    withAnimation {
                        configuration.isExpanded.toggle()
                    }
                }
                Spacer()
                    .frame(height: 8)
            }
        }
        .buttonStyle(.plain)
        if configuration.isExpanded {
            configuration.content
                .padding(.leading, 0)
                .padding(.top, 0)
                .padding(.bottom, 0)
                .disclosureGroupStyle(self)
        }
    }
}
struct ContentView: View {
    
    @Environment(\.colorScheme) var colorScheme
    
    @State private var selectedList: Tutorials? = allLibraries[0]
    
    @State var selectedImage: ImageItems? = imageGrid[0]
    
    @State var expand = true
    @State var expand2 = true
    @State var expand3 = true
    @State private var isHidden = false
    
    var body: some View {
        NavigationSplitView {
            
            VStack(alignment: .leading) {
                List(selection: $selectedList) {
                    ForEach(allLibraries, id: \.self) { item in
                        HStack {
                            Image(systemName: item.icon)
                                .frame(width: 16, height: 16, alignment: .center)
                                .foregroundColor(self.selectedList == item ? Color.white : Color.accentColor)
                            Text(item.name)
                                .foregroundColor(self.selectedList == item ? Color.white : nil)
                        }
                        .font(.system(size: 13, weight: .medium, design: .rounded))
                        .foregroundColor(colorScheme == .dark ? .white : .black)
                        .padding(0)
                    }
                    DisclosureGroup("家庭计划图形", isExpanded: $expand) {
                        ForEach(lessons, id: \.self) { item in
                            HStack {
                                Image(systemName: item.icon)
                                    .frame(width: 16, height: 16, alignment: .center)
                                    .foregroundColor(self.selectedList == item ? Color.white : Color.accentColor)
                                Text(item.name)
                                    .foregroundColor(self.selectedList == item ? Color.white : nil)
                            }
                            .font(.system(size: 13, weight: .medium, design: .rounded))
                            .foregroundColor(colorScheme == .dark ? .white : .black)
                            .padding(.top, 5)
                            .padding(.bottom, 5)
                        }
                    }
                    .listRowInsets(EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 0))
                    .font(.system(size: 12, weight: .medium, design: .rounded))
                    .foregroundColor(.secondary)
                    .disclosureGroupStyle(MyDisclosureStyle())
                    
                    DisclosureGroup("花园计划图形", isExpanded: $expand2) {
                        ForEach(videos, id: \.self) { item in
                            HStack {
                                Image(systemName: item.icon)
                                    .resizable()
                                    .aspectRatio(contentMode: .fit)
                                    .frame(width: 16, height: 16, alignment: .center)
                                    .foregroundColor(self.selectedList == item ? Color.white : Color.accentColor)
                                Text(item.name)
                                    .foregroundColor(self.selectedList == item ? Color.white : nil)
                            }
                            .font(.system(size: 13, weight: .medium, design: .rounded))
                            .foregroundColor(colorScheme == .dark ? .white : .black)
                            .padding(.top, 5)
                            .padding(.bottom, 5)
                       
<details>
<summary>英文:</summary>
I am putting a grid view together in the details section of a navigation view. I would like to be able to to have a selection state on individual grid items as the user clicks them, similar to the way the List works. I don't want multiple selections, so toggle doesn't work. I would also like to customise the selection style.
```import SwiftUI
struct Tutorials: Identifiable, Hashable {
    var id = UUID()
    let name: String
    let icon: String
}
struct ImageItems: Identifiable, Hashable {
    var id = UUID()
    let name: String
    let icon: String
}
let allLibraries = [Tutorials(name: "All Library Items", icon: "house")]
let lessons = [Tutorials(name: "Bathroom", icon: "bathtub"),
                   Tutorials(name: "Bedroom", icon: "bed.double"),
                   Tutorials(name: "Dining Room", icon: "table.furniture"),
                   Tutorials(name: "Doors & Windows", icon: "door.left.hand.open"),
               Tutorials(name: "Kitchen", icon: "cooktop"),
               Tutorials(name: "Lighting", icon: "lamp.table"),
               Tutorials(name: "Living Room", icon: "sofa"),
               Tutorials(name: "Miscellaneous", icon: "ellipsis.circle"),
               Tutorials(name: "Office & Study", icon: "printer"),
               Tutorials(name: "Stairs", icon: "stairs"),
               Tutorials(name: "Utility Room", icon: "washer")]
let videos = [Tutorials(name: "Decoration", icon: "paintbrush"),
               Tutorials(name: "Furniture", icon: "chair"),
               Tutorials(name: "Miscellaneous", icon: "ellipsis.circle"),
              Tutorials(name: "Plants & Trees", icon: "tree"),
              Tutorials(name: "Ponds & Pools", icon: "figure.open.water.swim"),
              Tutorials(name: "Structures", icon: "door.garage.closed")]
let imageGrid = [ImageItems(name: "Decoration", icon: "paintbrush"),
              ImageItems(name: "Furniture", icon: "chair"),
              ImageItems(name: "Miscellaneous", icon: "ellipsis.circle"),
              ImageItems(name: "Plants & Trees", icon: "tree"),
              ImageItems(name: "Ponds & Pools", icon: "figure.open.water.swim"),
              ImageItems(name: "Structures", icon: "door.garage.closed"),
              ImageItems(name: "Furniture", icon: "chair"),
              ImageItems(name: "Miscellaneous", icon: "ellipsis.circle"),
              ImageItems(name: "Plants & Trees", icon: "tree"),
              ImageItems(name: "Ponds & Pools", icon: "figure.open.water")]
struct MyDisclosureStyle: DisclosureGroupStyle {
    func makeBody(configuration: Configuration) -> some View {
            Button {
                withAnimation {
                    configuration.isExpanded.toggle()
                }
            } label: {
                VStack {
                    Spacer()
                        .frame(height: 15)
                    HStack(alignment: .firstTextBaseline) {
                        configuration.label
                        Spacer()
                        Image(systemName:"chevron.right")
                            .rotationEffect(.degrees(configuration.isExpanded ? 90 : 0))
                    }
                    .padding(.top, 0)
                    .padding(.bottom, 0)
                    .contentShape(Rectangle())
                    .onTapGesture {
                        withAnimation {
                            configuration.isExpanded.toggle()
                        }
                    }
                    Spacer()
                        .frame(height: 8)
                }
            }
            .buttonStyle(.plain)
        if configuration.isExpanded {
                    configuration.content
                    //indents the disclosure content
                        .padding(.leading, 0)
                        .padding(.top, 0)
                        .padding(.bottom, 0)
                        .disclosureGroupStyle(self)
                        
                }
    }
}
struct ContentView: View {
    
    @Environment(\.colorScheme) var colorScheme
    
    @State private var selectedList:Tutorials? = allLibraries[0]
    
    @State var selectedImage:ImageItems? = imageGrid[0]
    
    @State var expand = true
    @State var expand2 = true
    @State var expand3 = true
    @State private var isHidden = false
        
    var body: some View {
        NavigationSplitView {
            
            VStack(alignment: .leading) {
                List(selection:$selectedList) {
                        ForEach(allLibraries, id: \.self) { item in
                            HStack {
                                Image(systemName:item.icon)
                                    .frame(width:16, height:16, alignment: .center)
                                    .foregroundColor(self.selectedList == item ? Color.white : Color.accentColor)
                                Text(item.name)
                                    .foregroundColor(self.selectedList == item ? Color.white : nil)
                            }
                            .font(.system(size: 13,weight: .medium, design: .rounded))
                            .foregroundColor(colorScheme == .dark ? .white : .black)
                            .padding(0)
                        }
                        DisclosureGroup("Home Plan Graphics", isExpanded: $expand) {
                            ForEach(lessons, id: \.self) { item in
                                HStack {
                                    Image(systemName:item.icon)
                                        .frame(width:16, height:16, alignment: .center)
                                        .foregroundColor(self.selectedList == item ? Color.white : Color.accentColor)
                                    Text(item.name)
                                        .foregroundColor(self.selectedList == item ? Color.white : nil)
                                }
                                .font(.system(size: 13,weight: .medium, design: .rounded))
                                .foregroundColor(colorScheme == .dark ? .white : .black)
                                .padding(.top, 5)
                                .padding(.bottom, 5)
                            }
                            
                        }
                        .listRowInsets(EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 0))
                        .font(.system(size: 12, weight: .medium, design: .rounded))
                        .foregroundColor(.secondary)
                        .disclosureGroupStyle(MyDisclosureStyle())
                    
                    DisclosureGroup("Garden Plan Graphics", isExpanded: $expand2) {
                        ForEach(videos, id: \.self) { item in
                                HStack {
                                    Image(systemName:item.icon)
                                        .resizable()
                                        .aspectRatio(contentMode: .fit)
                                        .frame(width:16, height:16, alignment: .center)
                                        .foregroundColor(self.selectedList == item ? Color.white : Color.accentColor)
                                    Text(item.name)
                                        .foregroundColor(self.selectedList == item ? Color.white : nil)
                                }
                                .font(.system(size: 13, weight: .medium, design: .rounded))
                                .foregroundColor(colorScheme == .dark ? .white : .black)
                                .padding(.top, 5)
                                .padding(.bottom, 5)
                        }
                    }
                    .listRowInsets(EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 0))
                    .font(.system(size: 12, weight: .medium, design: .rounded))
                    .foregroundColor(.secondary)
                    .disclosureGroupStyle(MyDisclosureStyle())
                    
                }
            }
            .navigationSplitViewColumnWidth(min: 250, ideal: 250, max: 400)
            .padding([.leading, .trailing], 5)
        }
        
        detail: {
            
            let columns = Array(
                repeating: GridItem.init(.adaptive(minimum: 120), spacing: 20),
                count: 1)
            ScrollView {
                LazyVGrid(columns: columns, spacing: 20) {
                    
                    ForEach(imageGrid, id: \.self) { item in
                            GridItemView(item: item)
                        }
                    }
                    .padding()
                }
                .background(colorScheme == .dark ? nil : Color.white)
        }
        
    }
}
struct GridItemView: View {
    
    @Environment(\.colorScheme) var colorScheme
    
    let item: ImageItems
    
    var body: some View {
        GeometryReader { reader in
            VStack(spacing: 5) {
                Image(systemName: item.icon)
                    .resizable()
                    .aspectRatio(contentMode: .fit)
                    .frame(width: 40, height:40, alignment: .center)
                    .foregroundColor(colorScheme == .dark ? nil : .black)
                    .fontWeight(.light)
                Text(item.name)
                    .font(.system(size: 13, weight: .medium, design: .rounded))
                    .foregroundColor(colorScheme == .dark ? nil : .black)
            }
            .frame(width: reader.size.width, height: reader.size.height)
            .background(colorScheme == .dark ? nil : Color.white)
        }
        .frame(height:100)
        .overlay(
            RoundedRectangle(cornerRadius: 10)
                .stroke(colorScheme == .dark ? .white.opacity(0.2) : .black.opacity(0.2), lineWidth: 2)
        )
        
        
        
    }
    
}
struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}
答案1
得分: 1
首先,请将您的结构体命名为单数形式,所选图像是一个名为 ImageItem 的对象,由于有一个默认值,因此无需将其设置为可选的。
@State private var selectedImage: ImageItem = imageGrid[0]
结构体中的 id 成员应为常量 (let)。
LazyVGrid 不支持选择,但在实践中,单个选择只是一种特殊的单元格样式。
我的建议是在 GridItemView 中添加一个点击手势,将 selectedImage 设置为所点击的网格项。此外,向 GridItemView 添加一个 isSelected 属性,并根据其状态来设置项的样式。
LazyVGrid(columns: columns, spacing: 20) {
     ForEach(imageGrid, id: \.self) { item in
         GridItemView(item: item, isSelected: item == selectedImage)
            .onTapGesture {
                selectedImage = item
            }
         }
     }
     .padding()
}
英文:
First of all please name your structs in singular form, the selected image is one ImageItem and as there is a default value there is no need to make it optional
@State private var selectedImage: ImageItem = imageGrid[0]
And the id members in the structs should be constants (let).
LazyVGrid doesn't support selection, but in practice a single selection is just a special cell style.
My suggestion is to add a tap gesture to the GridItemView which sets selectedImage to the tapped grid item. Further add an isSelected property to GridItemView and style the item depending on the state
LazyVGrid(columns: columns, spacing: 20) {
ForEach(imageGrid, id: \.self) { item in
GridItemView(item: item, isSelected: item == selectedImage)
.onTapGesture {
selectedImage = item
}
}
}
.padding()
}
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。


评论