Skip to content

Commit 8bad996

Browse files
committed
[add] vertical/horizontal flow grids
1 parent 89544e2 commit 8bad996

File tree

6 files changed

+494
-0
lines changed

6 files changed

+494
-0
lines changed

.gitignore

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
# Xcode
2+
#
3+
# gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore
4+
5+
## User settings
6+
xcuserdata/
7+
8+
## macOS Env
9+
.DS_Store
10+
11+
.vscode/
12+
13+
## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9)
14+
*.xcscmblueprint
15+
*.xccheckout
16+
17+
## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4)
18+
build/
19+
DerivedData/
20+
*.moved-aside
21+
*.pbxuser
22+
!default.pbxuser
23+
*.mode1v3
24+
!default.mode1v3
25+
*.mode2v3
26+
!default.mode2v3
27+
*.perspectivev3
28+
!default.perspectivev3
29+
30+
## Obj-C/Swift specific
31+
*.hmap
32+
33+
## App packaging
34+
*.ipa
35+
*.dSYM.zip
36+
*.dSYM
37+
38+
## Playgrounds
39+
timeline.xctimeline
40+
playground.xcworkspace
41+
42+
# Swift Package Manager
43+
#
44+
# Add this line if you want to avoid checking in source code from Swift Package Manager dependencies.
45+
Packages/
46+
# Package.pins
47+
# Package.resolved
48+
# *.xcodeproj
49+
#
50+
# Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata
51+
# hence it is not needed unless you have added a package configuration file to your project
52+
# .swiftpm
53+
54+
.build/
55+
/.build
56+
57+
.swiftpm/configuration/registries.json
58+
.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
59+
.netrc
60+
61+
# CocoaPods
62+
#
63+
# We recommend against adding the Pods directory to your .gitignore. However
64+
# you should judge for yourself, the pros and cons are mentioned at:
65+
# https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control
66+
#
67+
# Pods/
68+
#
69+
# Add this line if you want to avoid checking in source code from the Xcode workspace
70+
# *.xcworkspace
71+
72+
# Carthage
73+
#
74+
# Add this line if you want to avoid checking in source code from Carthage dependencies.
75+
# Carthage/Checkouts
76+
77+
Carthage/Build/
78+
79+
# Accio dependency management
80+
Dependencies/
81+
.accio/
82+
83+
# fastlane
84+
#
85+
# It is recommended to not store the screenshots in the git repo.
86+
# Instead, use fastlane to re-generate the screenshots whenever they are needed.
87+
# For more information about the recommended setup visit:
88+
# https://docs.fastlane.tools/best-practices/source-control/#source-control
89+
90+
fastlane/report.xml
91+
fastlane/Preview.html
92+
fastlane/screenshots/**/*.png
93+
fastlane/test_output
94+
95+
# Code Injection
96+
#
97+
# After new code Injection tools there's a generated folder /iOSInjectionProject
98+
# https://github.com/johnno1962/injectionforxcode
99+
100+
iOSInjectionProject/

.swift-format

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
{
2+
"fileScopedDeclarationPrivacy" : {
3+
"accessLevel" : "private"
4+
},
5+
"indentation" : {
6+
"spaces" : 4
7+
},
8+
"indentConditionalCompilationBlocks" : false,
9+
"indentSwitchCaseLabels" : false,
10+
"lineBreakAroundMultilineExpressionChainComponents" : false,
11+
"lineBreakBeforeControlFlowKeywords" : false,
12+
"lineBreakBeforeEachArgument" : false,
13+
"lineBreakBeforeEachGenericRequirement" : false,
14+
"lineLength" : 150,
15+
"maximumBlankLines" : 1,
16+
"multiElementCollectionTrailingCommas" : true,
17+
"noAssignmentInExpressions" : {
18+
"allowedFunctions" : [
19+
"XCTAssertNoThrow"
20+
]
21+
},
22+
"prioritizeKeepingFunctionOutputTogether" : false,
23+
"respectsExistingLineBreaks" : true,
24+
"rules" : {
25+
"AllPublicDeclarationsHaveDocumentation" : false,
26+
"AlwaysUseLiteralForEmptyCollectionInit" : false,
27+
"AlwaysUseLowerCamelCase" : true,
28+
"AmbiguousTrailingClosureOverload" : true,
29+
"BeginDocumentationCommentWithOneLineSummary" : false,
30+
"DoNotUseSemicolons" : true,
31+
"DontRepeatTypeInStaticProperties" : true,
32+
"FileScopedDeclarationPrivacy" : true,
33+
"FullyIndirectEnum" : true,
34+
"GroupNumericLiterals" : true,
35+
"IdentifiersMustBeASCII" : true,
36+
"NeverForceUnwrap" : false,
37+
"NeverUseForceTry" : false,
38+
"NeverUseImplicitlyUnwrappedOptionals" : false,
39+
"NoAccessLevelOnExtensionDeclaration" : true,
40+
"NoAssignmentInExpressions" : true,
41+
"NoBlockComments" : true,
42+
"NoCasesWithOnlyFallthrough" : true,
43+
"NoEmptyTrailingClosureParentheses" : true,
44+
"NoLabelsInCasePatterns" : true,
45+
"NoLeadingUnderscores" : false,
46+
"NoParensAroundConditions" : true,
47+
"NoPlaygroundLiterals" : true,
48+
"NoVoidReturnOnFunctionSignature" : true,
49+
"OmitExplicitReturns" : false,
50+
"OneCasePerLine" : true,
51+
"OneVariableDeclarationPerLine" : true,
52+
"OnlyOneTrailingClosureArgument" : false,
53+
"OrderedImports" : true,
54+
"ReplaceForEachWithForLoop" : false,
55+
"ReturnVoidInsteadOfEmptyTuple" : true,
56+
"TypeNamesShouldBeCapitalized" : true,
57+
"UseEarlyExits" : false,
58+
"UseLetInEveryBoundCaseVariable" : true,
59+
"UseShorthandTypeNames" : true,
60+
"UseSingleLinePropertyGetter" : true,
61+
"UseSynthesizedInitializer" : true,
62+
"UseTripleSlashForDocumentationComments" : true,
63+
"UseWhereClausesInForLoops" : false,
64+
"ValidateDocumentationComments" : false
65+
},
66+
"spacesAroundRangeFormationOperators" : false,
67+
"tabWidth" : 8,
68+
"version" : 1
69+
}

Package.swift

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
// swift-tools-version: 6.0
2+
3+
import PackageDescription
4+
5+
let package = Package(
6+
name: "swiftui-flow-grids",
7+
platforms: [
8+
.iOS(.v16),
9+
.macCatalyst(.v16),
10+
.macOS(.v13),
11+
.tvOS(.v16),
12+
.visionOS(.v1),
13+
.watchOS(.v9),
14+
],
15+
products: [
16+
.library(
17+
name: "FlowGrids",
18+
targets: ["FlowGrids"])
19+
],
20+
targets: [
21+
.target(name: "FlowGrids"),
22+
.testTarget(
23+
name: "FlowGridsTests",
24+
dependencies: ["FlowGrids"]
25+
),
26+
]
27+
)

Sources/FlowGrids/HFlowGrid.swift

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
//
2+
// HFlowGrid.swift
3+
// swiftui-flow-grids
4+
//
5+
// Created by zijievv on 05/03/2025.
6+
// Copyright © 2025 zijievv. All rights reserved.
7+
//
8+
9+
import SwiftUI
10+
11+
@available(iOS 16.0, macCatalyst 16.0, macOS 13.0, tvOS 16.0, visionOS 1.0, watchOS 9.0, *)
12+
public struct HFlowGrid: Layout {
13+
private let colAlignment: VerticalAlignment
14+
private let itemAlignment: HorizontalAlignment
15+
private let colSpacing: CGFloat
16+
private let itemSpacing: CGFloat
17+
18+
public init(
19+
columnAlignment: VerticalAlignment = .top,
20+
itemAlignment: HorizontalAlignment = .leading,
21+
columnSpacing: CGFloat? = nil,
22+
itemSpacing: CGFloat? = nil
23+
) {
24+
self.colAlignment = columnAlignment
25+
self.itemAlignment = itemAlignment
26+
self.colSpacing = columnSpacing ?? 8
27+
self.itemSpacing = itemSpacing ?? 8
28+
}
29+
30+
public func sizeThatFits(proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) -> CGSize {
31+
let flow: Flow = .calculate(
32+
maxHeight: proposal.replacingUnspecifiedDimensions().height,
33+
spacing: colSpacing,
34+
itemSpacing: itemSpacing,
35+
subviews: subviews)
36+
return .init(width: flow.width, height: proposal.height ?? flow.height)
37+
}
38+
39+
public func placeSubviews(in bounds: CGRect, proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) {
40+
let flow: Flow = .calculate(maxHeight: bounds.height, spacing: colSpacing, itemSpacing: itemSpacing, subviews: subviews)
41+
var x = bounds.minX
42+
var y: CGFloat
43+
for col in flow.columns {
44+
y = colStartY(in: bounds, colHeight: col.height)
45+
for index in col.indices {
46+
let size = subviews[index].sizeThatFits(.unspecified)
47+
let itemX = itemStartX(width: size.width, colMinX: x, colWidth: col.width)
48+
subviews[index].place(at: .init(x: itemX, y: y), proposal: .init(size))
49+
y += size.height + itemSpacing
50+
}
51+
x += col.width + colSpacing
52+
}
53+
}
54+
55+
private func colStartY(in bounds: CGRect, colHeight: CGFloat) -> CGFloat {
56+
switch colAlignment {
57+
case .top:
58+
return bounds.minY
59+
case .bottom:
60+
return bounds.maxY - colHeight
61+
case .center, .firstTextBaseline, .lastTextBaseline:
62+
return (bounds.maxY + bounds.minY - colHeight) / 2
63+
default:
64+
assertionFailure("unknown vertical alignment '\(colAlignment)'")
65+
return (bounds.maxY + bounds.minY - colHeight) / 2
66+
}
67+
}
68+
69+
private func itemStartX(width: CGFloat, colMinX: CGFloat, colWidth: CGFloat) -> CGFloat {
70+
switch itemAlignment {
71+
case .leading, .listRowSeparatorLeading:
72+
return colMinX
73+
case .trailing, .listRowSeparatorTrailing:
74+
return colMinX + colWidth - width
75+
case .center:
76+
return colMinX + (colWidth - width) / 2
77+
default:
78+
assertionFailure("unknown horizontal alignment '\(itemAlignment)'")
79+
return colMinX + (colWidth - width) / 2
80+
}
81+
}
82+
83+
private struct Flow {
84+
let width: CGFloat
85+
let height: CGFloat
86+
let columns: [Column]
87+
88+
static func calculate(maxHeight: CGFloat, spacing: CGFloat, itemSpacing: CGFloat, subviews: Subviews) -> Self {
89+
var cols: [Column] = []
90+
var col: [Int] = []
91+
var colWidth: CGFloat = 0
92+
var x: CGFloat = 0
93+
var y: CGFloat = 0
94+
var height: CGFloat = 0
95+
for (index, subview) in subviews.enumerated() {
96+
let size = subview.sizeThatFits(.unspecified)
97+
if !col.isEmpty && maxHeight < y + itemSpacing + size.height {
98+
cols.append(.init(width: colWidth, height: y, indices: col))
99+
col = []
100+
x += (x == 0 ? colWidth : colWidth + spacing)
101+
y = 0
102+
colWidth = 0
103+
}
104+
col.append(index)
105+
y += (y == 0 ? size.height : size.height + itemSpacing)
106+
height = max(y, height)
107+
colWidth = max(colWidth, size.width)
108+
}
109+
if !col.isEmpty {
110+
cols.append(.init(width: colWidth, height: y, indices: col))
111+
x += (x == 0 ? colWidth : colWidth + spacing)
112+
}
113+
return .init(width: x, height: height, columns: cols)
114+
}
115+
116+
struct Column {
117+
let width: CGFloat
118+
let height: CGFloat
119+
let indices: [Int]
120+
}
121+
}
122+
}
123+
124+
#if DEBUG
125+
#Preview {
126+
HFlowGrid(columnAlignment: .top, itemAlignment: .leading, columnSpacing: 5, itemSpacing: 0) {
127+
Greetings.texts()
128+
}
129+
.frame(height: 100)
130+
.border(.primary)
131+
.padding()
132+
}
133+
#endif

0 commit comments

Comments
 (0)