Skip to content

Commit 0fbdd1a

Browse files
committed
Use UIDiffableDataSource.apply to perform diff and only update what's changed (with animation)
Since the diffable data source diffing happens on its own dedicated queue (not the main queue) that may also run work on the main thread, we had to replace the `ImmediateWhenOnMainQueueScheduler` with the `ImmediateWhenOnMainThreadScheduler`. Also, we had to rearrange the setup of some tests since the diffable data source eagerly preloads cells.
1 parent 952ace7 commit 0fbdd1a

5 files changed

Lines changed: 50 additions & 14 deletions

File tree

EssentialApp/EssentialApp/CombineHelpers.swift

Lines changed: 39 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -117,8 +117,8 @@ private extension FeedCache {
117117
}
118118

119119
extension Publisher {
120-
func dispatchOnMainQueue() -> AnyPublisher<Output, Failure> {
121-
receive(on: DispatchQueue.immediateWhenOnMainQueueScheduler).eraseToAnyPublisher()
120+
func dispatchOnMainThread() -> AnyPublisher<Output, Failure> {
121+
receive(on: DispatchQueue.immediateWhenOnMainThreadScheduler).eraseToAnyPublisher()
122122
}
123123
}
124124

@@ -168,6 +168,39 @@ extension DispatchQueue {
168168
DispatchQueue.main.schedule(after: date, interval: interval, tolerance: tolerance, options: options, action)
169169
}
170170
}
171+
172+
static var immediateWhenOnMainThreadScheduler: ImmediateWhenOnMainThreadScheduler {
173+
ImmediateWhenOnMainThreadScheduler()
174+
}
175+
176+
struct ImmediateWhenOnMainThreadScheduler: Scheduler {
177+
typealias SchedulerTimeType = DispatchQueue.SchedulerTimeType
178+
typealias SchedulerOptions = DispatchQueue.SchedulerOptions
179+
180+
var now: SchedulerTimeType {
181+
DispatchQueue.main.now
182+
}
183+
184+
var minimumTolerance: SchedulerTimeType.Stride {
185+
DispatchQueue.main.minimumTolerance
186+
}
187+
188+
func schedule(options: SchedulerOptions?, _ action: @escaping () -> Void) {
189+
guard Thread.isMainThread else {
190+
return DispatchQueue.main.schedule(options: options, action)
191+
}
192+
193+
action()
194+
}
195+
196+
func schedule(after date: SchedulerTimeType, tolerance: SchedulerTimeType.Stride, options: SchedulerOptions?, _ action: @escaping () -> Void) {
197+
DispatchQueue.main.schedule(after: date, tolerance: tolerance, options: options, action)
198+
}
199+
200+
func schedule(after date: SchedulerTimeType, interval: SchedulerTimeType.Stride, tolerance: SchedulerTimeType.Stride, options: SchedulerOptions?, _ action: @escaping () -> Void) -> Cancellable {
201+
DispatchQueue.main.schedule(after: date, interval: interval, tolerance: tolerance, options: options, action)
202+
}
203+
}
171204
}
172205

173206
typealias AnyDispatchQueueScheduler = AnyScheduler<DispatchQueue.SchedulerTimeType, DispatchQueue.SchedulerOptions>
@@ -176,6 +209,10 @@ extension AnyDispatchQueueScheduler {
176209
static var immediateOnMainQueue: Self {
177210
DispatchQueue.immediateWhenOnMainQueueScheduler.eraseToAnyScheduler()
178211
}
212+
213+
static var immediateOnMainThread: Self {
214+
DispatchQueue.immediateWhenOnMainThreadScheduler.eraseToAnyScheduler()
215+
}
179216
}
180217

181218
extension Scheduler {

EssentialApp/EssentialApp/LoadResourcePresentationAdapter.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ final class LoadResourcePresentationAdapter<Resource, View: ResourceView> {
2424
isLoading = true
2525

2626
cancellable = loader()
27-
.dispatchOnMainQueue()
27+
.dispatchOnMainThread()
2828
.handleEvents(receiveCancel: { [weak self] in
2929
self?.isLoading = false
3030
})

EssentialApp/EssentialAppTests/FeedAcceptanceTests.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ class FeedAcceptanceTests: XCTestCase {
8686
httpClient: HTTPClientStub = .offline,
8787
store: InMemoryFeedStore = .empty
8888
) -> ListViewController {
89-
let sut = SceneDelegate(httpClient: httpClient, store: store, scheduler: .immediateOnMainQueue)
89+
let sut = SceneDelegate(httpClient: httpClient, store: store, scheduler: .immediateOnMainThread)
9090
sut.window = UIWindow(frame: CGRect(x: 0, y: 0, width: 390, height: 1))
9191
sut.configureWindow()
9292

@@ -97,7 +97,7 @@ class FeedAcceptanceTests: XCTestCase {
9797
}
9898

9999
private func enterBackground(with store: InMemoryFeedStore) {
100-
let sut = SceneDelegate(httpClient: HTTPClientStub.offline, store: store, scheduler: .immediateOnMainQueue)
100+
let sut = SceneDelegate(httpClient: HTTPClientStub.offline, store: store, scheduler: .immediateOnMainThread)
101101
sut.sceneWillResignActive(UIApplication.shared.connectedScenes.first!)
102102
}
103103

EssentialApp/EssentialAppTests/FeedUIIntegrationTests.swift

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -180,7 +180,7 @@ class FeedUIIntegrationTests: XCTestCase {
180180
func test_loadMoreActions_requestMoreFromLoader() {
181181
let (sut, loader) = makeSUT()
182182
sut.simulateAppearance()
183-
loader.completeFeedLoading()
183+
loader.completeFeedLoading(with: [makeImage()])
184184

185185
XCTAssertEqual(loader.loadMoreCallCount, 0, "Expected no requests before until load more action")
186186

@@ -209,13 +209,13 @@ class FeedUIIntegrationTests: XCTestCase {
209209
sut.simulateAppearance()
210210
XCTAssertFalse(sut.isShowingLoadMoreFeedIndicator, "Expected no loading indicator once view appears")
211211

212-
loader.completeFeedLoading(at: 0)
212+
loader.completeFeedLoading(with: [makeImage()], at: 0)
213213
XCTAssertFalse(sut.isShowingLoadMoreFeedIndicator, "Expected no loading indicator once loading completes successfully")
214214

215215
sut.simulateLoadMoreFeedAction()
216216
XCTAssertTrue(sut.isShowingLoadMoreFeedIndicator, "Expected loading indicator on load more action")
217217

218-
loader.completeLoadMore(at: 0)
218+
loader.completeLoadMore(with: [makeImage()], at: 0)
219219
XCTAssertFalse(sut.isShowingLoadMoreFeedIndicator, "Expected no loading indicator once user initiated loading completes successfully")
220220

221221
sut.simulateLoadMoreFeedAction()
@@ -278,10 +278,9 @@ class FeedUIIntegrationTests: XCTestCase {
278278
let (sut, loader) = makeSUT()
279279

280280
sut.simulateAppearance()
281-
loader.completeFeedLoading(with: [image0, image1])
282-
283281
XCTAssertEqual(loader.loadedImageURLs, [], "Expected no image URL requests until views become visible")
284-
282+
283+
loader.completeFeedLoading(with: [image0, image1])
285284
sut.simulateFeedImageViewVisible(at: 0)
286285
XCTAssertEqual(loader.loadedImageURLs, [image0.url], "Expected first image URL request once first view becomes visible")
287286

@@ -436,9 +435,9 @@ class FeedUIIntegrationTests: XCTestCase {
436435
let (sut, loader) = makeSUT()
437436

438437
sut.simulateAppearance()
439-
loader.completeFeedLoading(with: [image0, image1])
440438
XCTAssertEqual(loader.loadedImageURLs, [], "Expected no image URL requests until image is near visible")
441-
439+
440+
loader.completeFeedLoading(with: [image0, image1])
442441
sut.simulateFeedImageViewNearVisible(at: 0)
443442
XCTAssertEqual(loader.loadedImageURLs, [image0.url], "Expected first image URL request once first image is near visible")
444443

EssentialFeed/EssentialFeediOS/Shared UI/Controllers/ListViewController.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ public final class ListViewController: UITableViewController, UITableViewDataSou
7373
snapshot.appendItems(cellControllers, toSection: section)
7474
}
7575

76-
dataSource.applySnapshotUsingReloadData(snapshot)
76+
dataSource.apply(snapshot)
7777
}
7878

7979
public func display(_ viewModel: ResourceLoadingViewModel) {

0 commit comments

Comments
 (0)