├── .gitignore
├── .swiftpm
└── xcode
│ └── package.xcworkspace
│ └── xcshareddata
│ └── IDEWorkspaceChecks.plist
├── LICENSE
├── Package.swift
├── README.md
└── Sources
└── FlowStackLayout
├── FlowStack.swift
└── FlowStackLayout.swift
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | /.build
3 | /Packages
4 | /*.xcodeproj
5 | xcuserdata/
6 | DerivedData/
7 | .swiftpm/config/registries.json
8 | .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
9 | .netrc
10 |
--------------------------------------------------------------------------------
/.swiftpm/xcode/package.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2022 smyshlaevalex
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/Package.swift:
--------------------------------------------------------------------------------
1 | // swift-tools-version: 5.7
2 | // The swift-tools-version declares the minimum version of Swift required to build this package.
3 |
4 | import PackageDescription
5 |
6 | let package = Package(
7 | name: "FlowStackLayout",
8 | platforms: [.iOS(.v16), .macCatalyst(.v16), .macOS(.v13), .tvOS(.v16), .watchOS(.v9)],
9 | products: [
10 | .library(
11 | name: "FlowStackLayout",
12 | targets: ["FlowStackLayout"]),
13 | ],
14 | dependencies: [
15 | ],
16 | targets: [
17 | .target(
18 | name: "FlowStackLayout",
19 | dependencies: [])
20 | ]
21 | )
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # FlowStackLayout
2 |
3 | `FlowStack` acts like a `HStack` until there are no more space for new cells, then `FlowStack` will wrap to the next row
4 |
5 | `FlowStack` supports vertical and horizontal alignment
6 |
7 | Horizontal alignment will align rows
8 |
9 | Vertical alignment will align cells based on row height
10 |
11 | `FlowStackLayout` conforms to `Layout` protocol, while `FlowStack` conforms to `View` and wraps `FlowStackLayout`
12 |
13 | View modifiers can't be used with `Layout` protocol, so `FlowStack` is preferred for normal use
14 |
15 | `FlowStack(alignment: Alignment = .center, horizontalSpacing: Double? = nil, verticalSpacing: Double? = nil, content: @escaping () -> Content)`
16 |
17 | ```swift
18 | FlowStack {
19 | ForEach(0...20, id: \.self) {
20 | Text(String($0))
21 | .foregroundColor(Color(uiColor: .systemBackground))
22 | .padding([.leading, .trailing])
23 | .frame(height: 30)
24 | .background {
25 | Capsule()
26 | .fill(.mint)
27 | }
28 | }
29 | }
30 | ```
31 |
32 | `FlowStackLayout(alignment: Alignment = .center, horizontalSpacing: Double? = nil, verticalSpacing: Double? = nil)`
33 |
34 | ```swift
35 | let layout = isVertical ? AnyLayout(VStack()) : AnyLayout(FlowStackLayout())
36 |
37 | layout {
38 |
39 | }
40 | ```
41 |
42 | ## Installation
43 |
44 | ### Swift Package Manager
45 |
46 | ```swift
47 | https://github.com/smyshlaevalex/FlowStackLayout.git
48 | ```
49 |
--------------------------------------------------------------------------------
/Sources/FlowStackLayout/FlowStack.swift:
--------------------------------------------------------------------------------
1 | //
2 | // FlowStack.swift
3 | //
4 | //
5 | // Created by Alexander Smyshlaev on 30.06.2022.
6 | //
7 |
8 | import SwiftUI
9 |
10 | public struct FlowStack: View {
11 | private let alignment: Alignment
12 | private let horizontalSpacing: Double?
13 | private let verticalSpacing: Double?
14 | private let content: () -> Content
15 |
16 | public init(alignment: Alignment = .center,
17 | horizontalSpacing: Double? = nil,
18 | verticalSpacing: Double? = nil,
19 | @ViewBuilder content: @escaping () -> Content) {
20 | self.alignment = alignment
21 | self.horizontalSpacing = horizontalSpacing
22 | self.verticalSpacing = verticalSpacing
23 | self.content = content
24 | }
25 |
26 | public var body: some View {
27 | FlowStackLayout(alignment: alignment, horizontalSpacing: horizontalSpacing, verticalSpacing: verticalSpacing) {
28 | content()
29 | }
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/Sources/FlowStackLayout/FlowStackLayout.swift:
--------------------------------------------------------------------------------
1 | //
2 | // FlowStackLayout.swift
3 | //
4 | //
5 | // Created by Alexander Smyshlaev on 30.06.2022.
6 | //
7 |
8 | import SwiftUI
9 |
10 | public struct FlowStackLayoutResult {
11 | let containerSize: CGSize
12 | let subviewPositions: [CGPoint]
13 | }
14 |
15 | public struct FlowStackLayout: Layout {
16 | private let alignment: Alignment
17 | private let horizontalSpacing: Double?
18 | private let verticalSpacing: Double?
19 |
20 | public init(alignment: Alignment = .center, horizontalSpacing: Double? = nil, verticalSpacing: Double? = nil) {
21 | self.alignment = alignment
22 | self.horizontalSpacing = horizontalSpacing
23 | self.verticalSpacing = verticalSpacing
24 | }
25 |
26 | public func makeCache(subviews: Subviews) -> FlowStackLayoutResult {
27 | FlowStackLayoutResult(containerSize: .zero, subviewPositions: [])
28 | }
29 |
30 | public func updateCache(_ cache: inout FlowStackLayoutResult, subviews: Subviews) {
31 | cache = FlowStackLayoutResult(containerSize: .zero, subviewPositions: [])
32 | }
33 |
34 | public func sizeThatFits(proposal: ProposedViewSize, subviews: Subviews, cache: inout FlowStackLayoutResult) -> CGSize {
35 | cache = layout(maxWidth: proposal.replacingUnspecifiedDimensions().width, subviews: subviews)
36 | return cache.containerSize
37 | }
38 |
39 | public func placeSubviews(in bounds: CGRect, proposal: ProposedViewSize, subviews: Subviews, cache: inout FlowStackLayoutResult) {
40 | for (subview, subviewPosition) in zip(subviews, cache.subviewPositions) {
41 | subview.place(at: CGPoint(x: bounds.minX + subviewPosition.x,
42 | y: bounds.minY + subviewPosition.y),
43 | proposal: proposal)
44 | }
45 | }
46 |
47 | private func layout(maxWidth: CGFloat, subviews: Subviews) -> FlowStackLayoutResult {
48 | var containerSize: CGSize = .zero
49 | var currentSubviewPosition: CGPoint = .zero
50 | var previousHorizontalViewSpacing: ViewSpacing?
51 | var verticalViewSpacing = ViewSpacing()
52 | var subviewFrameRows: [[CGRect]] = []
53 | var currentSubviewFrameRow: [CGRect] = []
54 |
55 | for subview in subviews {
56 | let size = subview.sizeThatFits(.unspecified)
57 | let horizontalSpacing = previousHorizontalViewSpacing.flatMap {
58 | self.horizontalSpacing ?? subview.spacing.distance(to: $0, along: .horizontal)
59 | } ?? 0
60 | previousHorizontalViewSpacing = subview.spacing
61 |
62 | let subviewWidthAndSpacing = size.width + horizontalSpacing
63 |
64 | let newContainerWidth = currentSubviewPosition.x + subviewWidthAndSpacing
65 | if newContainerWidth <= maxWidth {
66 | currentSubviewPosition.x += horizontalSpacing
67 |
68 | currentSubviewFrameRow.append(CGRect(origin: currentSubviewPosition, size: size))
69 |
70 | containerSize.width = max(containerSize.width, newContainerWidth)
71 | currentSubviewPosition.x += size.width
72 |
73 | verticalViewSpacing.formUnion(subview.spacing)
74 |
75 | let newContainerHeight = currentSubviewPosition.y + size.height
76 | containerSize.height = max(containerSize.height, newContainerHeight)
77 | } else {
78 | currentSubviewPosition.x = 0
79 | currentSubviewPosition.y = containerSize.height
80 |
81 | let verticalSpacing = self.verticalSpacing ?? subview.spacing.distance(to: verticalViewSpacing, along: .vertical)
82 | verticalViewSpacing = ViewSpacing()
83 |
84 | currentSubviewPosition.y += verticalSpacing
85 |
86 | subviewFrameRows.append(currentSubviewFrameRow)
87 | currentSubviewFrameRow = []
88 | currentSubviewFrameRow.append(CGRect(origin: currentSubviewPosition, size: size))
89 |
90 | let newContainerHeight = currentSubviewPosition.y + size.height
91 | containerSize.height = max(containerSize.height, newContainerHeight)
92 |
93 | containerSize.width = max(containerSize.width, size.width)
94 |
95 | currentSubviewPosition.x += size.width
96 | }
97 | }
98 |
99 | subviewFrameRows.append(currentSubviewFrameRow)
100 | currentSubviewFrameRow = []
101 |
102 | subviewFrameRows = subviewFrameRows.map { rowFrames in
103 | let trailingSpacing = containerSize.width - (rowFrames.last?.maxX ?? 0)
104 | let rowHeight = rowFrames.max(by: { $0.height < $1.height })?.height ?? 10
105 | let xOffset: CGFloat
106 | switch alignment.horizontal {
107 | case .leading:
108 | xOffset = 0
109 |
110 | case .center:
111 | xOffset = trailingSpacing / 2
112 |
113 | case .trailing:
114 | xOffset = trailingSpacing
115 |
116 | default:
117 | xOffset = 0
118 | }
119 |
120 | return rowFrames.map { frame in
121 | let yOffset: CGFloat
122 | switch alignment.vertical {
123 | case .top:
124 | yOffset = 0
125 |
126 | case .center:
127 | yOffset = (rowHeight - frame.height) / 2
128 |
129 | case .bottom:
130 | yOffset = rowHeight - frame.height
131 |
132 | default:
133 | yOffset = 0
134 | }
135 |
136 | return frame.offsetBy(dx: xOffset, dy: yOffset)
137 | }
138 | }
139 |
140 | let subviewPositions = subviewFrameRows.flatMap { $0 } .map(\.origin)
141 |
142 | return FlowStackLayoutResult(containerSize: containerSize, subviewPositions: subviewPositions)
143 | }
144 | }
145 |
--------------------------------------------------------------------------------