Created
March 27, 2023 16:30
-
-
Save SimplyKyra/5cf6ec5da50cb937854facc572a95b82 to your computer and use it in GitHub Desktop.
Custom SwiftUI multipicker that allows custom list with image and multiple selection. Learn more about it here: https://www.simplykyra.com/update-to-my-custom-picker-with-multi-selection-in-swiftui-now-with-images/
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// | |
// ContentView.swift | |
// Shared | |
// | |
// Created by Kyra on 2/2/22. | |
// | |
import SwiftUI | |
struct myItem: Hashable, Identifiable { | |
var id: Int | |
var name:String | |
var imageName:String | |
} | |
struct ContentView: View { | |
@State var selectedItems = [myItem]() | |
@State var allItems:[myItem] = [ | |
myItem(id: 0, name: "smile", imageName: "face.smiling"), | |
myItem(id: 1, name: "heart", imageName: "heart"), | |
myItem(id: 2, name: "music", imageName: "music.quarternote.3"), | |
myItem(id: 3, name: "scribble", imageName: "scribble"), | |
myItem(id: 4, name: "fire", imageName: "flame"), | |
myItem(id: 5, name: "piano", imageName: "pianokeys"), | |
myItem(id: 6, name: "puzzle", imageName: "puzzlepiece"), | |
] | |
var body: some View { | |
#if os(macOS) | |
macOSview(selectedItems: selectedItems, allItems: allItems) | |
#else //iOS | |
iOSview(selectedItems: selectedItems, allItems: allItems) | |
#endif | |
} | |
} | |
// This View is functional on macOS and iOS; however, I prefer | |
// using a NavigationalLink for iOS rather than the popover so | |
// setting this one to macOS for this example | |
struct macOSview: View { | |
@State var selectedItems:[myItem] | |
@State var allItems:[myItem] | |
// Needed to show the popover | |
@State var showingPopover:Bool = false | |
var body: some View { | |
Form { | |
HStack() { | |
// Rather than a picker we're using Text for the label | |
// and a button for the picker itself | |
Text("Select Items:") | |
.foregroundColor(.white) | |
Button(action: { | |
// The only job of the button is to toggle the showing | |
// popover boolean so it pops up and we can select our items | |
showingPopover.toggle() | |
}) { | |
HStack { | |
Spacer() | |
Image(systemName: "\($selectedItems.count).circle") | |
.font(.title2) | |
Image(systemName: "chevron.right") | |
.font(.caption) | |
} | |
.foregroundColor(Color(red: 0.4192, green: 0.2358, blue: 0.3450)) | |
} | |
.popover(isPresented: $showingPopover) { | |
MultiSelectPickerView(allItems: allItems, selectedItems: $selectedItems) | |
// If you have issues with it being too skinny you can hardcode the width | |
.frame(width: 300) | |
} | |
} | |
// Made a quick text section so we can see what we selected | |
Text("My selected items are:") | |
.foregroundColor(.white) | |
if selectedItems.count > 0 { | |
ForEach(selectedItems) { item in | |
HStack { | |
Text("\t*\(item.name) - ") | |
Image(systemName: item.imageName) | |
} | |
} | |
// Text("\t* \(selectedItems.joined(separator: "\n\t* "))") | |
.foregroundColor(.white) | |
} | |
} | |
.padding() | |
.background(Color(red: 0.4192, green: 0.2358, blue: 0.3450)) | |
.navigationTitle("My Items") | |
} | |
} | |
// The iOS version uses a NavigationLink that requires being | |
// in a NavigationView to work so I chose to move the entire | |
// thing into a new View. | |
struct iOSview:View { | |
@State var selectedItems:[myItem] | |
@State var allItems:[myItem] | |
var body: some View { | |
NavigationView { | |
Form { | |
// Since this is for iOS I included sections | |
Section("Choose your items:", content: { | |
// Rather than a button we're using a NavigationLink but passing | |
// in the same destination | |
NavigationLink(destination: { | |
MultiSelectPickerView(allItems: allItems, selectedItems: $selectedItems) | |
.navigationTitle("Choose Your Items") | |
}, label: { | |
// And then the label and dynamic number are displayed in the label. | |
// We don't need to include the chevron as it's done for us in the link | |
HStack { | |
Text("Select Items:") | |
.foregroundColor(Color(red: 0.4192, green: 0.2358, blue: 0.3450)) | |
Spacer() | |
Image(systemName: "\($selectedItems.count).circle") | |
.foregroundColor(Color(red: 0.4192, green: 0.2358, blue: 0.3450)) | |
.font(.title2) | |
} | |
}) | |
}) | |
// Made a quick text section so we can see what we selected | |
Section("My selected items are:", content: { | |
ForEach(selectedItems) { item in | |
HStack { | |
Text("\t* \(item.name)") | |
Spacer() | |
Image(systemName: item.imageName) | |
Spacer() | |
} | |
.foregroundColor(Color(red: 0.4192, green: 0.2358, blue: 0.3450)) | |
} | |
}) | |
} | |
.navigationTitle("My Items") | |
} | |
} | |
} | |
// The struct that the custom picker (button) opens which | |
// is minorly adapted from: | |
// https://gist.github.com/dippnerd/5841898c2cf945994ba85871446329fa | |
struct MultiSelectPickerView: View { | |
// The list of items we want to show | |
@State var allItems: [myItem] | |
// Binding to the selected items we want to track | |
@Binding var selectedItems: [myItem] | |
var body: some View { | |
Form { | |
List { | |
ForEach(allItems, id: \.self) { item in | |
Button(action: { | |
withAnimation { | |
if self.selectedItems.contains(item) { | |
// Previous comment: you may need to adapt this piece | |
self.selectedItems.removeAll(where: { $0 == item }) | |
} else { | |
self.selectedItems.append(item) | |
} | |
} | |
}) { | |
HStack { | |
Image(systemName: "checkmark") | |
.opacity(self.selectedItems.contains(item) ? 1.0 : 0.0) | |
Image(systemName: item.imageName) | |
} | |
} | |
.foregroundColor(.primary) | |
} | |
} | |
} | |
} | |
} | |
struct ContentView_Previews: PreviewProvider { | |
static var previews: some View { | |
ContentView() | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment