@@ -9,75 +9,90 @@ import UIKit
99/// containing the ``View/keyboardToolbar(animateChanges:body:)`` modifier is updated, so any
1010/// state necessary for the toolbar should live in the view itself.
1111public 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 )
1420}
1521
1622@resultBuilder
1723public 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 )
2231 }
32+ public typealias FinalResult = Component
2333
2434 public static func buildExpression( _ expression: any ToolbarItem ) -> Component {
25- [ expression]
35+ . expression( expression )
2636 }
2737
2838 public static func buildBlock( _ components: Component ... ) -> Component {
29- components . flatMap { $0 }
39+ . block ( components )
3040 }
3141
3242 public static func buildArray( _ components: [ Component ] ) -> Component {
33- components . flatMap { $0 }
43+ . array ( components )
3444 }
3545
3646 public static func buildOptional( _ component: Component ? ) -> Component {
37- component ?? [ ]
47+ . optional ( component)
3848 }
3949
4050 public static func buildEither( first component: Component ) -> Component {
41- component
51+ . eitherFirst ( component)
4252 }
4353
4454 public static func buildEither( second component: Component ) -> Component {
45- component
55+ . eitherSecond ( component)
4656 }
4757}
4858
49- final class CallbackBarButtonItem : UIBarButtonItem {
50- private var callback : ( ) -> Void
59+ extension Button : ToolbarItem {
60+ public final class ItemType : UIBarButtonItem {
61+ var callback : ( ) -> Void
5162
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 ( )
5566
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+ }
6076
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+ }
6481 }
6582
66- @objc
67- func onTap( ) {
68- callback ( )
83+ public func createBarButtonItem( ) -> ItemType {
84+ ItemType ( title: label, callback: action)
6985 }
70- }
7186
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
7590 }
7691}
7792
7893@available ( iOS 14 , macCatalyst 14 , tvOS 14 , * )
7994extension Spacer : ToolbarItem {
80- public func asBarButtonItem ( ) -> UIBarButtonItem {
95+ public func createBarButtonItem ( ) -> UIBarButtonItem {
8196 if let minLength, minLength > 0 {
8297 print (
8398 """
@@ -89,44 +104,64 @@ extension Spacer: ToolbarItem {
89104 }
90105 return . flexibleSpace( )
91106 }
107+
108+ public func updateBarButtonItem( _: inout UIBarButtonItem ) {
109+ // no-op
110+ }
92111}
93112
94113struct FixedWidthToolbarItem < Base: ToolbarItem > : ToolbarItem {
95114 var base : Base
96115 var width : Int ?
97116
98- func asBarButtonItem ( ) -> UIBarButtonItem {
99- let item = base. asBarButtonItem ( )
117+ func createBarButtonItem ( ) -> Base . ItemType {
118+ let item = base. createBarButtonItem ( )
100119 if let width {
101120 item. width = CGFloat ( width)
102121 }
103122 return item
104123 }
124+
125+ func updateBarButtonItem( _ item: inout Base . ItemType ) {
126+ base. updateBarButtonItem ( & item)
127+ if let width {
128+ item. width = CGFloat ( width)
129+ }
130+ }
105131}
106132
107133// Setting width on a flexible space is ignored, you must use a fixed space from the outset
108134@available ( iOS 14 , macCatalyst 14 , tvOS 14 , * )
109135struct FixedWidthSpacerItem : ToolbarItem {
110136 var width : Int ?
111137
112- func asBarButtonItem ( ) -> UIBarButtonItem {
138+ func createBarButtonItem ( ) -> UIBarButtonItem {
113139 if let width {
114140 . fixedSpace( CGFloat ( width) )
115141 } else {
116142 . flexibleSpace( )
117143 }
118144 }
145+
146+ func updateBarButtonItem( _ item: inout UIBarButtonItem ) {
147+ item = createBarButtonItem ( )
148+ }
119149}
120150
121151struct ColoredToolbarItem < Base: ToolbarItem > : ToolbarItem {
122152 var base : Base
123153 var color : Color
124154
125- func asBarButtonItem ( ) -> UIBarButtonItem {
126- let item = base. asBarButtonItem ( )
155+ func createBarButtonItem ( ) -> Base . ItemType {
156+ let item = base. createBarButtonItem ( )
127157 item. tintColor = color. uiColor
128158 return item
129159 }
160+
161+ func updateBarButtonItem( _ item: inout Base . ItemType ) {
162+ base. updateBarButtonItem ( & item)
163+ item. tintColor = color. uiColor
164+ }
130165}
131166
132167extension ToolbarItem {
@@ -150,12 +185,97 @@ extension ToolbarItem {
150185 }
151186}
152187
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+
153273enum ToolbarKey : EnvironmentKey {
154- static let defaultValue : ( ( UIToolbar ) -> Void ) ? = nil
274+ static let defaultValue : ( ( KeyboardToolbar ) -> Void ) ? = nil
155275}
156276
157277extension EnvironmentValues {
158- var updateToolbar : ( ( UIToolbar ) -> Void ) ? {
278+ var updateToolbar : ( ( KeyboardToolbar ) -> Void ) ? {
159279 get { self [ ToolbarKey . self] }
160280 set { self [ ToolbarKey . self] = newValue }
161281 }
@@ -169,11 +289,11 @@ extension View {
169289 /// - body: The toolbar's contents
170290 public func keyboardToolbar(
171291 animateChanges: Bool = true ,
172- @ToolbarBuilder body: @escaping ( ) -> ToolbarBuilder . Component
292+ @ToolbarBuilder body: @escaping ( ) -> ToolbarBuilder . FinalResult
173293 ) -> some View {
174294 EnvironmentModifier ( self ) { environment in
175295 environment. with ( \. updateToolbar) { toolbar in
176- toolbar. setItems ( body ( ) . map { $0 . asBarButtonItem ( ) } , animated: animateChanges)
296+ toolbar. setItems ( body ( ) , animated: animateChanges)
177297 toolbar. sizeToFit ( )
178298 }
179299 }
0 commit comments