-
Notifications
You must be signed in to change notification settings - Fork 176
Expand file tree
/
Copy pathFlowLayout.swift
More file actions
134 lines (118 loc) · 5.04 KB
/
Copy pathFlowLayout.swift
File metadata and controls
134 lines (118 loc) · 5.04 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
/*
See the LICENSE.txt file for this sample’s licensing information.
Abstract:
A flow layout for SwiftUI's layout.
*/
import SwiftUI
struct FlowLayout: Layout {
var alignment: Alignment = .center
var spacing: CGFloat?
func sizeThatFits(proposal: ProposedViewSize, subviews: Subviews, cache: inout Void) -> CGSize {
let result = FlowResult(
in: proposal.replacingUnspecifiedDimensions().width,
subviews: subviews,
alignment: alignment,
spacing: spacing
)
return result.bounds
}
func placeSubviews(in bounds: CGRect, proposal: ProposedViewSize, subviews: Subviews, cache: inout Void) {
let result = FlowResult(
in: proposal.replacingUnspecifiedDimensions().width,
subviews: subviews,
alignment: alignment,
spacing: spacing
)
for row in result.rows {
let rowXOffset = (bounds.width - row.frame.width) * alignment.horizontal.percent
for index in row.range {
let xPos = rowXOffset + row.frame.minX + row.xOffsets[index - row.range.lowerBound] + bounds.minX
let rowYAlignment = (row.frame.height - subviews[index].sizeThatFits(.unspecified).height) *
alignment.vertical.percent
let yPos = row.frame.minY + rowYAlignment + bounds.minY
subviews[index].place(at: CGPoint(x: xPos, y: yPos), anchor: .topLeading, proposal: .unspecified)
}
}
}
struct FlowResult {
var bounds = CGSize.zero
var rows = [Row]()
struct Row {
var range: Range<Int>
var xOffsets: [Double]
var frame: CGRect
}
init(in maxPossibleWidth: Double, subviews: Subviews, alignment: Alignment, spacing: CGFloat?) {
var itemsInRow = 0
var remainingWidth = maxPossibleWidth.isFinite ? maxPossibleWidth : .greatestFiniteMagnitude
var rowMinY = 0.0
var rowHeight = 0.0
var xOffsets: [Double] = []
for (index, subview) in zip(subviews.indices, subviews) {
let idealSize = subview.sizeThatFits(.unspecified)
if index != 0 && widthInRow(index: index, idealWidth: idealSize.width) > remainingWidth {
// Finish the current row without this subview.
finalizeRow(index: max(index - 1, 0), idealSize: idealSize)
}
addToRow(index: index, idealSize: idealSize)
if index == subviews.count - 1 {
// Finish this row; it's either full or we're on the last view anyway.
finalizeRow(index: index, idealSize: idealSize)
}
}
func spacingBefore(index: Int) -> Double {
guard itemsInRow > 0 else { return 0 }
return spacing ?? subviews[index - 1].spacing.distance(to: subviews[index].spacing, along: .horizontal)
}
func widthInRow(index: Int, idealWidth: Double) -> Double {
idealWidth + spacingBefore(index: index)
}
func addToRow(index: Int, idealSize: CGSize) {
let width = widthInRow(index: index, idealWidth: idealSize.width)
xOffsets.append(maxPossibleWidth - remainingWidth + spacingBefore(index: index))
// Allocate width to this item (and spacing).
remainingWidth -= width
// Ensure the row height is as tall as the tallest item.
rowHeight = max(rowHeight, idealSize.height)
// Can fit in this row, add it.
itemsInRow += 1
}
func finalizeRow(index: Int, idealSize: CGSize) {
let rowWidth = maxPossibleWidth - remainingWidth
rows.append(
Row(
range: index - max(itemsInRow - 1, 0) ..< index + 1,
xOffsets: xOffsets,
frame: CGRect(x: 0, y: rowMinY, width: rowWidth, height: rowHeight)
)
)
bounds.width = max(bounds.width, rowWidth)
let ySpacing = spacing ?? ViewSpacing().distance(to: ViewSpacing(), along: .vertical)
bounds.height += rowHeight + (rows.count > 1 ? ySpacing : 0)
rowMinY += rowHeight + ySpacing
itemsInRow = 0
rowHeight = 0
xOffsets.removeAll()
remainingWidth = maxPossibleWidth
}
}
}
}
private extension HorizontalAlignment {
var percent: Double {
switch self {
case .leading: return 0
case .trailing: return 1
default: return 0.5
}
}
}
private extension VerticalAlignment {
var percent: Double {
switch self {
case .top: return 0
case .bottom: return 1
default: return 0.5
}
}
}