Skip to content
This repository was archived by the owner on Nov 4, 2022. It is now read-only.

Commit 6e84389

Browse files
Add Waterfall layout (work in progress)
Minor Refactoring + Waterfall Layout WIP
2 parents 1a31399 + e3ffee2 commit 6e84389

15 files changed

Lines changed: 563 additions & 55 deletions

File tree

Demo/ASCollectionViewDemo.xcodeproj/project.pbxproj

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
B86C6F4E234B0B9D00522AEF /* StoryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B86C6F41234B0B9D00522AEF /* StoryView.swift */; };
3333
B86C6F4F234B0B9D00522AEF /* MainView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B86C6F42234B0B9D00522AEF /* MainView.swift */; };
3434
B89129FE235DB71900D8BA90 /* TagsScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = B89129FC235DB71900D8BA90 /* TagsScreen.swift */; };
35+
B899D70123752CEC001BB5FA /* WaterfallScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = B899D6FD23752BDE001BB5FA /* WaterfallScreen.swift */; };
3536
B8A95E3923607F850017A7EA /* CustomDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8A95E3823607F850017A7EA /* CustomDelegate.swift */; };
3637
B8A95E3C2360800D0017A7EA /* MagazineLayout in Frameworks */ = {isa = PBXBuildFile; productRef = B8A95E3B2360800D0017A7EA /* MagazineLayout */; };
3738
B8A95E3E236081500017A7EA /* MagazineLayoutScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8A95E3D236081500017A7EA /* MagazineLayoutScreen.swift */; };
@@ -66,6 +67,7 @@
6667
B86C6F42234B0B9D00522AEF /* MainView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MainView.swift; sourceTree = "<group>"; };
6768
B89129FC235DB71900D8BA90 /* TagsScreen.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TagsScreen.swift; sourceTree = "<group>"; };
6869
B89129FD235DB71900D8BA90 /* TagStore.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TagStore.swift; sourceTree = "<group>"; };
70+
B899D6FD23752BDE001BB5FA /* WaterfallScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WaterfallScreen.swift; sourceTree = "<group>"; };
6971
B8A95E3823607F850017A7EA /* CustomDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomDelegate.swift; sourceTree = "<group>"; };
7072
B8A95E3D236081500017A7EA /* MagazineLayoutScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MagazineLayoutScreen.swift; sourceTree = "<group>"; };
7173
B8D8292D2371077400D3F0AE /* SampleUsage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = SampleUsage.swift; path = ../readmeAssets/SampleUsage.swift; sourceTree = "<group>"; };
@@ -174,6 +176,7 @@
174176
B86C6F39234B0B9D00522AEF /* PhotoGrid */,
175177
B86C6F3B234B0B9D00522AEF /* AppStore */,
176178
B86C6F3E234B0B9D00522AEF /* InstaFeed */,
179+
B899D6FE23752BE2001BB5FA /* Waterfall */,
177180
);
178181
path = Screens;
179182
sourceTree = "<group>";
@@ -215,6 +218,14 @@
215218
path = Tags;
216219
sourceTree = "<group>";
217220
};
221+
B899D6FE23752BE2001BB5FA /* Waterfall */ = {
222+
isa = PBXGroup;
223+
children = (
224+
B899D6FD23752BDE001BB5FA /* WaterfallScreen.swift */,
225+
);
226+
path = Waterfall;
227+
sourceTree = "<group>";
228+
};
218229
B8A95E3723607F760017A7EA /* MagazineLayout */ = {
219230
isa = PBXGroup;
220231
children = (
@@ -315,6 +326,7 @@
315326
B86C6F48234B0B9D00522AEF /* App.swift in Sources */,
316327
B8268B522363EF03008C99A3 /* RemindersScreen.swift in Sources */,
317328
B86C6F47234B0B9D00522AEF /* Post.swift in Sources */,
329+
B899D70123752CEC001BB5FA /* WaterfallScreen.swift in Sources */,
318330
B86C6F49234B0B9D00522AEF /* PhotoGridScreen.swift in Sources */,
319331
B86C6F4E234B0B9D00522AEF /* StoryView.swift in Sources */,
320332
B86C6F4D234B0B9D00522AEF /* InstaFeedScreen.swift in Sources */,

Demo/ASCollectionViewDemo/MainView.swift

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,7 @@ struct MainView: View
88
{
99
NavigationView
1010
{
11-
List
12-
{
11+
List {
1312
NavigationLink(destination: PhotoGridScreen())
1413
{
1514
Image(systemName: "1.square.fill")
@@ -40,6 +39,11 @@ struct MainView: View
4039
Image(systemName: "6.square.fill")
4140
Text("Magazine Layout (with context menu)")
4241
}
42+
NavigationLink(destination: WaterfallScreen())
43+
{
44+
Image(systemName: "7.square.fill")
45+
Text("Waterfall (Work in progress)")
46+
}
4347
}
4448
.navigationBarTitle("Demo App")
4549
}

Demo/ASCollectionViewDemo/Models/Post.swift

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ struct Post: Identifiable
1010
var caption: String
1111
var aspectRatio: CGFloat
1212
var randomNumberForImage: Int
13+
var offset: Int
1314

1415
var url: URL
1516
{
@@ -29,13 +30,37 @@ struct Post: Identifiable
2930
randomNumberForImage.hashValue
3031
}
3132

32-
static func randomPost(_ randomNumber: Int, aspectRatio: CGFloat) -> Post
33+
static func randomPost(_ randomNumber: Int, aspectRatio: CGFloat, offset: Int = 0) -> Post
3334
{
3435
Post(
3536
username: Lorem.fullName,
3637
location: Lorem.words(Int.random(in: 1...3)),
3738
caption: Lorem.sentences(1...3),
3839
aspectRatio: aspectRatio,
39-
randomNumberForImage: randomNumber)
40+
randomNumberForImage: randomNumber,
41+
offset: offset
42+
)
43+
}
44+
}
45+
46+
47+
48+
struct DataSource
49+
{
50+
static func postsForSection(_ sectionID: Int, number: Int = 12) -> [Post]
51+
{
52+
(0..<number).map
53+
{ b -> Post in
54+
let aspect: CGFloat = .random(in: 0.3 ... 1.5)
55+
return Post.randomPost(sectionID * 10_000 + b, aspectRatio: aspect, offset: b)
56+
}
57+
}
58+
59+
static func appsForSection(_ sectionID: Int) -> [App]
60+
{
61+
(0...17).map
62+
{ b -> App in
63+
App.randomApp(sectionID * 10_000 + b)
64+
}
4065
}
4166
}

Demo/ASCollectionViewDemo/Screens/InstaFeed/InstaFeedScreen.swift

Lines changed: 0 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -110,25 +110,6 @@ struct InstaFeedScreen: View
110110
}
111111
}
112112

113-
struct DataSource
114-
{
115-
static func postsForSection(_ sectionID: Int, number: Int = 12) -> [Post]
116-
{
117-
(0..<number).map
118-
{ b -> Post in
119-
let aspect: CGFloat = [1.0, 1.5, 0.75].randomElement() ?? 1
120-
return Post.randomPost(sectionID * 10_000 + b, aspectRatio: aspect)
121-
}
122-
}
123-
124-
static func appsForSection(_ sectionID: Int) -> [App]
125-
{
126-
(0...17).map
127-
{ b -> App in
128-
App.randomApp(sectionID * 10_000 + b)
129-
}
130-
}
131-
}
132113

133114
struct FeedView_Previews: PreviewProvider
134115
{

Demo/ASCollectionViewDemo/Screens/Tags/TagsScreen.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ class AlignedFlowLayout: UICollectionViewFlowLayout
5353
{
5454
if let collectionView = self.collectionView
5555
{
56-
return collectionView.frame.size != newBounds.size
56+
return collectionView.frame.width != newBounds.width //We only care about changes in the width
5757
}
5858

5959
return false
Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
// ASCollectionView. Created by Apptek Studios 2019
2+
3+
import ASCollectionView
4+
import SwiftUI
5+
import UIKit
6+
7+
8+
///THIS IS A WORK IN PROGRESS
9+
struct WaterfallScreen: View
10+
{
11+
@State var data: [Post] = DataSource.postsForSection(1, number: 1000)
12+
@State var selectedItems: [SectionID: IndexSet] = [:]
13+
14+
@Environment(\.editMode) private var editMode
15+
var isEditing: Bool
16+
{
17+
editMode?.wrappedValue.isEditing ?? false
18+
}
19+
20+
typealias SectionID = Int
21+
22+
var section: ASCollectionViewSection<SectionID>
23+
{
24+
ASCollectionViewSection(
25+
id: 0,
26+
data: data,
27+
onCellEvent: onCellEvent)
28+
{ item, state in
29+
GeometryReader { geom in
30+
ZStack(alignment: .bottomTrailing)
31+
{
32+
ASRemoteImageView(item.squareThumbURL)
33+
.scaledToFill()
34+
.frame(width: geom.size.width, height: geom.size.height)
35+
.opacity(state.isSelected ? 0.7 : 1.0)
36+
37+
if state.isSelected
38+
{
39+
ZStack
40+
{
41+
Circle()
42+
.fill(Color.blue)
43+
Circle()
44+
.strokeBorder(Color.white, lineWidth: 2)
45+
Image(systemName: "checkmark")
46+
.font(.system(size: 10, weight: .bold))
47+
.foregroundColor(.white)
48+
}
49+
.frame(width: 20, height: 20)
50+
.padding(10)
51+
} else {
52+
Text("\(item.offset)")
53+
.font(.title)
54+
.bold()
55+
.padding(2)
56+
.background(Color(.systemBackground).opacity(0.5))
57+
.cornerRadius(4)
58+
.padding(10)
59+
}
60+
}
61+
.frame(width: geom.size.width, height: geom.size.height)
62+
.clipped()
63+
}
64+
}
65+
}
66+
67+
var body: some View
68+
{
69+
ASCollectionView(
70+
selectedItems: $selectedItems,
71+
sections: [section])
72+
.layout(self.layout)
73+
.customDelegate(WaterfallScreenLayoutDelegate.init)
74+
.contentInsets(.init(top: 10, left: 10, bottom: 10, right: 10))
75+
.navigationBarTitle("Waterfall (Work in progress)", displayMode: .inline)
76+
.navigationBarItems(
77+
trailing:
78+
HStack(spacing: 20)
79+
{
80+
if self.isEditing
81+
{
82+
Button(action: {
83+
if let (_, indexSet) = self.selectedItems.first
84+
{
85+
self.data.remove(atOffsets: indexSet)
86+
}
87+
})
88+
{
89+
Image(systemName: "trash")
90+
}
91+
}
92+
93+
EditButton()
94+
})
95+
}
96+
97+
func onCellEvent(_ event: CellEvent<Post>)
98+
{
99+
switch event
100+
{
101+
case let .onAppear(item):
102+
ASRemoteImageManager.shared.load(item.squareThumbURL)
103+
case let .onDisappear(item):
104+
ASRemoteImageManager.shared.cancelLoad(for: item.squareThumbURL)
105+
case let .prefetchForData(data):
106+
for item in data
107+
{
108+
ASRemoteImageManager.shared.load(item.squareThumbURL)
109+
}
110+
case let .cancelPrefetchForData(data):
111+
for item in data
112+
{
113+
ASRemoteImageManager.shared.cancelLoad(for: item.squareThumbURL)
114+
}
115+
}
116+
}
117+
}
118+
119+
extension WaterfallScreen
120+
{
121+
var layout: ASCollectionLayout<Int>
122+
{
123+
ASCollectionLayout
124+
{
125+
let layout = ASWaterfallLayout()
126+
return layout
127+
}
128+
}
129+
}
130+
131+
struct WaterfallScreen_Previews: PreviewProvider
132+
{
133+
static var previews: some View
134+
{
135+
WaterfallScreen()
136+
}
137+
}
138+
139+
140+
class WaterfallScreenLayoutDelegate: ASCollectionViewDelegate, ASWaterfallLayoutDelegate {
141+
/// We explicitely provide a height here. If providing no delegate, this layout will use auto-sizing, however this causes problems if rotating the device (due to limitaitons in UICollecitonView and autosizing cells that are not visible)
142+
func heightForCell(at indexPath: IndexPath, context: ASWaterfallLayout.CellLayoutContext) -> CGFloat {
143+
guard let post: Post = getDataForItem(at: indexPath) else { return 100 }
144+
return context.width / post.aspectRatio
145+
}
146+
}

README.md

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@ A SwiftUI implementation of UICollectionView & UITableView. Here's some of its u
1616
### Pull requests and suggestions welcome :)
1717
<a href="https://github.com/apptekstudios/ASCollectionView/issues">Report Bug</a> · <a href="https://github.com/apptekstudios/ASCollectionView/issues">Suggest a feature</a>
1818

19-
2019
## Table of Contents
2120
* [Getting Started](#getting-started)
2221
* [Usage](#usage)
@@ -29,7 +28,7 @@ A SwiftUI implementation of UICollectionView & UITableView. Here's some of its u
2928

3029

3130
### Screenshots from demo app
32-
<img src="/readmeAssets/demo1.jpeg" width="300"> <img src="/readmeAssets/demo2.jpeg" width="300"> <img src="/readmeAssets/demo3.jpeg" width="300"> <img src="/readmeAssets/demo4.jpeg" width="300"> <img src="/readmeAssets/demo5.jpeg" width="300"> <img src="/readmeAssets/demo6.jpeg" width="300">
31+
<img src="/readmeAssets/demo1.jpeg" width="300"> <img src="/readmeAssets/demo2.jpeg" width="300"> <img src="/readmeAssets/demo3.jpeg" width="300"> <img src="/readmeAssets/demo4.jpeg" width="300"> <img src="/readmeAssets/demo5.jpeg" width="300"> <img src="/readmeAssets/demo6.jpeg" width="300"> <img src="/readmeAssets/demo7.jpeg" width="500">
3332

3433
## Getting Started
3534
ASCollectionView is a swift package.

Sources/ASCollectionView/ASCollectionView.swift

Lines changed: 17 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -50,10 +50,10 @@ extension ASCollectionView where SectionID == Int
5050
/**
5151
Initializes a collection view with a single section of static content
5252
*/
53-
init(@ViewArrayBuilder content: () -> [AnyView])
53+
init(@ViewArrayBuilder staticContent: (() -> [AnyView])) //Clashing with above functions in Swift 5.1, therefore internal for time being
5454
{
5555
sections = [
56-
ASCollectionViewSection(id: 0, content: content)
56+
ASCollectionViewSection(id: 0, content: staticContent)
5757
]
5858
}
5959
}
@@ -212,8 +212,14 @@ public struct ASCollectionView<SectionID: Hashable>: UIViewControllerRepresentab
212212
cell.invalidateLayout = {
213213
collectionView.collectionViewLayout.invalidateLayout()
214214
}
215-
cell.selfSizeHorizontal = self.delegate?.collectionView(cellShouldSelfSizeHorizontallyForItemAt: indexPath) ?? true
216-
cell.selfSizeVertical = self.delegate?.collectionView(cellShouldSelfSizeVerticallyForItemAt: indexPath) ?? true
215+
cell.selfSizeHorizontal =
216+
self.delegate?.collectionView(cellShouldSelfSizeHorizontallyForItemAt: indexPath)
217+
?? (collectionView.collectionViewLayout as? ASCollectionViewLayoutProtocol)?.selfSizeHorizontally
218+
?? true
219+
cell.selfSizeVertical =
220+
self.delegate?.collectionView(cellShouldSelfSizeVerticallyForItemAt: indexPath)
221+
?? (collectionView.collectionViewLayout as? ASCollectionViewLayoutProtocol)?.selfSizeVertically
222+
?? true
217223
cell.setupFor(
218224
id: itemID,
219225
hostingController: hostController)
@@ -411,6 +417,7 @@ extension ASCollectionView.Coordinator
411417
{
412418
func setupPrefetching()
413419
{
420+
let numberToPreload = 10
414421
prefetchSubscription = queuePrefetch
415422
.collect(.byTime(DispatchQueue.main, 0.1)) // Wanted to use .throttle(for: 0.1, scheduler: DispatchQueue(label: "ASCollectionView PREFETCH"), latest: true) -> THIS CRASHES?? BUG??
416423
.compactMap
@@ -430,33 +437,33 @@ extension ASCollectionView.Coordinator
430437
let sectionIndexPaths = self.parent.sections[item.section].dataSource.getIndexPaths(withSectionIndex: item.section)
431438
let nextItemsInSection: ArraySlice<IndexPath> = {
432439
guard (item.last + 1) < sectionIndexPaths.endIndex else { return [] }
433-
return sectionIndexPaths[(item.last + 1)..<min(item.last + 6, sectionIndexPaths.endIndex)]
440+
return sectionIndexPaths[(item.last + 1)..<min(item.last + numberToPreload + 1, sectionIndexPaths.endIndex)]
434441
}()
435442
let previousItemsInSection: ArraySlice<IndexPath> = {
436443
guard (item.first - 1) >= sectionIndexPaths.startIndex else { return [] }
437-
return sectionIndexPaths[max(sectionIndexPaths.startIndex, item.first - 5)..<item.first]
444+
return sectionIndexPaths[max(sectionIndexPaths.startIndex, item.first - numberToPreload)..<item.first]
438445
}()
439446
return Array(nextItemsInSection) + Array(previousItemsInSection)
440447
}
441448
// CHECK IF THERES AN EARLIER SECTION TO PRELOAD
442449
if
443450
let firstSection = toPrefetch.keys.min(), // FIND THE EARLIEST VISIBLE SECTION
444451
(firstSection - 1) >= self.parent.sections.startIndex, // CHECK THERE IS A SECTION BEFORE THIS
445-
let firstIndex = visibleIndexPathsBySection[firstSection]?.first, firstIndex < 5 // CHECK HOW CLOSE TO THIS SECTION WE ARE
452+
let firstIndex = visibleIndexPathsBySection[firstSection]?.first, firstIndex < numberToPreload // CHECK HOW CLOSE TO THIS SECTION WE ARE
446453
{
447454
let precedingSection = firstSection - 1
448-
toPrefetch[precedingSection] = self.parent.sections[precedingSection].dataSource.getIndexPaths(withSectionIndex: precedingSection).suffix(5)
455+
toPrefetch[precedingSection] = self.parent.sections[precedingSection].dataSource.getIndexPaths(withSectionIndex: precedingSection).suffix(numberToPreload)
449456
}
450457
// CHECK IF THERES A LATER SECTION TO PRELOAD
451458
if
452459
let lastSection = toPrefetch.keys.max(), // FIND THE LAST VISIBLE SECTION
453460
(lastSection + 1) < self.parent.sections.endIndex, // CHECK THERE IS A SECTION AFTER THIS
454461
let lastIndex = visibleIndexPathsBySection[lastSection]?.last,
455462
let lastSectionEndIndex = self.parent.sections[lastSection].dataSource.getIndexPaths(withSectionIndex: lastSection).last?.item,
456-
(lastSectionEndIndex - lastIndex) < 5 // CHECK HOW CLOSE TO THIS SECTION WE ARE
463+
(lastSectionEndIndex - lastIndex) < numberToPreload // CHECK HOW CLOSE TO THIS SECTION WE ARE
457464
{
458465
let nextSection = lastSection + 1
459-
toPrefetch[nextSection] = Array(self.parent.sections[nextSection].dataSource.getIndexPaths(withSectionIndex: nextSection).prefix(5))
466+
toPrefetch[nextSection] = Array(self.parent.sections[nextSection].dataSource.getIndexPaths(withSectionIndex: nextSection).prefix(numberToPreload))
460467
}
461468
return toPrefetch
462469
}

0 commit comments

Comments
 (0)