Skip to content

Commit eb94c0c

Browse files
authored
Merge pull request #2 from loloop/mc/on-appear
0.0.2 release
2 parents 7ed908f + a673aaa commit eb94c0c

10 files changed

Lines changed: 188 additions & 24 deletions

File tree

Examples/HackerNews/HackerNews.xcodeproj/project.pbxproj

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
objects = {
88

99
/* Begin PBXBuildFile section */
10+
B40EFB752B36244600DE023E /* Identifiable+RemoveDuplicates.swift in Sources */ = {isa = PBXBuildFile; fileRef = B40EFB742B36244600DE023E /* Identifiable+RemoveDuplicates.swift */; };
1011
B44C4EE22B2CC1D000711A81 /* HackerNewsApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = B44C4EE12B2CC1D000711A81 /* HackerNewsApp.swift */; };
1112
B44C4EE42B2CC1D000711A81 /* HackerNewsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = B44C4EE32B2CC1D000711A81 /* HackerNewsViewController.swift */; };
1213
B44C4EE62B2CC1D100711A81 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = B44C4EE52B2CC1D100711A81 /* Assets.xcassets */; };
@@ -16,6 +17,7 @@
1617
/* End PBXBuildFile section */
1718

1819
/* Begin PBXFileReference section */
20+
B40EFB742B36244600DE023E /* Identifiable+RemoveDuplicates.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Identifiable+RemoveDuplicates.swift"; sourceTree = "<group>"; };
1921
B44C4EDE2B2CC1D000711A81 /* HackerNews.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = HackerNews.app; sourceTree = BUILT_PRODUCTS_DIR; };
2022
B44C4EE12B2CC1D000711A81 /* HackerNewsApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HackerNewsApp.swift; sourceTree = "<group>"; };
2123
B44C4EE32B2CC1D000711A81 /* HackerNewsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HackerNewsViewController.swift; sourceTree = "<group>"; };
@@ -61,6 +63,7 @@
6163
B44C4EE12B2CC1D000711A81 /* HackerNewsApp.swift */,
6264
B44C4EE32B2CC1D000711A81 /* HackerNewsViewController.swift */,
6365
B44C4EF32B2CD8F700711A81 /* News.swift */,
66+
B40EFB742B36244600DE023E /* Identifiable+RemoveDuplicates.swift */,
6467
B44C4EE52B2CC1D100711A81 /* Assets.xcassets */,
6568
B44C4EE72B2CC1D100711A81 /* Preview Content */,
6669
);
@@ -129,6 +132,8 @@
129132
Base,
130133
);
131134
mainGroup = B44C4ED52B2CC1D000711A81;
135+
packageReferences = (
136+
);
132137
productRefGroup = B44C4EDF2B2CC1D000711A81 /* Products */;
133138
projectDirPath = "";
134139
projectRoot = "";
@@ -156,6 +161,7 @@
156161
buildActionMask = 2147483647;
157162
files = (
158163
B44C4EE42B2CC1D000711A81 /* HackerNewsViewController.swift in Sources */,
164+
B40EFB752B36244600DE023E /* Identifiable+RemoveDuplicates.swift in Sources */,
159165
B44C4EF42B2CD8F700711A81 /* News.swift in Sources */,
160166
B44C4EE22B2CC1D000711A81 /* HackerNewsApp.swift in Sources */,
161167
);
@@ -300,7 +306,7 @@
300306
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
301307
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
302308
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
303-
IPHONEOS_DEPLOYMENT_TARGET = 17.0;
309+
IPHONEOS_DEPLOYMENT_TARGET = 16.0;
304310
LD_RUNPATH_SEARCH_PATHS = (
305311
"$(inherited)",
306312
"@executable_path/Frameworks",
@@ -331,7 +337,7 @@
331337
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
332338
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
333339
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
334-
IPHONEOS_DEPLOYMENT_TARGET = 17.0;
340+
IPHONEOS_DEPLOYMENT_TARGET = 16.0;
335341
LD_RUNPATH_SEARCH_PATHS = (
336342
"$(inherited)",
337343
"@executable_path/Frameworks",

Examples/HackerNews/HackerNews/HackerNewsApp.swift

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,3 @@ struct HackerNewsApp: App {
1919
}
2020
}
2121
}
22-
23-
struct HackerNews: UIViewControllerRepresentable {
24-
func makeUIViewController(context: Context) -> HackerNewsViewController { .init() }
25-
func updateUIViewController(_ uiViewController: HackerNewsViewController, context: Context) {}
26-
}

Examples/HackerNews/HackerNews/HackerNewsViewController.swift

Lines changed: 58 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -16,24 +16,37 @@ enum APIState<T> {
1616
case finished(T)
1717
}
1818

19+
struct Page<T: Hashable> {
20+
let items: [T]
21+
let currentPage: Int
22+
}
23+
1924
final class HackerNewsViewController: DiffableViewController {
2025

2126
override func viewDidLoad() {
2227
super.viewDidLoad()
23-
24-
reload()
28+
configureCollectionView()
2529

2630
Task {
27-
try await fetchNews()
31+
state = .loading
32+
await reload()
33+
try await fetch()
2834
}
2935
}
3036

31-
var state: APIState<[NewsItem]> = .idle {
32-
didSet {
33-
reload()
34-
}
37+
func configureCollectionView() {
38+
collectionView.refreshControl = UIRefreshControl(
39+
frame: .zero,
40+
primaryAction: .init(
41+
handler: { [weak self] _ in
42+
Task {
43+
try await self?.fetch(fullyReload: true)
44+
}
45+
}))
3546
}
3647

48+
var state: APIState<Page<NewsItem>> = .idle
49+
3750
@CollectionViewBuilder
3851
override var sections: [any CollectionSection] {
3952
List {
@@ -43,7 +56,7 @@ final class HackerNewsViewController: DiffableViewController {
4356
case .loading:
4457
ActivityIndicator()
4558
case .finished(let news):
46-
ForEach(data: news) { item in
59+
ForEach(data: news.items) { item in
4760
News(item)
4861
.onTap { [weak self] in
4962
guard let url = URL(string: item.url) else { return }
@@ -52,25 +65,57 @@ final class HackerNewsViewController: DiffableViewController {
5265
.navigationController?
5366
.present(controller, animated: true)
5467
}
55-
.padding(.vertical(8).horizontal(12))
68+
.padding(.vertical(8).horizontal(12))
69+
}
70+
if news.currentPage < 10 {
71+
ActivityIndicator()
72+
.onAppear { [weak self] in
73+
try await self?.fetch()
74+
}
5675
}
5776
}
5877
}
5978
.contentInsetsReference(.readableContent)
6079
}
6180

62-
func fetchNews() async throws {
63-
state = .loading
81+
func fetch(fullyReload: Bool = false) async throws {
82+
let currentPage = if case .finished(let page) = state, !fullyReload {
83+
page.currentPage
84+
} else {
85+
1
86+
}
6487

65-
guard let url = URL(string: "https://api.hackerwebapp.com/news") else { return }
88+
guard let url = URL(
89+
string: "https://api.hackerwebapp.com/news?page=\(currentPage)")
90+
else { return }
91+
6692
let (data, _) = try await URLSession.shared.data(from: url)
6793
let decoder = JSONDecoder()
6894
decoder.keyDecodingStrategy = .convertFromSnakeCase
6995
let news = try decoder.decode([NewsItem].self, from: data)
70-
state = .finished(news)
96+
97+
if case .finished(let t) = state {
98+
state = .finished(
99+
.init(
100+
items: (t.items + news).removeDuplicates(),
101+
currentPage: currentPage+1))
102+
} else {
103+
state = .finished(
104+
.init(
105+
items: news,
106+
currentPage: currentPage+1))
107+
}
108+
109+
collectionView.refreshControl?.endRefreshing()
110+
await reload()
71111
}
72112
}
73113

114+
struct HackerNews: UIViewControllerRepresentable {
115+
func makeUIViewController(context: Context) -> HackerNewsViewController { .init() }
116+
func updateUIViewController(_ uiViewController: HackerNewsViewController, context: Context) {}
117+
}
118+
74119
#Preview {
75120
NavigationStack {
76121
HackerNews()
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
//
2+
// Identifiable+RemoveDuplicates.swift
3+
// HackerNews
4+
//
5+
// Created by Mauricio Cardozo on 22/12/23.
6+
//
7+
8+
import Foundation
9+
10+
extension Array where Element: Identifiable {
11+
func removeDuplicates() -> Self {
12+
var uniqueElements: [Element] = []
13+
for element in self {
14+
if !uniqueElements.contains(where: { $0.id == element.id }) {
15+
uniqueElements.append(element)
16+
}
17+
}
18+
19+
return uniqueElements
20+
}
21+
}

Examples/HackerNews/HackerNews/News.swift

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,15 +9,15 @@ import DiffableUI
99
import Foundation
1010
import SwiftUI
1111

12-
struct NewsItem: Codable, Hashable {
12+
struct NewsItem: Codable, Hashable, Identifiable {
1313
let id: Int
1414
let title: String
1515
let points: Int?
1616
let user: String?
1717
let timeAgo: String
1818
let commentsCount: Int
1919
let url: String
20-
let domain: String
20+
let domain: String?
2121
}
2222

2323
struct News: CollectionItem {
@@ -30,7 +30,7 @@ struct News: CollectionItem {
3030
var reuseIdentifier: String { "news-cell" }
3131
func configure(cell: NewsCell) {
3232
cell.title = item.title
33-
cell.domain = "(\(item.domain))"
33+
cell.domain = "(\(item.domain ?? ""))"
3434
cell.username = if let user = item.user {
3535
"by \(user)"
3636
} else { "" }

Sources/DiffableUI/CollectionItem.swift

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ public protocol CollectionItem: Equatable, Hashable, Identifiable {
1919
func configure(cell: CellType)
2020
func didSelect()
2121
func setBehaviors(cell: CellType)
22+
func willDisplay()
2223
}
2324

2425
// MARK: - Internal behavbiors & default conformances
@@ -29,6 +30,8 @@ extension CollectionItem {
2930

3031
public func setBehaviors(cell: CellType) {}
3132

33+
public func willDisplay() {}
34+
3235
public var cellClass: CellType.Type {
3336
CellType.self
3437
}

Sources/DiffableUI/DiffableViewController.swift

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,25 @@ open class DiffableViewController: UICollectionViewController {
4242
let oldValue = self.computedSections
4343
self.computedSections = sections
4444

45+
updateAllVisibleItems(
46+
oldSections: oldValue,
47+
newSections: sections,
48+
collectionView: collectionView,
49+
dataSource: diffableDataSource)
50+
51+
let shouldAnimate = animated && !oldValue.isEmpty && !computedSections.isEmpty
52+
53+
diffableDataSource.apply(
54+
computedSections.snapshot,
55+
animatingDifferences: shouldAnimate,
56+
completion: completion)
57+
}
58+
59+
@MainActor
60+
public func reload(animated: Bool = true, completion: (() -> Void)? = nil) async {
61+
let oldValue = self.computedSections
62+
self.computedSections = sections
63+
4564
updateAllVisibleItems(
4665
oldSections: oldValue,
4766
newSections: sections,
@@ -111,6 +130,15 @@ open class DiffableViewController: UICollectionViewController {
111130
item.didSelect()
112131
}
113132

133+
public override func collectionView(
134+
_ collectionView: UICollectionView,
135+
willDisplay cell: UICollectionViewCell,
136+
forItemAt indexPath: IndexPath)
137+
{
138+
let item = computedSections[indexPath.section].items[indexPath.row]
139+
item.willDisplay()
140+
}
141+
114142
private static func cellProvider(
115143
collectionView: UICollectionView,
116144
indexPath: IndexPath,

Sources/DiffableUI/Items/ForEach.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ extension CollectionItemBuilder {
1515
}
1616

1717
public struct ForEach {
18-
public init<T>(data: [T], @CollectionItemBuilder items: (T) -> [any CollectionItem]) {
18+
public init<T>(data: any RandomAccessCollection<T>, @CollectionItemBuilder items: (T) -> [any CollectionItem]) {
1919
self.items = data.map { items($0) }.flatMap { $0 }
2020
}
2121

Sources/DiffableUI/Items/Tappable.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ public struct Tappable<T: CollectionItem>: CollectionItem {
2929
}
3030

3131
public func didSelect() {
32+
_innerItem.didSelect()
3233
_action?()
3334
}
3435

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
//
2+
// OnAppearable.swift
3+
//
4+
//
5+
// Created by Mauricio Cardozo on 21/12/23.
6+
//
7+
8+
#if canImport(UIKit)
9+
import Foundation
10+
11+
public struct Taskable<T: CollectionItem>: CollectionItem {
12+
init(
13+
item: T,
14+
priority: TaskPriority?,
15+
action: @escaping () async throws -> Void) {
16+
self._innerItem = item
17+
self.priority = priority
18+
self.action = action
19+
}
20+
21+
let _innerItem: T
22+
let priority: TaskPriority?
23+
let action: (() async throws -> Void)?
24+
25+
public var id: AnyHashable {
26+
_innerItem.id
27+
}
28+
29+
public var reuseIdentifier: String {
30+
_innerItem.reuseIdentifier + "-taskable"
31+
}
32+
33+
public var item: some Hashable {
34+
_innerItem.item
35+
}
36+
37+
public func didSelect() {
38+
_innerItem.didSelect()
39+
}
40+
41+
public func setBehaviors(cell: T.CellType) {
42+
_innerItem.setBehaviors(cell: cell)
43+
}
44+
45+
public func configure(cell: T.CellType) {
46+
_innerItem.configure(cell: cell)
47+
}
48+
49+
public func willDisplay() {
50+
_innerItem.willDisplay()
51+
Task(priority: priority) {
52+
try await action?()
53+
}
54+
}
55+
}
56+
57+
extension CollectionItem {
58+
public func onAppear(
59+
priority: TaskPriority? = nil,
60+
action: @escaping () async throws -> Void) -> some CollectionItem
61+
{
62+
Taskable(item: self, priority: priority, action: action)
63+
}
64+
}
65+
#endif

0 commit comments

Comments
 (0)