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.
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()
}
}
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()
}