Skip to content

Commit

Permalink
Added dynamic spacing to fill the whole width.
Browse files Browse the repository at this point in the history
* Updated parameter order
* Updated comments
  • Loading branch information
dkk committed Apr 17, 2021
1 parent 3146d09 commit d1fc706
Show file tree
Hide file tree
Showing 4 changed files with 75 additions and 29 deletions.
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# WrappingHStack

WrappingHStack is a UI Element that works in a very similar way to HStack, but automatically positioning overflowing elements on next lines.
WrappingHStack is a UI Element that works in a very similar way to HStack, but automatically positions overflowing elements on next lines.

## Example

Expand All @@ -22,7 +22,7 @@ WrappingHStack {
Text("and loop")
.bold()

WrappingHStack(data: 1...20, id:\.self) {
WrappingHStack(1...20, id:\.self) {
Text("Item: \($0)")
.padding(3)
.background(Rectangle().stroke())
Expand All @@ -43,7 +43,7 @@ Requirements iOS 13+

### Swift Package
```swift
.package(url: "https://github.com/dkk/WrappingHStack", .upToNextMajor(from: "1.1.0"))
.package(url: "https://github.com/dkk/WrappingHStack", .upToNextMajor(from: "2.0.0"))
```
## Usage

Expand All @@ -63,7 +63,7 @@ WrappingHStack {

or like a ForEach to loop over items:
```swift
WrappingHStack(data: 1...30, id:\.self) {
WrappingHStack(1...30, id:\.self) {
Text("Item: \($0)")
}
```
Expand Down
43 changes: 32 additions & 11 deletions Sources/WrappingHStack/InternalWrappingHStack.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ import SwiftUI
// based on https://swiftui.diegolavalle.com/posts/linewrapping-stacks/
struct InternalWrappingHStack: View {
var width: CGFloat
var alignment: Alignment
var spacing: CGFloat
var alignment: HorizontalAlignment
var spacing: WrappingHStack.Spacing
var content: [WrappingHStack.ViewType]

var firstItems: [Int] {
Expand All @@ -16,18 +16,18 @@ struct InternalWrappingHStack: View {
return (firstItems + [contentIterator.offset], 0)
case .any(let anyView):
#if os(iOS)
let hostingController = UIHostingController(rootView: HStack(spacing: spacing) { anyView })
let hostingController = UIHostingController(rootView: HStack(spacing: spacing.estimatedSpacing) { anyView })
#else
let hostingController = NSHostingController(rootView: HStack(spacing: spacing) { anyView })
let hostingController = NSHostingController(rootView: HStack(spacing: spacing.estimatedSpacing) { anyView })
#endif

let itemWidth = hostingController.view.intrinsicContentSize.width

if result.currentLineWidth + itemWidth + spacing > width {
if result.currentLineWidth + itemWidth + spacing.estimatedSpacing > width {
currentLineWidth = itemWidth
firstItems.append(contentIterator.offset)
} else {
currentLineWidth += itemWidth + spacing
currentLineWidth += itemWidth + spacing.estimatedSpacing
}
return (firstItems, currentLineWidth)
}
Expand All @@ -46,15 +46,36 @@ struct InternalWrappingHStack: View {
i == totalLanes - 1 ? content.count - 1 : firstItems[i + 1] - 1
}

private func line(laneIndex: Int) -> some View {
HStack(spacing: spacing.estimatedSpacing) {
ForEach(startOf(lane: laneIndex) ... endOf(lane: laneIndex), id: \.self) {
if case .any(let anyView) = content[$0] {
anyView
}
}
}
}

var body: some View {
VStack(alignment: alignment.horizontal, spacing: 0) {
VStack(alignment: alignment, spacing: 0) {
ForEach(0 ..< totalLanes, id: \.self) { laneIndex in
HStack(alignment: alignment.vertical, spacing: spacing) {
ForEach(startOf(lane: laneIndex) ... endOf(lane: laneIndex), id: \.self) {
if case .any(let anyView) = content[$0] {
anyView
if case .constant = spacing {
line(laneIndex: laneIndex)
} else if laneIndex == totalLanes - 1 && startOf(lane: laneIndex) == endOf(lane: laneIndex) {
line(laneIndex: laneIndex)
} else {
HStack(spacing: 0) {
ForEach(startOf(lane: laneIndex) ... endOf(lane: laneIndex), id: \.self) {
if case .any(let anyView) = content[$0] {
anyView
}

if endOf(lane: laneIndex) != $0 {
Spacer(minLength: spacing.estimatedSpacing)
}
}
}
.frame(maxWidth: .infinity)
}
}
}
Expand Down
51 changes: 38 additions & 13 deletions Sources/WrappingHStack/WrappingHStack.swift
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
import SwiftUI

/// WrappingHStack is a UI Element that works in a very similar way to HStack, but automatically positions overflowing elements on next lines.
/// It can be customized by using alignment (controls the alignment of the items, it will get ignored when combined with a `.dynamic` spacing
/// for all but last lines with single elements), spacing (use `.constant` for fixed spacing and `.dynamic` to have the items fill the width
/// of the WrappingHSTack)
public struct WrappingHStack: View {
private struct CGFloatPreferenceKey: PreferenceKey {
static var defaultValue = CGFloat.zero
Expand All @@ -20,9 +24,23 @@ public struct WrappingHStack: View {
}
}

public enum Spacing {
case constant(CGFloat)
case dynamic(minSpacing: CGFloat)

internal var estimatedSpacing: CGFloat {
switch self {
case .constant(let constantSpacing):
return constantSpacing
case .dynamic(minSpacing: let minSpacing):
return minSpacing
}
}
}

var items: [ViewType]
var alignment: Alignment
var spacing: CGFloat
var alignment: HorizontalAlignment
var spacing: Spacing
@State private var height: CGFloat = 0

public var body: some View {
Expand Down Expand Up @@ -58,34 +76,41 @@ public extension WrappingHStack {
}
}

init<Data: RandomAccessCollection, Content: View>(alignment: Alignment = .topLeading, spacing: CGFloat = 8, data: Data, id: KeyPath<Data.Element, Data.Element> = \.self, content: @escaping (Data.Element) -> Content) {
/// Instatiates a WrappingHStack
/// - Parameters:
/// - data: The items to show
/// - id: The `KeyPath` to use as id for the items
/// - alignment: Controls the alignment of the items. This will get ignored when combined with a `.dynamic` spacing for all
/// but last lines with single elements
/// - spacing: Use `.constant` for fixed spacing and `.dynamic` to have the items fill the width of the WrappingHSTack
init<Data: RandomAccessCollection, Content: View>(_ data: Data, id: KeyPath<Data.Element, Data.Element> = \.self, alignment: HorizontalAlignment = .leading, spacing: Spacing = .constant(8), content: @escaping (Data.Element) -> Content) {
self.spacing = spacing
self.alignment = alignment
self.items = data.map { Self.viewType(from: content($0[keyPath: id])) }
}

init<A: View>(alignment: Alignment = .topLeading, spacing: CGFloat = 8, @ViewBuilder content: () -> A) {
init<A: View>(alignment: HorizontalAlignment = .leading, spacing: Spacing = .constant(8), @ViewBuilder content: () -> A) {
self.spacing = spacing
self.alignment = alignment
self.items = [Self.viewType(from: content())]
}

init<A: View, B: View>(alignment: Alignment = .topLeading, spacing: CGFloat = 8, @ViewBuilder content: () -> TupleView<(A, B)>) {
init<A: View, B: View>(alignment: HorizontalAlignment = .leading, spacing: Spacing = .constant(8), @ViewBuilder content: () -> TupleView<(A, B)>) {
self.spacing = spacing
self.alignment = alignment
self.items = [Self.viewType(from: content().value.0),
Self.viewType(from: content().value.1)]
}

init<A: View, B: View, C: View>(alignment: Alignment = .topLeading, spacing: CGFloat = 8, @ViewBuilder content: () -> TupleView<(A, B, C)>) {
init<A: View, B: View, C: View>(alignment: HorizontalAlignment = .leading, spacing: Spacing = .constant(8), @ViewBuilder content: () -> TupleView<(A, B, C)>) {
self.spacing = spacing
self.alignment = alignment
self.items = [Self.viewType(from: content().value.0),
Self.viewType(from: content().value.1),
Self.viewType(from: content().value.2)]
}

init<A: View, B: View, C: View, D: View>(alignment: Alignment = .topLeading, spacing: CGFloat = 8, @ViewBuilder content: () -> TupleView<(A, B, C, D)>) {
init<A: View, B: View, C: View, D: View>(alignment: HorizontalAlignment = .leading, spacing: Spacing = .constant(8), @ViewBuilder content: () -> TupleView<(A, B, C, D)>) {
self.spacing = spacing
self.alignment = alignment
self.items = [Self.viewType(from: content().value.0),
Expand All @@ -94,7 +119,7 @@ public extension WrappingHStack {
Self.viewType(from: content().value.3)]
}

init<A: View, B: View, C: View, D: View, E: View>(alignment: Alignment = .topLeading, spacing: CGFloat = 8, @ViewBuilder content: () -> TupleView<(A, B, C, D, E)>) {
init<A: View, B: View, C: View, D: View, E: View>(alignment: HorizontalAlignment = .leading, spacing: Spacing = .constant(8), @ViewBuilder content: () -> TupleView<(A, B, C, D, E)>) {
self.spacing = spacing
self.alignment = alignment
self.items = [Self.viewType(from: content().value.0),
Expand All @@ -104,7 +129,7 @@ public extension WrappingHStack {
Self.viewType(from: content().value.4)]
}

init<A: View, B: View, C: View, D: View, E: View, F: View>(alignment: Alignment = .topLeading, spacing: CGFloat = 8, @ViewBuilder content: () -> TupleView<(A, B, C, D, E, F)>) {
init<A: View, B: View, C: View, D: View, E: View, F: View>(alignment: HorizontalAlignment = .leading, spacing: Spacing = .constant(8), @ViewBuilder content: () -> TupleView<(A, B, C, D, E, F)>) {
self.spacing = spacing
self.alignment = alignment
self.items = [Self.viewType(from: content().value.0),
Expand All @@ -115,7 +140,7 @@ public extension WrappingHStack {
Self.viewType(from: content().value.5)]
}

init<A: View, B: View, C: View, D: View, E: View, F: View, G: View>(alignment: Alignment = .topLeading, spacing: CGFloat = 8, @ViewBuilder content: () -> TupleView<(A, B, C, D, E, F, G)>) {
init<A: View, B: View, C: View, D: View, E: View, F: View, G: View>(alignment: HorizontalAlignment = .leading, spacing: Spacing = .constant(8), @ViewBuilder content: () -> TupleView<(A, B, C, D, E, F, G)>) {
self.spacing = spacing
self.alignment = alignment
self.items = [Self.viewType(from: content().value.0),
Expand All @@ -127,7 +152,7 @@ public extension WrappingHStack {
Self.viewType(from: content().value.6)]
}

init<A: View, B: View, C: View, D: View, E: View, F: View, G: View, H: View>(alignment: Alignment = .topLeading, spacing: CGFloat = 8, @ViewBuilder content: () -> TupleView<(A, B, C, D, E, F, G, H)>) {
init<A: View, B: View, C: View, D: View, E: View, F: View, G: View, H: View>(alignment: HorizontalAlignment = .leading, spacing: Spacing = .constant(8), @ViewBuilder content: () -> TupleView<(A, B, C, D, E, F, G, H)>) {
self.spacing = spacing
self.alignment = alignment
self.items = [Self.viewType(from: content().value.0),
Expand All @@ -140,7 +165,7 @@ public extension WrappingHStack {
Self.viewType(from: content().value.7)]
}

init<A: View, B: View, C: View, D: View, E: View, F: View, G: View, H: View, I: View>(alignment: Alignment = .topLeading, spacing: CGFloat = 8, @ViewBuilder content: () -> TupleView<(A, B, C, D, E, F ,G, H, I)>) {
init<A: View, B: View, C: View, D: View, E: View, F: View, G: View, H: View, I: View>(alignment: HorizontalAlignment = .leading, spacing: Spacing = .constant(8), @ViewBuilder content: () -> TupleView<(A, B, C, D, E, F ,G, H, I)>) {
self.spacing = spacing
self.alignment = alignment
self.items = [Self.viewType(from: content().value.0),
Expand All @@ -154,7 +179,7 @@ public extension WrappingHStack {
Self.viewType(from: content().value.8)]
}

init<A: View, B: View, C: View, D: View, E: View, F: View, G: View, H: View, I: View, J: View>(alignment: Alignment = .topLeading, spacing: CGFloat = 8, @ViewBuilder content: () -> TupleView<(A, B, C, D, E, F ,G, H, I, J)>) {
init<A: View, B: View, C: View, D: View, E: View, F: View, G: View, H: View, I: View, J: View>(alignment: HorizontalAlignment = .leading, spacing: Spacing = .constant(8), @ViewBuilder content: () -> TupleView<(A, B, C, D, E, F ,G, H, I, J)>) {
self.spacing = spacing
self.alignment = alignment
self.items = [Self.viewType(from: content().value.0),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ struct ExampleView: View {

NewLine()

WrappingHStack(data: 1...20, id:\.self) {
WrappingHStack(1...20, id:\.self) {
Text("Item: \($0)")
.padding(3)
.background(Rectangle().stroke())
Expand Down

0 comments on commit d1fc706

Please sign in to comment.