-
-
Notifications
You must be signed in to change notification settings - Fork 324
/
Copy pathSelectorView.swift
135 lines (116 loc) · 3.9 KB
/
SelectorView.swift
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
//
// Swiftfin is subject to the terms of the Mozilla Public
// License, v2.0. If a copy of the MPL was not distributed with this
// file, you can obtain one at https://mozilla.org/MPL/2.0/.
//
// Copyright (c) 2025 Jellyfin & Jellyfin Contributors
//
import Defaults
import SwiftUI
// TODO: Label generic not really necessary if just restricting to `Text`
// - go back to `any View` implementation instead
enum SelectorType {
case single
case multi
}
struct SelectorView<Element: Displayable & Hashable, Label: View>: View {
@Default(.accentColor)
private var accentColor
@State
private var selectedItems: Set<Element>
private let selectionBinding: Binding<Set<Element>>
private let sources: [Element]
private var label: (Element) -> Label
private let type: SelectorType
private init(
selection: Binding<Set<Element>>,
sources: [Element],
label: @escaping (Element) -> Label,
type: SelectorType
) {
self.selectionBinding = selection
self._selectedItems = State(initialValue: selection.wrappedValue)
self.sources = sources
self.label = label
self.type = type
}
var body: some View {
List(sources, id: \.hashValue) { element in
Button {
switch type {
case .single:
handleSingleSelect(with: element)
case .multi:
handleMultiSelect(with: element)
}
} label: {
HStack {
label(element)
Spacer()
if selectedItems.contains(element) {
Image(systemName: "checkmark.circle.fill")
.resizable()
.backport
.fontWeight(.bold)
.aspectRatio(1, contentMode: .fit)
.frame(width: 36, height: 36)
.symbolRenderingMode(.palette)
.foregroundStyle(accentColor.overlayColor, accentColor)
}
}
}
}
.onChange(of: selectionBinding.wrappedValue) { newValue in
selectedItems = newValue
}
}
private func handleSingleSelect(with element: Element) {
selectedItems = [element]
selectionBinding.wrappedValue = selectedItems
}
private func handleMultiSelect(with element: Element) {
if selectedItems.contains(element) {
selectedItems.remove(element)
} else {
selectedItems.insert(element)
}
selectionBinding.wrappedValue = selectedItems
}
}
extension SelectorView where Label == Text {
init(selection: Binding<[Element]>, sources: [Element], type: SelectorType) {
let setBinding = Binding<Set<Element>>(
get: { Set(selection.wrappedValue) },
set: { newValue in
selection.wrappedValue = Array(newValue)
}
)
self.init(
selection: setBinding,
sources: sources,
label: { Text($0.displayTitle).foregroundColor(.primary) },
type: type
)
}
init(selection: Binding<Element>, sources: [Element]) {
let setBinding = Binding<Set<Element>>(
get: { Set([selection.wrappedValue]) },
set: { newValue in
if let first = newValue.first {
selection.wrappedValue = first
}
}
)
self.init(
selection: setBinding,
sources: sources,
label: { Text($0.displayTitle).foregroundColor(.primary) },
type: .single
)
}
}
extension SelectorView {
func label(@ViewBuilder _ content: @escaping (Element) -> Label) -> Self {
copy(modifying: \.label, with: content)
}
}