@@ -38,6 +38,13 @@ public protocol Pill: Equatable, Hashable {
3838 var title : String { get }
3939}
4040
41+ // MARK: - Enums
42+
43+ public enum StackStyle {
44+ case wrap
45+ case noWrap
46+ }
47+
4148// MARK: - Pill customizations
4249
4350public struct PillOptions {
@@ -52,8 +59,8 @@ public struct PillOptions {
5259 /// when animating in/out in its parent view
5360 public var animation : Animation = . spring( )
5461
55- /// Width of the pill
56- public var width : CGFloat = 50
62+ /// Minimum width of the pill
63+ public var minWidth : CGFloat = 50
5764
5865 /// Height of the pill
5966 public var height : CGFloat = 15
@@ -78,6 +85,22 @@ public struct PillOptions {
7885 /// Padding of elements inside PillItem
7986 public var padding : CGFloat = 5
8087
88+ /// Whether pills should wrap to new line or not
89+ public var stackStyle : StackStyle = StackStyle . noWrap
90+
91+ /// Spacing applied vertically between pill rows
92+ public var verticalSpacing : CGFloat = 5
93+
94+ /// Spacing applied horizontally between pills
95+ public var horizontalSpacing : CGFloat = 2
96+
97+ /// The alignment of the pills in the `PillPickerView`
98+ /// when not wrapping the content
99+ public var staticAlignment : HorizontalAlignment = . leading
100+
101+ /// The alignment of the pills in the `PillPickerView`
102+ /// when it is wrapping the content
103+ public var wrappingAlignment : Alignment = . topLeading
81104}
82105
83106// MARK: - Main view
@@ -109,13 +132,16 @@ public struct PillPickerView<T: Pill>: View {
109132 // MARK: - Body
110133
111134 public var body : some View {
112- FlowStack ( items: items, viewGenerator: { item in
113- PillView (
114- options: options,
115- item: item,
116- selectedPills: $selectedPills
117- )
118- } )
135+ switch options. stackStyle {
136+ case StackStyle . noWrap:
137+ StaticStack ( options: options, items: items, viewGenerator: { item in
138+ PillView ( options: options, item: item, selectedPills: $selectedPills)
139+ } )
140+ case StackStyle . wrap:
141+ FlowStack ( options: options, items: items, viewGenerator: { item in
142+ PillView ( options: options, item: item, selectedPills: $selectedPills)
143+ } )
144+ }
119145 }
120146}
121147
@@ -164,9 +190,9 @@ public extension PillPickerView {
164190 }
165191
166192 /// The minimum width of each pill
167- func pillWidth ( _ value: CGFloat ) -> PillPickerView {
193+ func pillMinWidth ( _ value: CGFloat ) -> PillPickerView {
168194 var view = self
169- view. options. width = value
195+ view. options. minWidth = value
170196 return view
171197 }
172198
@@ -199,12 +225,49 @@ public extension PillPickerView {
199225 return view
200226 }
201227
228+ /// Padding of content inside each pill
202229 func pillPadding( _ value: CGFloat ) -> PillPickerView {
203230 var view = self
204231 view. options. padding = value
205232 return view
206233 }
207234
235+ /// The stack style the PillPickerView uses, either wrapping
236+ /// the pills to new lines or having them statically placed
237+ func pillStackStyle( _ value: StackStyle ) -> PillPickerView {
238+ var view = self
239+ view. options. stackStyle = value
240+ return view
241+ }
242+
243+ /// Set the vertical spacing of pills inside PillPickerView
244+ func pillViewVerticalSpacing( _ value: CGFloat ) -> PillPickerView {
245+ var view = self
246+ view. options. verticalSpacing = value
247+ return view
248+ }
249+
250+ /// Set the horizontal spacing of pills inside PillPickerView
251+ func pillViewHorizontalSpacing( _ value: CGFloat ) -> PillPickerView {
252+ var view = self
253+ view. options. horizontalSpacing = value
254+ return view
255+ }
256+
257+ /// Set alignment of pills when statically placed
258+ func pillViewStaticAlignment( _ value: HorizontalAlignment ) -> PillPickerView {
259+ var view = self
260+ view. options. staticAlignment = value
261+ return view
262+ }
263+
264+ /// Set alignment of pills when dynamically placed and wrapping
265+ func pillViewWrappingAlignment( _ value: Alignment ) -> PillPickerView {
266+ var view = self
267+ view. options. wrappingAlignment = value
268+ return view
269+ }
270+
208271}
209272
210273// MARK: - Child views
@@ -250,7 +313,7 @@ struct PillView<T: Pill>: View {
250313 }
251314 }
252315 . padding ( options. padding)
253- . frame ( minWidth: options. width )
316+ . frame ( minWidth: options. minWidth )
254317 } )
255318 . buttonStyle (
256319 PillItemStyle (
@@ -322,7 +385,70 @@ struct PillItemStyle: ButtonStyle {
322385 }
323386}
324387
388+ // MARK: - StaticStack
389+
390+
391+ /// Stack of pills not wrapping to a new line
392+ struct StaticStack < T, V> : View where T: Hashable , V: View {
393+
394+ /// Alias for function type generating content
395+ typealias ContentGenerator = ( T ) -> V
396+
397+ let options : PillOptions
398+
399+ /// Collection of items passed to view
400+ var items : [ T ]
401+
402+ /// Content generator function
403+ var viewGenerator : ContentGenerator
404+
405+ /// Chunk size which `items` is divided into
406+ @State private var chunkSize : Int = 1
407+
408+ private func calculateChunkSize( geometry: GeometryProxy ) {
409+ let availableWidth = geometry. size. width
410+ let itemWidth : CGFloat = 100
411+
412+ chunkSize = max ( Int ( availableWidth / itemWidth) , 1 )
413+ }
414+
415+ var body : some View {
416+ GeometryReader { geometry in
417+ VStack ( alignment: options. staticAlignment, spacing: options. verticalSpacing) {
418+ ForEach ( items. chunked ( into: chunkSize) , id: \. self) { chunk in
419+ HStack ( spacing: options. horizontalSpacing) {
420+ ForEach ( chunk, id: \. self) { item in
421+ viewGenerator ( item)
422+ }
423+ }
424+ }
425+ }
426+ . onAppear {
427+ calculateChunkSize ( geometry: geometry)
428+ }
429+
430+ /// Dynamically generate chunk size based on
431+ /// screen direction and dimension
432+ . onChange ( of: geometry. size. width) { _ in
433+ calculateChunkSize ( geometry: geometry)
434+ }
435+ }
436+ }
437+ }
438+
439+ // MARK: - Extension
325440
441+ extension Array {
442+
443+ /// This method takes an integer `size`
444+ /// and returns a two-dimensional array ([[Element]])
445+ /// where the original array is divided into chunks of the specified size.
446+ func chunked( into size: Int ) -> [ [ Element ] ] {
447+ stride ( from: 0 , to: count, by: size) . map {
448+ Array ( self [ $0..< Swift . min ( $0 + size, count) ] )
449+ }
450+ }
451+ }
326452
327453// MARK: - FlowStack
328454
@@ -336,17 +462,13 @@ public struct FlowStack<T, V>: View where T: Hashable, V: View {
336462 /// Alias for function type generating content
337463 typealias ContentGenerator = ( T ) -> V
338464
465+ let options : PillOptions
466+
339467 /// Collection of items passed to view
340468 var items : [ T ]
341469
342470 /// Content generator function
343471 var viewGenerator : ContentGenerator
344-
345- /// Horizontal spacing of each item
346- var horizontalSpacing : CGFloat = 2
347-
348- /// Vertical spacing for each item
349- var verticalSpacing : CGFloat = 0
350472
351473 /// Current total height calculated
352474 @State private var totalHeight = CGFloat . zero
@@ -359,7 +481,7 @@ public struct FlowStack<T, V>: View where T: Hashable, V: View {
359481 generateContent ( in: geometry)
360482 }
361483 }
362- . frame ( height : totalHeight)
484+ . frame ( maxHeight : totalHeight)
363485 }
364486
365487 // MARK: - Content Generation
@@ -368,11 +490,11 @@ public struct FlowStack<T, V>: View where T: Hashable, V: View {
368490 var width = CGFloat . zero
369491 var height = CGFloat . zero
370492
371- return ZStack ( alignment: . topLeading ) {
493+ return ZStack ( alignment: options . wrappingAlignment ) {
372494 ForEach ( items, id: \. self) { item in
373495 viewGenerator ( item)
374- . padding ( . horizontal, horizontalSpacing)
375- . padding ( . vertical, verticalSpacing)
496+ . padding ( . horizontal, options . horizontalSpacing)
497+ . padding ( . vertical, options . verticalSpacing)
376498 . alignmentGuide ( . leading, computeValue: { dimension in
377499 return calculateLeadingAlignment ( dimension: dimension, item: item)
378500 } )
@@ -385,8 +507,12 @@ public struct FlowStack<T, V>: View where T: Hashable, V: View {
385507
386508 // MARK: - Alignment calculations
387509
510+ /// Checks if adding the item's width to the current width value exceeds the
511+ /// available width (given by `geometry.size.width`). If it does, it resets width
512+ /// to 0 and subtracts the item's height from height to move to the next row.
513+ /// Otherwise, it returns the current `width` value and updates `width` by subtracting the item's width.
388514 func calculateLeadingAlignment( dimension: ViewDimensions , item: T ) -> CGFloat {
389- if abs ( width - dimension. width) > geometry. size. width {
515+ if abs ( width - ( dimension. width + dimension . width / 2 ) ) > geometry. size. width {
390516 width = 0
391517 height -= dimension. height
392518 }
@@ -399,6 +525,9 @@ public struct FlowStack<T, V>: View where T: Hashable, V: View {
399525 return result
400526 }
401527
528+ /// Used to calculate the top (vertical) alignment for each item.
529+ /// It receives the item itself and returns the current height value.
530+ /// If the item is the last one, it resets `height` to 0.
402531 func calculateTopAlignment( item: T ) -> CGFloat {
403532 let result = height
404533 if item == items. last {
@@ -410,7 +539,10 @@ public struct FlowStack<T, V>: View where T: Hashable, V: View {
410539 }
411540
412541 // MARK: - Height Calculation
413-
542+
543+ /// Used to calculate the total height of the view. It wraps the ZStack
544+ /// in a GeometryReader to obtain the height of the content and updates
545+ /// the `totalHeight` state variable accordingly.
414546 private func viewHeightReader( _ binding: Binding < CGFloat > ) -> some View {
415547 return GeometryReader { geometry -> Color in
416548 let rect = geometry. frame ( in: . local)
0 commit comments