Skip to content

Commit

Permalink
Render loaded feed on successful load completion
Browse files Browse the repository at this point in the history
  • Loading branch information
TrabelsiAchraf committed Nov 26, 2021
1 parent 5a82e6b commit 2121127
Show file tree
Hide file tree
Showing 4 changed files with 115 additions and 3 deletions.
4 changes: 4 additions & 0 deletions EssentialFeed.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
FD4C0809275127AA00464864 /* EssentialFeed.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 164CFB1A2710941C00F36330 /* EssentialFeed.framework */; platformFilter = ios; };
FD4C080A275127AA00464864 /* EssentialFeed.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 164CFB1A2710941C00F36330 /* EssentialFeed.framework */; platformFilter = ios; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
FD4C080F2751286800464864 /* FeedViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD4C080E2751286800464864 /* FeedViewController.swift */; };
FD4C081127516D3F00464864 /* FeedImageCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD4C081027516D3F00464864 /* FeedImageCell.swift */; };
FD719E562711C41700432290 /* RemoteFeedLoader.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD719E552711C41700432290 /* RemoteFeedLoader.swift */; };
FD7A5AA627482A0F0037090B /* EssentialFeedCacheIntegrationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD7A5AA527482A0F0037090B /* EssentialFeedCacheIntegrationTests.swift */; };
FD7A5AA727482A0F0037090B /* EssentialFeed.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 164CFB1A2710941C00F36330 /* EssentialFeed.framework */; };
Expand Down Expand Up @@ -129,6 +130,7 @@
FD27A527271C7CBB00030983 /* EssentialFeedAPIEndToEndTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = EssentialFeedAPIEndToEndTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
FD27A529271C7CBC00030983 /* EssentialFeedAPIEndToEndTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EssentialFeedAPIEndToEndTests.swift; sourceTree = "<group>"; };
FD4C080E2751286800464864 /* FeedViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedViewController.swift; sourceTree = "<group>"; };
FD4C081027516D3F00464864 /* FeedImageCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedImageCell.swift; sourceTree = "<group>"; };
FD719E552711C41700432290 /* RemoteFeedLoader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoteFeedLoader.swift; sourceTree = "<group>"; };
FD7A5AA327482A0F0037090B /* EssentialFeedCacheIntegrationTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = EssentialFeedCacheIntegrationTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
FD7A5AA527482A0F0037090B /* EssentialFeedCacheIntegrationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EssentialFeedCacheIntegrationTests.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -266,6 +268,7 @@
isa = PBXGroup;
children = (
FD4C080E2751286800464864 /* FeedViewController.swift */,
FD4C081027516D3F00464864 /* FeedImageCell.swift */,
);
path = EssentialFeediOS;
sourceTree = "<group>";
Expand Down Expand Up @@ -682,6 +685,7 @@
buildActionMask = 2147483647;
files = (
FD4C080F2751286800464864 /* FeedViewController.swift in Sources */,
FD4C081127516D3F00464864 /* FeedImageCell.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
Expand Down
14 changes: 14 additions & 0 deletions EssentialFeediOS/FeedImageCell.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
//
// FeedImageCell.swift
// EssentialFeediOS
//
// Created by Achraf Trabelsi on 26/11/2021.
//

import UIKit

public final class FeedImageCell: UITableViewCell {
public var locationContainer = UIView()
public var locationLabel = UILabel()
public var descriptionLabel = UILabel()
}
18 changes: 17 additions & 1 deletion EssentialFeediOS/FeedViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import EssentialFeed

final public class FeedViewController: UITableViewController {
private var loader: FeedLoader?
private var tableModel = [FeedImage]()

// We use a convenience initializer since we don't need any custom initialization.
// This way, we don't need to implement the UIViewController's required initializers!
Expand All @@ -28,8 +29,23 @@ final public class FeedViewController: UITableViewController {

@objc private func load() {
refreshControl?.beginRefreshing()
loader?.load { [weak self] _ in
loader?.load { [weak self] result in
self?.tableModel = (try? result.get()) ?? []
self?.tableView.reloadData()
self?.refreshControl?.endRefreshing()
}
}

public override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return tableModel.count
}

public override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cellModel = tableModel[indexPath.row]
let cell = FeedImageCell()
cell.locationContainer.isHidden = (cellModel.location == nil)
cell.descriptionLabel.text = cellModel.description
cell.locationLabel.text = cellModel.location
return cell
}
}
82 changes: 80 additions & 2 deletions EssentialFeediOSTests/FeedViewControllerTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,24 @@ final class FeedViewControllerTests: XCTestCase {
XCTAssertFalse(sut.isShowingLoadingIndicator, "Expected no loading indicator once user initiated loading is completed")
}

func test_loadFeedCompletion_rendersSuccessfullyLoadedFeed() {
let image0 = makeImage(description: "a description", location: "a location")
let image1 = makeImage(description: nil, location: "another location")
let image2 = makeImage(description: "another description", location: nil)
let image3 = makeImage(description: nil, location: nil)
let (sut, loader) = makeSUT()

sut.loadViewIfNeeded()
assertThat(sut, isRendering: [])

loader.completeFeedLoading(with: [image0], at: 0)
assertThat(sut, isRendering: [image0])

sut.simulateUserInitiatedFeedReload()
loader.completeFeedLoading(with: [image0, image1, image2, image3], at: 1)
assertThat(sut, isRendering: [image0, image1, image2, image3])
}

// MARK: -Helpers

private func makeSUT(file: StaticString = #file, line: UInt = #line)
Expand All @@ -53,6 +71,38 @@ final class FeedViewControllerTests: XCTestCase {
return (sut, loader)
}

private func assertThat(_ sut: FeedViewController, isRendering feed: [FeedImage],
file: StaticString = #file, line: UInt = #line) {
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)
}
}

private func assertThat(_ sut: FeedViewController, hasViewConfiguredFor image: FeedImage, at index: Int,
file: StaticString = #file, line: UInt = #line) {
let view = sut.feedImageView(at: index)

guard let cell = view as? FeedImageCell else {
return XCTFail("Expected \(FeedImageCell.self) instance, got \(String(describing: view)) instead", file: file, line: line)
}

let shouldLocationBeVisible = (image.location != nil)
XCTAssertEqual(cell.isShowingLocation, shouldLocationBeVisible, "Expected `isShowingLocation` to be \(shouldLocationBeVisible) for image view at index (\(index))", file: file, line: line)

XCTAssertEqual(cell.locationText, image.location, "Expected location text to be \(String(describing: image.location)) for image view at index (\(index))", file: file, line: line)

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 makeImage(description: String? = nil, location: String? = nil,
url: URL = URL(string: "http://any-url.com")!) -> FeedImage {
return FeedImage(id: UUID(), description: description, location: location, url: url)
}

class LoaderSpy: FeedLoader {
private var completions = [(FeedLoader.Result) -> Void]()

Expand All @@ -64,8 +114,8 @@ final class FeedViewControllerTests: XCTestCase {
completions.append(completion)
}

func completeFeedLoading(at index: Int) {
completions[index](.success([]))
func completeFeedLoading(with feed: [FeedImage] = [], at index: Int) {
completions[index](.success(feed))
}
}
}
Expand All @@ -78,6 +128,34 @@ private extension FeedViewController {
var isShowingLoadingIndicator: Bool {
refreshControl?.isRefreshing == true
}

func numberOfRenderedFeedImageViews() -> Int {
return tableView.numberOfRows(inSection: feedImagesSection)
}

func feedImageView(at row: Int) -> UITableViewCell? {
let ds = tableView.dataSource
let index = IndexPath(row: row, section: feedImagesSection)
return ds?.tableView(tableView, cellForRowAt: index)
}

private var feedImagesSection: Int {
return 0
}
}

private extension FeedImageCell {
var isShowingLocation: Bool {
return !locationContainer.isHidden
}

var locationText: String? {
return locationLabel.text
}

var descriptionText: String? {
return descriptionLabel.text
}
}

private extension UIRefreshControl {
Expand Down

0 comments on commit 2121127

Please sign in to comment.