diff --git a/EssentialApp/EssentialAppTests/FeedUIIntegrationTests.swift b/EssentialApp/EssentialAppTests/FeedUIIntegrationTests.swift index e00a39d..9623e3e 100644 --- a/EssentialApp/EssentialAppTests/FeedUIIntegrationTests.swift +++ b/EssentialApp/EssentialAppTests/FeedUIIntegrationTests.swift @@ -78,6 +78,20 @@ final class FeedUIIntegrationTests: XCTestCase { assertThat(sut, isRendering: [image0, image1, image2, image3]) } + func test_loadFeedCompletion_rendersSuccessfullyLoadedEmptyFeedAfterNonEmptyFeed() { + let image0 = makeImage() + let image1 = makeImage() + let (sut, loader) = makeSUT() + + sut.simulateAppearance() + loader.completeFeedLoading(with: [image0, image1], at: 0) + assertThat(sut, isRendering: [image0, image1]) + + sut.simulateUserInitiatedFeedReload() + loader.completeFeedLoading(with: [], at: 1) + assertThat(sut, isRendering: []) + } + func test_loadFeedCompletion_doesNotAlterCurrentRenderingStateOnError() { let image0 = makeImage() let (sut, loader) = makeSUT() diff --git a/EssentialApp/EssentialAppTests/Helpers/FeedUIIntegrationTests+Assertions.swift b/EssentialApp/EssentialAppTests/Helpers/FeedUIIntegrationTests+Assertions.swift index 6310627..8d8f6eb 100644 --- a/EssentialApp/EssentialAppTests/Helpers/FeedUIIntegrationTests+Assertions.swift +++ b/EssentialApp/EssentialAppTests/Helpers/FeedUIIntegrationTests+Assertions.swift @@ -10,6 +10,8 @@ import EssentialFeediOS extension FeedUIIntegrationTests { func assertThat(_ sut: FeedViewController, isRendering feed: [FeedImage], file: StaticString = #file, line: UInt = #line) { + sut.view.enforceLayoutCycle() + guard sut.numberOfRenderedFeedImageViews() == feed.count else { return XCTFail("Expected \(feed.count) images, got \(sut.numberOfRenderedFeedImageViews()) instead.", file: file, line: line) } @@ -17,6 +19,8 @@ extension FeedUIIntegrationTests { feed.enumerated().forEach { index, image in assertThat(sut, hasViewConfiguredFor: image, at: index, file: file, line: line) } + + executeRunLoopToCleanUpReferences() } func assertThat(_ sut: FeedViewController, hasViewConfiguredFor image: FeedImage, at index: Int, file: StaticString = #file, line: UInt = #line) { @@ -34,4 +38,8 @@ extension FeedUIIntegrationTests { XCTAssertEqual(cell.descriptionText, image.description, "Expected description text to be \(String(describing: image.description)) for image view at index (\(index)", file: file, line: line) } + private func executeRunLoopToCleanUpReferences() { + RunLoop.current.run(until: Date()) + } + } diff --git a/EssentialApp/EssentialAppTests/Helpers/FeedViewController+TestHelpers.swift b/EssentialApp/EssentialAppTests/Helpers/FeedViewController+TestHelpers.swift index f2d37fc..3ffc8c4 100644 --- a/EssentialApp/EssentialAppTests/Helpers/FeedViewController+TestHelpers.swift +++ b/EssentialApp/EssentialAppTests/Helpers/FeedViewController+TestHelpers.swift @@ -18,9 +18,14 @@ extension FeedViewController { } private func prepareForFirstAppearance() { + setSmallFrameToPreventRenderingCells() replaceRefreshControlWithFakeForiOS17PlusSupport() } + private func setSmallFrameToPreventRenderingCells() { + tableView.frame = CGRect(x: 0, y: 0, width: 390, height: 1) + } + private func replaceRefreshControlWithFakeForiOS17PlusSupport() { let spyRefreshControl = UIRefreshControlSpy() diff --git a/EssentialApp/EssentialAppTests/Helpers/UIView+TestHelpers.swift b/EssentialApp/EssentialAppTests/Helpers/UIView+TestHelpers.swift new file mode 100644 index 0000000..ba8a865 --- /dev/null +++ b/EssentialApp/EssentialAppTests/Helpers/UIView+TestHelpers.swift @@ -0,0 +1,13 @@ +// +// Created by Rodrigo Porto. +// Copyright © 2025 PortoCode. All Rights Reserved. +// + +import UIKit + +extension UIView { + func enforceLayoutCycle() { + layoutIfNeeded() + RunLoop.current.run(until: Date()) + } +} diff --git a/EssentialFeed/EssentialFeediOS/Feed UI/Controllers/FeedViewController.swift b/EssentialFeed/EssentialFeediOS/Feed UI/Controllers/FeedViewController.swift index ad207f4..4e0e034 100644 --- a/EssentialFeed/EssentialFeediOS/Feed UI/Controllers/FeedViewController.swift +++ b/EssentialFeed/EssentialFeediOS/Feed UI/Controllers/FeedViewController.swift @@ -13,12 +13,14 @@ public protocol FeedViewControllerDelegate { public final class FeedViewController: UITableViewController, UITableViewDataSourcePrefetching, FeedLoadingView, FeedErrorView { @IBOutlet private(set) public var errorView: ErrorView? + private var loadingControllers = [IndexPath: FeedImageCellController]() + private var tableModel = [FeedImageCellController]() { didSet { tableView.reloadData() } } private var onViewIsAppearing: ((FeedViewController) -> Void)? - + public var delegate: FeedViewControllerDelegate? public override func viewDidLoad() { @@ -43,6 +45,7 @@ public final class FeedViewController: UITableViewController, UITableViewDataSou } public func display(_ cellControllers: [FeedImageCellController]) { + loadingControllers = [:] tableModel = cellControllers } @@ -83,10 +86,13 @@ public final class FeedViewController: UITableViewController, UITableViewDataSou } private func cellController(forRowAt indexPath: IndexPath) -> FeedImageCellController { - return tableModel[indexPath.row] + let controller = tableModel[indexPath.row] + loadingControllers[indexPath] = controller + return controller } private func cancelCellControllerLoad(forRowAt indexPath: IndexPath) { - cellController(forRowAt: indexPath).cancelLoad() + loadingControllers[indexPath]?.cancelLoad() + loadingControllers[indexPath] = nil } } diff --git a/EssentialFeed/EssentialFeediOS/Feed UI/Views/Feed.storyboard b/EssentialFeed/EssentialFeediOS/Feed UI/Views/Feed.storyboard index 40fea39..6c4bc8c 100644 --- a/EssentialFeed/EssentialFeediOS/Feed UI/Views/Feed.storyboard +++ b/EssentialFeed/EssentialFeediOS/Feed UI/Views/Feed.storyboard @@ -20,7 +20,7 @@