Skip to content

Instantly share code, notes, and snippets.

@mattyoung
Last active March 18, 2021 20:15
Show Gist options
  • Save mattyoung/e6f9bf8391f16f06b1929d8d7fac2211 to your computer and use it in GitHub Desktop.
Save mattyoung/e6f9bf8391f16f06b1929d8d7fac2211 to your computer and use it in GitHub Desktop.
import SwiftUI
protocol Selectable: Identifiable {
associatedtype Label : View
var isSelected: Bool { get set }
var label: Label { get }
}
struct Multiselect<T: Selectable>: View {
@Binding var items: [T]
var body: some View {
List {
ForEach($items) { index, item in
Toggle(isOn: item.isSelected) {
item.wrappedValue.label
}
}
}
}
}
// MARK: custom toggle styles
// ==========================
struct CheckboxToggleStyle: ToggleStyle {
@Environment(\.isEnabled) var isEnabled
func makeBody(configuration: Configuration) -> some View {
Button(action: { configuration.isOn.toggle() }){
HStack {
configuration.label
.frame(maxWidth: .infinity, alignment: .leading)
Image(systemName: configuration.isOn ? "checkmark.square" : "square")
.resizable()
.frame(width: 18, height: 18)
}
}
.foregroundColor(isEnabled ? .accentColor : .gray)
}
}
struct CheckmarkToggleStyle: ToggleStyle {
@Environment(\.isEnabled) var isEnabled
func makeBody(configuration: Configuration) -> some View {
Button(action: { withAnimation { configuration.isOn.toggle() } }){
HStack {
configuration.label
.foregroundColor(.primary)
.frame(maxWidth: .infinity, alignment: .leading)
if configuration.isOn {
Image(systemName: "checkmark")
.foregroundColor(.accentColor)
.animation(.default)
.transition(.move(edge: .trailing).combined(with: .opacity))
}
}
}
}
}
// MARK: demo example
// ==================
struct StringSelectable: Selectable {
let name: String
var isSelected = false
var label: some View { Text(name) }
var id: String { name }
}
struct MultiselectDemo: View {
@State var fruits = [
StringSelectable(name: "Apple", isSelected: true),
StringSelectable(name: "Banana"),
StringSelectable(name: "Kumquat", isSelected: true),
]
var body: some View {
Form {
Text("Number selected: \(fruits.filter { $0.isSelected }.count)")
Section(header: Text("Juicy Fruits")) {
Multiselect(items: $fruits)
.toggleStyle(CheckmarkToggleStyle())
}
Section(header: Text("Sour")) {
Multiselect(items: $fruits)
}
Section(header: Text("Bitter")) {
Multiselect(items: $fruits)
.toggleStyle(CheckboxToggleStyle())
}
}
}
}
struct MultiselectDemo_Previews: PreviewProvider {
static var previews: some View {
MultiselectDemo()
}
}
// see: https://swiftbysundell.com/articles/bindable-swiftui-list-elements/
struct IdentifiableIndices<Base> where Base: RandomAccessCollection, Base.Element: Identifiable {
typealias Index = Base.Index
struct Element: Identifiable {
let id: Base.Element.ID
let rawValue: Index
func callAsFunction() -> Index { rawValue }
}
fileprivate var base: Base
}
extension IdentifiableIndices: RandomAccessCollection {
var startIndex: Index { base.startIndex }
var endIndex: Index { base.endIndex }
subscript(position: Index) -> Element {
Element(id: base[position].id, rawValue: position)
}
func index(before index: Index) -> Index {
base.index(before: index)
}
func index(after index: Index) -> Index {
base.index(after: index)
}
}
extension ForEach where ID == Data.Element.ID, Data.Element: Identifiable, Content: View {
init<T>(_ data: Binding<T>, @ViewBuilder content: @escaping (T.Index, Binding<T.Element>) -> Content) where Data == IdentifiableIndices<T>, T: MutableCollection {
self.init(IdentifiableIndices(base: data.wrappedValue)) { index in
content(
index(),
Binding(
get: { data.wrappedValue[index()] },
set: { data.wrappedValue[index()] = $0 }
)
)
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment