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 @@