@@ -9,75 +9,90 @@ import UIKit
9
9
/// containing the ``View/keyboardToolbar(animateChanges:body:)`` modifier is updated, so any
10
10
/// state necessary for the toolbar should live in the view itself.
11
11
public protocol ToolbarItem {
12
- /// Convert the item to a `UIBarButtonItem`, which will be placed in the keyboard toolbar.
13
- func asBarButtonItem( ) -> UIBarButtonItem
12
+ /// The type of bar button item used to represent this item in UIKit.
13
+ associatedtype ItemType : UIBarButtonItem
14
+
15
+ /// Convert the item to an instance of `ItemType`.
16
+ func createBarButtonItem( ) -> ItemType
17
+
18
+ /// Update the item with new information (e.g. updated bindings). May be a no-op.
19
+ func updateBarButtonItem( _ item: inout ItemType )
14
20
}
15
21
16
22
@resultBuilder
17
23
public enum ToolbarBuilder {
18
- public typealias Component = [ any ToolbarItem ]
19
-
20
- public static func buildExpression( _ expression: some ToolbarItem ) -> Component {
21
- [ expression]
24
+ public enum Component {
25
+ case expression( any ToolbarItem )
26
+ case block( [ Component ] )
27
+ case array( [ Component ] )
28
+ indirect case optional( Component ? )
29
+ indirect case eitherFirst( Component )
30
+ indirect case eitherSecond( Component )
22
31
}
32
+ public typealias FinalResult = Component
23
33
24
34
public static func buildExpression( _ expression: any ToolbarItem ) -> Component {
25
- [ expression]
35
+ . expression( expression )
26
36
}
27
37
28
38
public static func buildBlock( _ components: Component ... ) -> Component {
29
- components . flatMap { $0 }
39
+ . block ( components )
30
40
}
31
41
32
42
public static func buildArray( _ components: [ Component ] ) -> Component {
33
- components . flatMap { $0 }
43
+ . array ( components )
34
44
}
35
45
36
46
public static func buildOptional( _ component: Component ? ) -> Component {
37
- component ?? [ ]
47
+ . optional ( component)
38
48
}
39
49
40
50
public static func buildEither( first component: Component ) -> Component {
41
- component
51
+ . eitherFirst ( component)
42
52
}
43
53
44
54
public static func buildEither( second component: Component ) -> Component {
45
- component
55
+ . eitherSecond ( component)
46
56
}
47
57
}
48
58
49
- final class CallbackBarButtonItem : UIBarButtonItem {
50
- private var callback : ( ) -> Void
59
+ extension Button : ToolbarItem {
60
+ public final class ItemType : UIBarButtonItem {
61
+ var callback : ( ) -> Void
51
62
52
- init ( title: String , callback: @escaping ( ) -> Void ) {
53
- self . callback = callback
54
- super. init ( )
63
+ init ( title: String , callback: @escaping ( ) -> Void ) {
64
+ self . callback = callback
65
+ super. init ( )
55
66
56
- self . title = title
57
- self . target = self
58
- self . action = #selector( onTap)
59
- }
67
+ self . title = title
68
+ self . target = self
69
+ self . action = #selector( onTap)
70
+ }
71
+
72
+ @available ( * , unavailable)
73
+ required init ? ( coder: NSCoder ) {
74
+ fatalError ( " init(coder:) is not used for this item " )
75
+ }
60
76
61
- @available ( * , unavailable)
62
- required init ? ( coder: NSCoder ) {
63
- fatalError ( " init(coder:) is not used for this item " )
77
+ @objc
78
+ func onTap( ) {
79
+ callback ( )
80
+ }
64
81
}
65
82
66
- @objc
67
- func onTap( ) {
68
- callback ( )
83
+ public func createBarButtonItem( ) -> ItemType {
84
+ ItemType ( title: label, callback: action)
69
85
}
70
- }
71
86
72
- extension Button : ToolbarItem {
73
- public func asBarButtonItem ( ) -> UIBarButtonItem {
74
- CallbackBarButtonItem ( title: label , callback : action )
87
+ public func updateBarButtonItem ( _ item : inout ItemType ) {
88
+ item . callback = action
89
+ item . title = label
75
90
}
76
91
}
77
92
78
93
@available ( iOS 14 , macCatalyst 14 , tvOS 14 , * )
79
94
extension Spacer : ToolbarItem {
80
- public func asBarButtonItem ( ) -> UIBarButtonItem {
95
+ public func createBarButtonItem ( ) -> UIBarButtonItem {
81
96
if let minLength, minLength > 0 {
82
97
print (
83
98
"""
@@ -89,44 +104,64 @@ extension Spacer: ToolbarItem {
89
104
}
90
105
return . flexibleSpace( )
91
106
}
107
+
108
+ public func updateBarButtonItem( _: inout UIBarButtonItem ) {
109
+ // no-op
110
+ }
92
111
}
93
112
94
113
struct FixedWidthToolbarItem < Base: ToolbarItem > : ToolbarItem {
95
114
var base : Base
96
115
var width : Int ?
97
116
98
- func asBarButtonItem ( ) -> UIBarButtonItem {
99
- let item = base. asBarButtonItem ( )
117
+ func createBarButtonItem ( ) -> Base . ItemType {
118
+ let item = base. createBarButtonItem ( )
100
119
if let width {
101
120
item. width = CGFloat ( width)
102
121
}
103
122
return item
104
123
}
124
+
125
+ func updateBarButtonItem( _ item: inout Base . ItemType ) {
126
+ base. updateBarButtonItem ( & item)
127
+ if let width {
128
+ item. width = CGFloat ( width)
129
+ }
130
+ }
105
131
}
106
132
107
133
// Setting width on a flexible space is ignored, you must use a fixed space from the outset
108
134
@available ( iOS 14 , macCatalyst 14 , tvOS 14 , * )
109
135
struct FixedWidthSpacerItem : ToolbarItem {
110
136
var width : Int ?
111
137
112
- func asBarButtonItem ( ) -> UIBarButtonItem {
138
+ func createBarButtonItem ( ) -> UIBarButtonItem {
113
139
if let width {
114
140
. fixedSpace( CGFloat ( width) )
115
141
} else {
116
142
. flexibleSpace( )
117
143
}
118
144
}
145
+
146
+ func updateBarButtonItem( _ item: inout UIBarButtonItem ) {
147
+ item = createBarButtonItem ( )
148
+ }
119
149
}
120
150
121
151
struct ColoredToolbarItem < Base: ToolbarItem > : ToolbarItem {
122
152
var base : Base
123
153
var color : Color
124
154
125
- func asBarButtonItem ( ) -> UIBarButtonItem {
126
- let item = base. asBarButtonItem ( )
155
+ func createBarButtonItem ( ) -> Base . ItemType {
156
+ let item = base. createBarButtonItem ( )
127
157
item. tintColor = color. uiColor
128
158
return item
129
159
}
160
+
161
+ func updateBarButtonItem( _ item: inout Base . ItemType ) {
162
+ base. updateBarButtonItem ( & item)
163
+ item. tintColor = color. uiColor
164
+ }
130
165
}
131
166
132
167
extension ToolbarItem {
@@ -150,12 +185,97 @@ extension ToolbarItem {
150
185
}
151
186
}
152
187
188
+ indirect enum ToolbarItemLocation : Hashable {
189
+ case expression( inside: ToolbarItemLocation ? )
190
+ case block( index: Int , inside: ToolbarItemLocation ? )
191
+ case array( index: Int , inside: ToolbarItemLocation ? )
192
+ case optional( inside: ToolbarItemLocation ? )
193
+ case eitherFirst( inside: ToolbarItemLocation ? )
194
+ case eitherSecond( inside: ToolbarItemLocation ? )
195
+ }
196
+
197
+ final class KeyboardToolbar : UIToolbar {
198
+ var locations : [ ToolbarItemLocation : UIBarButtonItem ] = [ : ]
199
+
200
+ func setItems(
201
+ _ components: ToolbarBuilder . FinalResult ,
202
+ animated: Bool
203
+ ) {
204
+ var newItems : [ UIBarButtonItem ] = [ ]
205
+ var newLocations : [ ToolbarItemLocation : UIBarButtonItem ] = [ : ]
206
+
207
+ visitItems ( component: components, inside: nil ) { location, expression in
208
+ var item =
209
+ if let oldItem = locations [ location] {
210
+ updateErasedItem ( expression, oldItem)
211
+ } else {
212
+ expression. createBarButtonItem ( )
213
+ }
214
+
215
+ newItems. append ( item)
216
+ newLocations [ location] = item
217
+ }
218
+
219
+ super. setItems ( newItems, animated: animated)
220
+ self . locations = newLocations
221
+ }
222
+
223
+ /// Used to open the existential to call ``ToolbarItem/updateBarButtonItem(_:)``.
224
+ private func updateErasedItem< T: ToolbarItem > ( _ expression: T , _ item: UIBarButtonItem )
225
+ -> UIBarButtonItem
226
+ {
227
+ if var castedItem = item as? T . ItemType {
228
+ expression. updateBarButtonItem ( & castedItem)
229
+ return castedItem
230
+ } else {
231
+ return expression. createBarButtonItem ( )
232
+ }
233
+ }
234
+
235
+ /// DFS on the `component` tree
236
+ private func visitItems(
237
+ component: ToolbarBuilder . Component ,
238
+ inside container: ToolbarItemLocation ? ,
239
+ callback: ( ToolbarItemLocation , any ToolbarItem ) -> Void
240
+ ) {
241
+ switch component {
242
+ case . expression( let expression) :
243
+ callback ( . expression( inside: container) , expression)
244
+ case . block( let elements) :
245
+ for (i, element) in elements. enumerated ( ) {
246
+ visitItems (
247
+ component: element, inside: . block( index: i, inside: container) ,
248
+ callback: callback)
249
+ }
250
+ case . array( let elements) :
251
+ for (i, element) in elements. enumerated ( ) {
252
+ visitItems (
253
+ component: element, inside: . array( index: i, inside: container) ,
254
+ callback: callback)
255
+ }
256
+ case . optional( let element) :
257
+ if let element {
258
+ visitItems (
259
+ component: element, inside: . optional( inside: container) , callback: callback
260
+ )
261
+ }
262
+ case . eitherFirst( let element) :
263
+ visitItems (
264
+ component: element, inside: . eitherFirst( inside: container) , callback: callback)
265
+ case . eitherSecond( let element) :
266
+ visitItems (
267
+ component: element, inside: . eitherSecond( inside: container) , callback: callback
268
+ )
269
+ }
270
+ }
271
+ }
272
+
153
273
enum ToolbarKey : EnvironmentKey {
154
- static let defaultValue : ( ( UIToolbar ) -> Void ) ? = nil
274
+ static let defaultValue : ( ( KeyboardToolbar ) -> Void ) ? = nil
155
275
}
156
276
157
277
extension EnvironmentValues {
158
- var updateToolbar : ( ( UIToolbar ) -> Void ) ? {
278
+ var updateToolbar : ( ( KeyboardToolbar ) -> Void ) ? {
159
279
get { self [ ToolbarKey . self] }
160
280
set { self [ ToolbarKey . self] = newValue }
161
281
}
@@ -169,11 +289,11 @@ extension View {
169
289
/// - body: The toolbar's contents
170
290
public func keyboardToolbar(
171
291
animateChanges: Bool = true ,
172
- @ToolbarBuilder body: @escaping ( ) -> ToolbarBuilder . Component
292
+ @ToolbarBuilder body: @escaping ( ) -> ToolbarBuilder . FinalResult
173
293
) -> some View {
174
294
EnvironmentModifier ( self ) { environment in
175
295
environment. with ( \. updateToolbar) { toolbar in
176
- toolbar. setItems ( body ( ) . map { $0 . asBarButtonItem ( ) } , animated: animateChanges)
296
+ toolbar. setItems ( body ( ) , animated: animateChanges)
177
297
toolbar. sizeToFit ( )
178
298
}
179
299
}
0 commit comments