Skip to content

Commit e9b3227

Browse files
authored
Add Tag API support (#584)
1 parent b5860b3 commit e9b3227

File tree

1 file changed

+270
-0
lines changed

1 file changed

+270
-0
lines changed
Lines changed: 270 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,270 @@
1+
//
2+
// Tag.swift
3+
// OpenSwiftUICore
4+
//
5+
// Audited for 6.5.4
6+
// Status: Complete
7+
// ID: 0F8CE0FEFF8003CACFB16F1C88624A9F (SwiftUICore)
8+
9+
// MARK: - View + Tag
10+
11+
@available(OpenSwiftUI_v1_0, *)
12+
extension View {
13+
14+
/// Sets the unique tag value of this view.
15+
///
16+
/// Use this modifier to differentiate among certain selectable views,
17+
/// like the possible values of a ``Picker`` or the tabs of a ``TabView``.
18+
/// Tag values can be of any type that conforms to the
19+
/// [Hashable](https://developer.apple.com/documentation/swift/hashable) protocol.
20+
///
21+
/// This modifier will write the tag value for the type `V`, as well as
22+
/// `Optional<V>` if `includeOptional` is enabled. Containers checking for
23+
/// tags of either type will see the value as set.
24+
///
25+
/// In the example below, the ``ForEach`` loop in the ``Picker`` view
26+
/// builder iterates over the `Flavor` enumeration. It extracts the string
27+
/// value of each enumeration element for use in constructing the row
28+
/// label, and uses the enumeration value as input to the `tag(_:)`
29+
/// modifier.
30+
///
31+
/// struct FlavorPicker: View {
32+
/// enum Flavor: String, CaseIterable, Identifiable {
33+
/// case chocolate, vanilla, strawberry
34+
/// var id: Self { self }
35+
/// }
36+
///
37+
/// @State private var selectedFlavor: Flavor? = nil
38+
///
39+
/// var body: some View {
40+
/// Picker("Flavor", selection: $selectedFlavor) {
41+
/// ForEach(Flavor.allCases) { flavor in
42+
/// Text(flavor.rawValue)
43+
/// .tag(flavor)
44+
/// }
45+
/// }
46+
/// }
47+
/// }
48+
///
49+
/// The selection type of the ``Picker`` is an `Optional<Flavor>` and so it
50+
/// will look for tags on its contents of `Optional<Flavor>`type. Since the
51+
/// tag modifier defaults to having `includeOptional` enabled, even though
52+
/// the tag for each option is a non-optional `Flavor`, the tag modifier
53+
/// writes values for both the non-optional, and optional versions of the
54+
/// value, allowing the contents to be selectable by the ``Picker``.
55+
///
56+
/// A ``ForEach`` automatically applies a default tag to each enumerated
57+
/// view using the `id` parameter of the corresponding element. If
58+
/// the element's `id` parameter and the picker's `selection` input
59+
/// have exactly the same type, or the same type but optional, you can omit
60+
/// the explicit tag modifier.
61+
///
62+
/// To see examples that don't require an explicit tag, see ``Picker``.
63+
///
64+
/// - Parameter tag: A [Hashable](https://developer.apple.com/documentation/swift/hashable)
65+
/// value to use as the view's tag.
66+
/// - Parameter includeOptional: If the tag value for `Optional<V>` should
67+
/// also be set.
68+
///
69+
/// - Returns: A view with the specified tag set.
70+
@_alwaysEmitIntoClient
71+
nonisolated public func tag<V>(_ tag: V, includeOptional: Bool = true) -> some View where V: Hashable {
72+
_trait(TagValueTraitKey<V>.self, .tagged(tag))
73+
._trait(
74+
TagValueTraitKey<V?>.self,
75+
includeOptional ? .tagged(Optional(tag)) : .untagged
76+
)
77+
}
78+
79+
/// Sets the view as acting as explicit untagged / auxiliary content that
80+
/// will not be wrapped by container views.
81+
///
82+
/// For example, `Picker` treats its contents as option button labels.
83+
/// A view that is marked as `untagged()` will result
84+
/// in the view not being considered an option, and just an extra element
85+
/// in the picker.
86+
@inlinable
87+
nonisolated public func _untagged() -> some View {
88+
_trait(IsAuxiliaryContentTraitKey.self, true)
89+
}
90+
91+
@usableFromInline
92+
@MainActor
93+
@preconcurrency
94+
func tag<V>(_ tag: V) -> some View where V: Hashable {
95+
_trait(TagValueTraitKey<V>.self, .tagged(tag))
96+
}
97+
}
98+
99+
// MARK: - TagValueTraitKey
100+
101+
@available(OpenSwiftUI_v1_0, *)
102+
@usableFromInline
103+
package struct TagValueTraitKey<V>: _ViewTraitKey where V: Hashable {
104+
@usableFromInline
105+
@frozen
106+
package enum Value {
107+
case untagged
108+
case tagged(V)
109+
}
110+
111+
@inlinable
112+
package static var defaultValue: TagValueTraitKey<V>.Value {
113+
.untagged
114+
}
115+
}
116+
117+
@available(*, unavailable)
118+
extension TagValueTraitKey.Value: Sendable {}
119+
120+
@available(*, unavailable)
121+
extension TagValueTraitKey: Sendable {}
122+
123+
// MARK: - IsAuxiliaryContentTraitKey
124+
125+
@available(OpenSwiftUI_v1_0, *)
126+
@usableFromInline
127+
package struct IsAuxiliaryContentTraitKey: _ViewTraitKey {
128+
@inlinable
129+
package static var defaultValue: Bool {
130+
false
131+
}
132+
}
133+
134+
@available(*, unavailable)
135+
extension IsAuxiliaryContentTraitKey: Sendable {}
136+
137+
extension ViewTraitCollection {
138+
package var isAuxiliaryContent: Bool {
139+
get { self[IsAuxiliaryContentTraitKey.self] }
140+
set { self[IsAuxiliaryContentTraitKey.self] = newValue }
141+
}
142+
}
143+
144+
// MARK: - ViewTraitCollection + Tag
145+
146+
extension ViewTraitCollection {
147+
package func tagValue<V>(for type: V.Type) -> V? where V: Hashable {
148+
let value = self[TagValueTraitKey<V>.self]
149+
return switch value {
150+
case let .tagged(tag): tag
151+
case .untagged: nil
152+
}
153+
}
154+
155+
package func tag<V>(for type: V.Type) -> V? where V: Hashable {
156+
let value = self[TagValueTraitKey<V>.self]
157+
return switch value {
158+
case let .tagged(tag): isAuxiliaryContent ? nil : tag
159+
case .untagged: nil
160+
}
161+
}
162+
163+
package mutating func setTagIfUnset<V>(for type: V.Type, value: V) where V: Hashable {
164+
setValueIfUnset(.tagged(value), for: TagValueTraitKey<V>.self)
165+
}
166+
167+
package mutating func setTag<V>(for type: V.Type, value: V) where V: Hashable {
168+
self[TagValueTraitKey<V>.self] = .tagged(value)
169+
}
170+
}
171+
172+
// MARK: - Binding + Tag
173+
174+
extension Binding {
175+
package func selecting(_ tag: Value?) -> Binding<Bool> where Value: Hashable {
176+
guard let tag else {
177+
return .false
178+
}
179+
return self == tag
180+
}
181+
}
182+
183+
extension Binding where Value: Hashable {
184+
package func projectingTagIndex(viewList: any ViewList) -> Binding<Int?> {
185+
projecting(TagIndexProjection<Value>(list: viewList))
186+
}
187+
}
188+
189+
// MARK: - TagIndexProjection
190+
191+
private class TagIndexProjection<Value>: Projection where Value: Hashable {
192+
let list: any ViewList
193+
var nextIndex: Int? = .zero
194+
var indexMap: [Int: Value] = [:]
195+
var tagMap: [Value: Int] = [:]
196+
197+
init(list: any ViewList) {
198+
self.list = list
199+
}
200+
201+
func get(base: Value) -> Int? {
202+
if let index = tagMap[base] {
203+
return index
204+
} else {
205+
var i: Int? = nil
206+
readUntil { index, value in
207+
let result = value == base
208+
if result {
209+
i = index
210+
}
211+
return result
212+
}
213+
return i
214+
}
215+
}
216+
217+
func set(base: inout Value, newValue: Int?) {
218+
guard let newValue else {
219+
return
220+
}
221+
if let tag = indexMap[newValue] {
222+
base = tag
223+
} else {
224+
readUntil { index, value in
225+
let result = newValue == index
226+
if result {
227+
base = value
228+
}
229+
return result
230+
}
231+
}
232+
}
233+
234+
func readUntil(_ body: (Int, Value) -> Bool) {
235+
guard var nextIndex else {
236+
return
237+
}
238+
var index = nextIndex
239+
let result = list.applySublists(
240+
from: &index,
241+
list: nil
242+
) { sublist in
243+
nextIndex &-= sublist.start
244+
defer { nextIndex &+= sublist.count }
245+
let traits = sublist.traits
246+
guard let tag = traits.tag(for: Value.self) else {
247+
return true
248+
}
249+
tagMap[tag] = nextIndex
250+
var index = nextIndex
251+
var count = list.count
252+
Swift.precondition(index + count >= index)
253+
while count != 0 {
254+
indexMap[index] = tag
255+
index &+= 1
256+
count &-= 1
257+
}
258+
return !body(nextIndex, tag)
259+
}
260+
self.nextIndex = result ? nil : nextIndex
261+
}
262+
263+
func hash(into hasher: inout Hasher) {
264+
hasher.combine(ObjectIdentifier(self))
265+
}
266+
267+
static func == (lhs: TagIndexProjection<Value>, rhs: TagIndexProjection<Value>) -> Bool {
268+
lhs === rhs
269+
}
270+
}

0 commit comments

Comments
 (0)