Skip to content

Commit 9f5f586

Browse files
authored
Merge pull request #36 from dkk/feature/performance-optimization
Performance optimization Fixes #34 & #4
2 parents 321dccb + ccade82 commit 9f5f586

File tree

9 files changed

+192
-156
lines changed

9 files changed

+192
-156
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import SwiftUI
2+
3+
/// This class manages content and the calculation of their widths (reusing it).
4+
/// It should be reused whenever possible.
5+
class ContentManager {
6+
enum ViewType {
7+
case any(AnyView)
8+
case newLine
9+
10+
init<V: View>(rawView: V) {
11+
switch rawView {
12+
case is NewLine: self = .newLine
13+
default: self = .any(AnyView(rawView))
14+
}
15+
}
16+
}
17+
18+
let items: [ViewType]
19+
lazy var widths: [Double] = {
20+
items.map {
21+
if case let .any(anyView) = $0 {
22+
return Self.getWidth(of: anyView)
23+
} else {
24+
return 0
25+
}
26+
}
27+
}()
28+
29+
init(items: [ViewType]) {
30+
self.items = items
31+
}
32+
33+
@inline(__always) private static func getWidth(of anyView: AnyView) -> Double {
34+
#if os(iOS)
35+
let hostingController = UIHostingController(rootView: HStack { anyView })
36+
#else
37+
let hostingController = NSHostingController(rootView: HStack { anyView })
38+
#endif
39+
return hostingController.sizeThatFits(in: CGSize(width: CGFloat.greatestFiniteMagnitude, height: CGFloat.greatestFiniteMagnitude)).width
40+
}
41+
42+
func isVisible(viewIndex: Int) -> Bool {
43+
widths[viewIndex] > 0
44+
}
45+
}

Sources/WrappingHStack/InternalWrappingHStack.swift

+17-68
Original file line numberDiff line numberDiff line change
@@ -1,108 +1,57 @@
11
import SwiftUI
22

3-
// based on https://swiftui.diegolavalle.com/posts/linewrapping-stacks/
3+
/// This View draws the WrappingHStack content taking into account the passed width, alignment and spacings.
4+
/// Note that the passed LineManager and ContentManager should be reused whenever possible.
45
struct InternalWrappingHStack: View {
5-
let width: CGFloat
66
let alignment: HorizontalAlignment
77
let spacing: WrappingHStack.Spacing
8-
let content: [WrappingHStack.ViewType]
9-
let firstItemOfEachLine: [Int]
108
let lineSpacing: CGFloat
9+
let lineManager: LineManager
10+
let contentManager: ContentManager
1111

12-
init(width: CGFloat, alignment: HorizontalAlignment, spacing: WrappingHStack.Spacing, lineSpacing: CGFloat, content: [WrappingHStack.ViewType]) {
13-
self.width = width
12+
init(width: CGFloat, alignment: HorizontalAlignment, spacing: WrappingHStack.Spacing, lineSpacing: CGFloat, lineManager: LineManager, contentManager: ContentManager) {
1413
self.alignment = alignment
1514
self.spacing = spacing
1615
self.lineSpacing = lineSpacing
17-
self.content = content
16+
self.contentManager = contentManager
17+
self.lineManager = lineManager
1818

19-
firstItemOfEachLine = content
20-
.enumerated()
21-
.reduce((firstItemOfEachLine: [], currentLineWidth: width)) { (result, contentIterator) -> (firstItemOfEachLine: [Int], currentLineWidth: CGFloat) in
22-
var (firstItemOfEachLine, currentLineWidth) = result
23-
24-
switch contentIterator.element {
25-
case .newLine:
26-
return (firstItemOfEachLine + [contentIterator.offset], width)
27-
case .any(let anyView) where Self.isVisible(view: anyView):
28-
let itemWidth = Self.getWidth(of: anyView)
29-
if result.currentLineWidth + itemWidth + spacing.minSpacing > width {
30-
currentLineWidth = itemWidth
31-
firstItemOfEachLine.append(contentIterator.offset)
32-
} else {
33-
currentLineWidth += itemWidth + spacing.minSpacing
34-
}
35-
return (firstItemOfEachLine, currentLineWidth)
36-
default:
37-
return result
38-
}
39-
}.0
40-
}
41-
42-
static func getWidth(of anyView: AnyView) -> Double {
43-
#if os(iOS)
44-
let hostingController = UIHostingController(rootView: HStack { anyView })
45-
#else
46-
let hostingController = NSHostingController(rootView: HStack { anyView })
47-
#endif
48-
return hostingController.sizeThatFits(in: CGSize(width: CGFloat.greatestFiniteMagnitude, height: CGFloat.greatestFiniteMagnitude)).width
49-
}
50-
51-
var totalLines: Int {
52-
firstItemOfEachLine.count
53-
}
54-
55-
func startOf(line i: Int) -> Int {
56-
firstItemOfEachLine[i]
57-
}
58-
59-
func endOf(line i: Int) -> Int {
60-
i == totalLines - 1 ? content.count - 1 : firstItemOfEachLine[i + 1] - 1
61-
}
62-
63-
func hasExactlyOneElement(line i: Int) -> Bool {
64-
startOf(line: i) == endOf(line: i)
19+
if !lineManager.isSetUp {
20+
lineManager.setup(contentManager: contentManager, width: width, spacing: spacing)
21+
}
6522
}
6623

6724
func shouldHaveSideSpacers(line i: Int) -> Bool {
6825
if case .constant = spacing {
6926
return true
7027
}
71-
if case .dynamic = spacing, hasExactlyOneElement(line: i) {
28+
if case .dynamic = spacing, lineManager.hasExactlyOneElement(line: i) {
7229
return true
7330
}
7431
return false
7532
}
76-
77-
@inline(__always) static func isVisible(view: AnyView) -> Bool {
78-
#if os(iOS)
79-
return UIHostingController(rootView: view).sizeThatFits(in: CGSize(width: CGFloat.greatestFiniteMagnitude, height: CGFloat.greatestFiniteMagnitude)).width > 0
80-
#else
81-
return NSHostingController(rootView: view).sizeThatFits(in: CGSize(width: CGFloat.greatestFiniteMagnitude, height: CGFloat.greatestFiniteMagnitude)).width > 0
82-
#endif
83-
}
8433

8534
var body: some View {
8635
VStack(alignment: alignment, spacing: lineSpacing) {
87-
ForEach(0 ..< totalLines, id: \.self) { lineIndex in
36+
ForEach(0 ..< lineManager.totalLines, id: \.self) { lineIndex in
8837
HStack(spacing: 0) {
8938
if alignment == .center || alignment == .trailing, shouldHaveSideSpacers(line: lineIndex) {
9039
Spacer(minLength: 0)
9140
}
9241

93-
ForEach(startOf(line: lineIndex) ... endOf(line: lineIndex), id: \.self) {
42+
ForEach(lineManager.startOf(line: lineIndex) ... lineManager.endOf(line: lineIndex), id: \.self) {
9443
if case .dynamicIncludingBorders = spacing,
95-
startOf(line: lineIndex) == $0
44+
lineManager.startOf(line: lineIndex) == $0
9645
{
9746
Spacer(minLength: spacing.minSpacing)
9847
}
9948

100-
if case .any(let anyView) = content[$0], Self.isVisible(view: anyView) {
49+
if case .any(let anyView) = contentManager.items[$0], contentManager.isVisible(viewIndex: $0) {
10150
anyView
10251
}
10352

104-
if endOf(line: lineIndex) != $0 {
105-
if case .any(let anyView) = content[$0], !Self.isVisible(view: anyView) { } else {
53+
if lineManager.endOf(line: lineIndex) != $0 {
54+
if case .any = contentManager.items[$0], !contentManager.isVisible(viewIndex: $0) { } else {
10655
if case .constant(let exactSpacing) = spacing {
10756
Spacer(minLength: 0)
10857
.frame(width: exactSpacing)
+59
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import Foundation
2+
3+
/// This class is in charge of calculating which items fit on which lines.
4+
/// It should be reused whenever possible.
5+
class LineManager {
6+
private var contentManager: ContentManager!
7+
private var spacing: WrappingHStack.Spacing!
8+
private var width: CGFloat!
9+
10+
lazy var firstItemOfEachLine: [Int] = {
11+
var firstOfEach = [Int]()
12+
var currentWidth: CGFloat = width
13+
for (index, element) in contentManager.items.enumerated() {
14+
switch element {
15+
case .newLine:
16+
firstOfEach += [index]
17+
currentWidth = width
18+
case .any where contentManager.isVisible(viewIndex: index):
19+
let itemWidth = contentManager.widths[index]
20+
if currentWidth + itemWidth + spacing.minSpacing > width {
21+
currentWidth = itemWidth
22+
firstOfEach.append(index)
23+
} else {
24+
currentWidth += itemWidth + spacing.minSpacing
25+
}
26+
default:
27+
break
28+
}
29+
}
30+
31+
return firstOfEach
32+
}()
33+
34+
var isSetUp: Bool {
35+
width != nil
36+
}
37+
38+
func setup(contentManager: ContentManager, width: CGFloat, spacing: WrappingHStack.Spacing) {
39+
self.contentManager = contentManager
40+
self.width = width
41+
self.spacing = spacing
42+
}
43+
44+
var totalLines: Int {
45+
firstItemOfEachLine.count
46+
}
47+
48+
func startOf(line i: Int) -> Int {
49+
firstItemOfEachLine[i]
50+
}
51+
52+
func endOf(line i: Int) -> Int {
53+
i == totalLines - 1 ? contentManager.items.count - 1 : firstItemOfEachLine[i + 1] - 1
54+
}
55+
56+
func hasExactlyOneElement(line i: Int) -> Bool {
57+
startOf(line: i) == endOf(line: i)
58+
}
59+
}

Sources/WrappingHStack/NewLine.swift

+1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import SwiftUI
22

3+
/// Use this item to force a line break in a WrappingHStack
34
public struct NewLine: View {
45
public init() { }
56
public let body = Spacer(minLength: .infinity)

0 commit comments

Comments
 (0)