Skip to content

Commit

Permalink
Use UIDiffableDataSource.apply to perform diff and only update what…
Browse files Browse the repository at this point in the history
…'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.
  • Loading branch information
caiozullo committed Mar 21, 2024
1 parent 952ace7 commit 0fbdd1a
Show file tree
Hide file tree
Showing 5 changed files with 50 additions and 14 deletions.
41 changes: 39 additions & 2 deletions EssentialApp/EssentialApp/CombineHelpers.swift
Original file line number Diff line number Diff line change
Expand Up @@ -117,8 +117,8 @@ private extension FeedCache {
}

extension Publisher {
func dispatchOnMainQueue() -> AnyPublisher<Output, Failure> {
receive(on: DispatchQueue.immediateWhenOnMainQueueScheduler).eraseToAnyPublisher()
func dispatchOnMainThread() -> AnyPublisher<Output, Failure> {
receive(on: DispatchQueue.immediateWhenOnMainThreadScheduler).eraseToAnyPublisher()
}
}

Expand Down Expand Up @@ -168,6 +168,39 @@ extension DispatchQueue {
DispatchQueue.main.schedule(after: date, interval: interval, tolerance: tolerance, options: options, action)
}
}

static var immediateWhenOnMainThreadScheduler: ImmediateWhenOnMainThreadScheduler {
ImmediateWhenOnMainThreadScheduler()
}

struct ImmediateWhenOnMainThreadScheduler: Scheduler {
typealias SchedulerTimeType = DispatchQueue.SchedulerTimeType
typealias SchedulerOptions = DispatchQueue.SchedulerOptions

var now: SchedulerTimeType {
DispatchQueue.main.now
}

var minimumTolerance: SchedulerTimeType.Stride {
DispatchQueue.main.minimumTolerance
}

func schedule(options: SchedulerOptions?, _ action: @escaping () -> Void) {
guard Thread.isMainThread else {
return DispatchQueue.main.schedule(options: options, action)
}

action()
}

func schedule(after date: SchedulerTimeType, tolerance: SchedulerTimeType.Stride, options: SchedulerOptions?, _ action: @escaping () -> Void) {
DispatchQueue.main.schedule(after: date, tolerance: tolerance, options: options, action)
}

func schedule(after date: SchedulerTimeType, interval: SchedulerTimeType.Stride, tolerance: SchedulerTimeType.Stride, options: SchedulerOptions?, _ action: @escaping () -> Void) -> Cancellable {
DispatchQueue.main.schedule(after: date, interval: interval, tolerance: tolerance, options: options, action)
}
}
}

typealias AnyDispatchQueueScheduler = AnyScheduler<DispatchQueue.SchedulerTimeType, DispatchQueue.SchedulerOptions>
Expand All @@ -176,6 +209,10 @@ extension AnyDispatchQueueScheduler {
static var immediateOnMainQueue: Self {
DispatchQueue.immediateWhenOnMainQueueScheduler.eraseToAnyScheduler()
}

static var immediateOnMainThread: Self {
DispatchQueue.immediateWhenOnMainThreadScheduler.eraseToAnyScheduler()
}
}

extension Scheduler {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ final class LoadResourcePresentationAdapter<Resource, View: ResourceView> {
isLoading = true

cancellable = loader()
.dispatchOnMainQueue()
.dispatchOnMainThread()
.handleEvents(receiveCancel: { [weak self] in
self?.isLoading = false
})
Expand Down
4 changes: 2 additions & 2 deletions EssentialApp/EssentialAppTests/FeedAcceptanceTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ class FeedAcceptanceTests: XCTestCase {
httpClient: HTTPClientStub = .offline,
store: InMemoryFeedStore = .empty
) -> ListViewController {
let sut = SceneDelegate(httpClient: httpClient, store: store, scheduler: .immediateOnMainQueue)
let sut = SceneDelegate(httpClient: httpClient, store: store, scheduler: .immediateOnMainThread)
sut.window = UIWindow(frame: CGRect(x: 0, y: 0, width: 390, height: 1))
sut.configureWindow()

Expand All @@ -97,7 +97,7 @@ class FeedAcceptanceTests: XCTestCase {
}

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

Expand Down
15 changes: 7 additions & 8 deletions EssentialApp/EssentialAppTests/FeedUIIntegrationTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -180,7 +180,7 @@ class FeedUIIntegrationTests: XCTestCase {
func test_loadMoreActions_requestMoreFromLoader() {
let (sut, loader) = makeSUT()
sut.simulateAppearance()
loader.completeFeedLoading()
loader.completeFeedLoading(with: [makeImage()])

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

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

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

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

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

sut.simulateLoadMoreFeedAction()
Expand Down Expand Up @@ -278,10 +278,9 @@ class FeedUIIntegrationTests: XCTestCase {
let (sut, loader) = makeSUT()

sut.simulateAppearance()
loader.completeFeedLoading(with: [image0, image1])

XCTAssertEqual(loader.loadedImageURLs, [], "Expected no image URL requests until views become visible")


loader.completeFeedLoading(with: [image0, image1])
sut.simulateFeedImageViewVisible(at: 0)
XCTAssertEqual(loader.loadedImageURLs, [image0.url], "Expected first image URL request once first view becomes visible")

Expand Down Expand Up @@ -436,9 +435,9 @@ class FeedUIIntegrationTests: XCTestCase {
let (sut, loader) = makeSUT()

sut.simulateAppearance()
loader.completeFeedLoading(with: [image0, image1])
XCTAssertEqual(loader.loadedImageURLs, [], "Expected no image URL requests until image is near visible")


loader.completeFeedLoading(with: [image0, image1])
sut.simulateFeedImageViewNearVisible(at: 0)
XCTAssertEqual(loader.loadedImageURLs, [image0.url], "Expected first image URL request once first image is near visible")

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ public final class ListViewController: UITableViewController, UITableViewDataSou
snapshot.appendItems(cellControllers, toSection: section)
}

dataSource.applySnapshotUsingReloadData(snapshot)
dataSource.apply(snapshot)
}

public func display(_ viewModel: ResourceLoadingViewModel) {
Expand Down

0 comments on commit 0fbdd1a

Please sign in to comment.