Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions EssentialApp/EssentialAppTests/FeedUIIntegrationTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,17 @@ 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)
}

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) {
Expand All @@ -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())
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -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()

Expand Down
13 changes: 13 additions & 0 deletions EssentialApp/EssentialAppTests/Helpers/UIView+TestHelpers.swift
Original file line number Diff line number Diff line change
@@ -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())
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand All @@ -43,6 +45,7 @@ public final class FeedViewController: UITableViewController, UITableViewDataSou
}

public func display(_ cellControllers: [FeedImageCellController]) {
loadingControllers = [:]
tableModel = cellControllers
}

Expand Down Expand Up @@ -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
}
}
6 changes: 3 additions & 3 deletions EssentialFeed/EssentialFeediOS/Feed UI/Views/Feed.storyboard
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Error label" textAlignment="center" lineBreakMode="tailTruncation" numberOfLines="0" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="5A2-jn-0GF">
<rect key="frame" x="0.0" y="0.0" width="393" height="34"/>
<rect key="frame" x="0.0" y="8" width="393" height="18"/>
<gestureRecognizers/>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
Expand All @@ -33,9 +33,9 @@
<color key="backgroundColor" red="1" green="0.41568627450980389" blue="0.41568627450980389" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<gestureRecognizers/>
<constraints>
<constraint firstItem="5A2-jn-0GF" firstAttribute="top" secondItem="4rO-dP-KUh" secondAttribute="top" id="LuN-ci-cAl"/>
<constraint firstItem="5A2-jn-0GF" firstAttribute="top" secondItem="4rO-dP-KUh" secondAttribute="top" constant="8" id="LuN-ci-cAl"/>
<constraint firstItem="5A2-jn-0GF" firstAttribute="leading" secondItem="4rO-dP-KUh" secondAttribute="leading" id="SKs-H9-mIi"/>
<constraint firstAttribute="bottom" secondItem="5A2-jn-0GF" secondAttribute="bottom" id="aPC-fa-cWF"/>
<constraint firstAttribute="bottom" secondItem="5A2-jn-0GF" secondAttribute="bottom" constant="8" id="aPC-fa-cWF"/>
<constraint firstAttribute="trailing" secondItem="5A2-jn-0GF" secondAttribute="trailing" id="fXX-CP-YAQ"/>
</constraints>
<connections>
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.