From 9a8d920ad4400c9faf8cdd23210d7c48b5b4d274 Mon Sep 17 00:00:00 2001 From: Cristian Rojas Date: Tue, 29 Apr 2025 17:29:24 +0200 Subject: [PATCH] Fix in flight retention --- .../Feed Cache/LocalFeedLoader.swift | 3 ++- .../Feed Feature/FeedLoader.swift | 6 ++++- .../FeedApi/RemoteFeedLoader.swift | 3 ++- .../FeedLoaderPresentationAdapter.swift | 10 ++++++-- .../MainQueueDispatchDecorator.swift | 4 ++-- .../Controllers/FeedViewController.swift | 6 +++++ .../FeedUI/FeedUIIntegrationTests.swift | 23 ++++++++++++++++--- 7 files changed, 45 insertions(+), 10 deletions(-) diff --git a/EssentialFeed/EssentialFeed/Feed Cache/LocalFeedLoader.swift b/EssentialFeed/EssentialFeed/Feed Cache/LocalFeedLoader.swift index ba75315..845ea2d 100644 --- a/EssentialFeed/EssentialFeed/Feed Cache/LocalFeedLoader.swift +++ b/EssentialFeed/EssentialFeed/Feed Cache/LocalFeedLoader.swift @@ -19,7 +19,7 @@ public final class LocalFeedLoader: FeedLoader { extension LocalFeedLoader { public typealias LoadResult = FeedLoader.Result - public func load(completion: @escaping (LoadResult) -> Void) { + public func load(completion: @escaping (LoadResult) -> Void) -> FeedLoaderTask? { store.retrieve { [weak self] result in guard let self else { return } switch result { @@ -32,6 +32,7 @@ extension LocalFeedLoader { completion(.success([])) } } + return nil } } diff --git a/EssentialFeed/EssentialFeed/Feed Feature/FeedLoader.swift b/EssentialFeed/EssentialFeed/Feed Feature/FeedLoader.swift index c447b7e..580737d 100644 --- a/EssentialFeed/EssentialFeed/Feed Feature/FeedLoader.swift +++ b/EssentialFeed/EssentialFeed/Feed Feature/FeedLoader.swift @@ -4,8 +4,12 @@ import Foundation +public protocol FeedLoaderTask { + func cancel() +} public protocol FeedLoader { typealias Result = Swift.Result<[FeedImage], Error> - func load(completion: @escaping (Result) -> Void) + @discardableResult + func load(completion: @escaping (Result) -> Void) -> FeedLoaderTask? } diff --git a/EssentialFeed/EssentialFeed/FeedApi/RemoteFeedLoader.swift b/EssentialFeed/EssentialFeed/FeedApi/RemoteFeedLoader.swift index b198fa6..4e2f44f 100644 --- a/EssentialFeed/EssentialFeed/FeedApi/RemoteFeedLoader.swift +++ b/EssentialFeed/EssentialFeed/FeedApi/RemoteFeedLoader.swift @@ -23,7 +23,7 @@ public final class RemoteFeedLoader: FeedLoader { self.client = client } - public func load(completion: @escaping (Result) -> Void) { + public func load(completion: @escaping (Result) -> Void) -> FeedLoaderTask? { client.get(from: url) { [weak self] result in guard self != nil else { return } switch result { @@ -33,6 +33,7 @@ public final class RemoteFeedLoader: FeedLoader { completion(.failure(Error.connectivity)) } } + return nil } private static func map(_ data: Data, from response: HTTPURLResponse) -> Result { diff --git a/EssentialFeed/EssentialFeediOS/Composers/FeedLoaderPresentationAdapter.swift b/EssentialFeed/EssentialFeediOS/Composers/FeedLoaderPresentationAdapter.swift index 0f9e042..80ccc45 100644 --- a/EssentialFeed/EssentialFeediOS/Composers/FeedLoaderPresentationAdapter.swift +++ b/EssentialFeed/EssentialFeediOS/Composers/FeedLoaderPresentationAdapter.swift @@ -13,13 +13,14 @@ final class FeedLoaderPresentationAdapter: FeedViewControllerDelegate { private let feedLoader: FeedLoader var presenter: FeedPresenter? + private var currentTask: FeedLoaderTask? init(feedLoader: FeedLoader) { self.feedLoader = feedLoader } func loadFeed() { presenter?.didStartLoadingFeed() - feedLoader.load { [weak self] result in + currentTask = feedLoader.load { [weak self] result in switch result { case let .success(feed): self?.presenter?.didFinishLoadingFeed(with: feed) @@ -32,4 +33,9 @@ final class FeedLoaderPresentationAdapter: FeedViewControllerDelegate { func didRequestFeedRefresh() { loadFeed() } -} \ No newline at end of file + + func didRequestFeedLoadCancel() { + currentTask?.cancel() + currentTask = nil + } +} diff --git a/EssentialFeed/EssentialFeediOS/Composers/MainQueueDispatchDecorator.swift b/EssentialFeed/EssentialFeediOS/Composers/MainQueueDispatchDecorator.swift index 64805a6..524a620 100644 --- a/EssentialFeed/EssentialFeediOS/Composers/MainQueueDispatchDecorator.swift +++ b/EssentialFeed/EssentialFeediOS/Composers/MainQueueDispatchDecorator.swift @@ -21,12 +21,12 @@ final class MainQueueDispatchDecorator { } extension MainQueueDispatchDecorator: FeedLoader where T == FeedLoader { - func load(completion: @escaping (FeedLoader.Result) -> Void) { + func load(completion: @escaping (FeedLoader.Result) -> Void) -> FeedLoaderTask? { decoratee.load { result in Self.dispatch { completion(result) } - } + } } } diff --git a/EssentialFeed/EssentialFeediOS/Feed UI/Controllers/FeedViewController.swift b/EssentialFeed/EssentialFeediOS/Feed UI/Controllers/FeedViewController.swift index 944c8ad..2e0281a 100644 --- a/EssentialFeed/EssentialFeediOS/Feed UI/Controllers/FeedViewController.swift +++ b/EssentialFeed/EssentialFeediOS/Feed UI/Controllers/FeedViewController.swift @@ -9,6 +9,7 @@ import UIKit protocol FeedViewControllerDelegate { func didRequestFeedRefresh() + func didRequestFeedLoadCancel() } public final class FeedViewController: UITableViewControllerExtendedLifecycle, UITableViewDataSourcePrefetching { @@ -28,6 +29,11 @@ public final class FeedViewController: UITableViewControllerExtendedLifecycle, U super.viewDidLoad() } + public override func viewWillDisappear(_ animated: Bool) { + super.viewWillDisappear(animated) + delegate?.didRequestFeedLoadCancel() + } + public override func viewFirstAppearance() { super.viewFirstAppearance() refresh() diff --git a/EssentialFeed/EssentialFeediOSTests/FeedUI/FeedUIIntegrationTests.swift b/EssentialFeed/EssentialFeediOSTests/FeedUI/FeedUIIntegrationTests.swift index 651dbb1..e358c6d 100644 --- a/EssentialFeed/EssentialFeediOSTests/FeedUI/FeedUIIntegrationTests.swift +++ b/EssentialFeed/EssentialFeediOSTests/FeedUI/FeedUIIntegrationTests.swift @@ -12,6 +12,13 @@ import EssentialFeediOS final class FeedUIIntegrationTests: XCTestCase { + func test_viewWillDisappear_requestsFeedLoadCancellation() { + let (sut, loader) = makeSUT() + sut.simulateAppearance() + sut.simulateDisappearance() + XCTAssertEqual(loader.feedCancelRequestsCount, 1) + } + func test_feedView_hasTitle() { let (sut, _) = makeSUT() @@ -397,10 +404,15 @@ final class FeedUIIntegrationTests: XCTestCase { private(set) var feedRequests = [(FeedLoader.Result) -> Void]() var loadFeedCallCount: Int { feedRequests.count } - + + var feedCancelRequestsCount = 0 + - func load(completion: @escaping (FeedLoader.Result) -> Void) { + func load(completion: @escaping (FeedLoader.Result) -> Void) -> FeedLoaderTask? { feedRequests.append(completion) + return TaskSpy { [weak self] in + self?.feedCancelRequestsCount += 1 + } } @@ -414,7 +426,7 @@ final class FeedUIIntegrationTests: XCTestCase { // MARK: - FeedImageDataLoader - private struct TaskSpy: FeedImageDataLoaderTask { + private struct TaskSpy: FeedImageDataLoaderTask, FeedLoaderTask { let cancelCallback: () -> Void func cancel() { cancelCallback() @@ -517,6 +529,11 @@ private extension FeedViewController { endAppearanceTransition() } + func simulateDisappearance() { + beginAppearanceTransition(false, animated: false) + endAppearanceTransition() + } + func replaceRefreshControlWithFake() { let fake = FakeRefreshControl() refreshControl?.allTargets.forEach{ target in