Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Image Feed UI] Load Feed Logic/UX (MVC implementation) #22

Merged
merged 16 commits into from
Nov 24, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
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
64 changes: 63 additions & 1 deletion EssentialFeed/EssentialFeed.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,11 @@
EDF8FE4E2AE2389D00605C5C /* FeedCacheTestHelpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = EDF8FE4D2AE2389D00605C5C /* FeedCacheTestHelpers.swift */; };
EDF8FE502AE2390B00605C5C /* SharedTestHelpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = EDF8FE4F2AE2390B00605C5C /* SharedTestHelpers.swift */; };
EDF8FE522AE2470300605C5C /* FeedCachePolicy.swift in Sources */ = {isa = PBXBuildFile; fileRef = EDF8FE512AE2470300605C5C /* FeedCachePolicy.swift */; };
EDFBE2402B10409C00EFB793 /* FeedViewControllerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EDFBE23F2B10409C00EFB793 /* FeedViewControllerTests.swift */; };
EDFBE2422B1046C600EFB793 /* EssentialFeed.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1438189629A135C0005B41C9 /* EssentialFeed.framework */; platformFilter = ios; };
EDFBE2432B1046C600EFB793 /* EssentialFeed.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 1438189629A135C0005B41C9 /* EssentialFeed.framework */; platformFilter = ios; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
EDFBE2472B1048CF00EFB793 /* XCTTestCase + MemoryLeakTracking.swift in Sources */ = {isa = PBXBuildFile; fileRef = ED3FD1222A267833006299CD /* XCTTestCase + MemoryLeakTracking.swift */; };
EDFBE2492B10534300EFB793 /* FeedViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = EDFBE2482B10534300EFB793 /* FeedViewController.swift */; };
/* End PBXBuildFile section */

/* Begin PBXContainerItemProxy section */
Expand Down Expand Up @@ -81,8 +86,29 @@
remoteGlobalIDString = 1438189529A135C0005B41C9;
remoteInfo = EssentialFeed;
};
EDFBE2442B1046C600EFB793 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 1438188D29A135C0005B41C9 /* Project object */;
proxyType = 1;
remoteGlobalIDString = 1438189529A135C0005B41C9;
remoteInfo = EssentialFeed;
};
/* End PBXContainerItemProxy section */

/* Begin PBXCopyFilesBuildPhase section */
EDFBE2462B1046C700EFB793 /* Embed Frameworks */ = {
isa = PBXCopyFilesBuildPhase;
buildActionMask = 2147483647;
dstPath = "";
dstSubfolderSpec = 10;
files = (
EDFBE2432B1046C600EFB793 /* EssentialFeed.framework in Embed Frameworks */,
);
name = "Embed Frameworks";
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXCopyFilesBuildPhase section */

/* Begin PBXFileReference section */
1438189629A135C0005B41C9 /* EssentialFeed.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = EssentialFeed.framework; sourceTree = BUILT_PRODUCTS_DIR; };
1438189929A135C0005B41C9 /* EssentialFeed.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = EssentialFeed.h; sourceTree = "<group>"; };
Expand All @@ -95,7 +121,7 @@
ED01EC782B01C5E900B405F4 /* EssentialFeedCacheIntegrationTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = EssentialFeedCacheIntegrationTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
ED01EC7A2B01C5E900B405F4 /* EssentialFeedCacheIntegrationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EssentialFeedCacheIntegrationTests.swift; sourceTree = "<group>"; };
ED01EC822B01C70700B405F4 /* EssentialFeedCacheIntegrationTests.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = EssentialFeedCacheIntegrationTests.xctestplan; sourceTree = SOURCE_ROOT; };
ED0EDEB52B0FAD55004857B7 /* EssentialFeedEndToEndTests.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = file; path = EssentialFeedEndToEndTests.xctestplan; sourceTree = SOURCE_ROOT; };
ED0EDEB52B0FAD55004857B7 /* EssentialFeedEndToEndTests.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = EssentialFeedEndToEndTests.xctestplan; sourceTree = SOURCE_ROOT; };
ED0EDEBB2B0FB680004857B7 /* EssentialFeediOS.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = EssentialFeediOS.framework; sourceTree = BUILT_PRODUCTS_DIR; };
ED0EDEC22B0FB681004857B7 /* EssentialFeediOSTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = EssentialFeediOSTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
ED0EDED02B0FB781004857B7 /* EssentialFeediOS.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = EssentialFeediOS.xctestplan; sourceTree = "<group>"; };
Expand Down Expand Up @@ -131,6 +157,8 @@
EDF8FE4D2AE2389D00605C5C /* FeedCacheTestHelpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedCacheTestHelpers.swift; sourceTree = "<group>"; };
EDF8FE4F2AE2390B00605C5C /* SharedTestHelpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SharedTestHelpers.swift; sourceTree = "<group>"; };
EDF8FE512AE2470300605C5C /* FeedCachePolicy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedCachePolicy.swift; sourceTree = "<group>"; };
EDFBE23F2B10409C00EFB793 /* FeedViewControllerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedViewControllerTests.swift; sourceTree = "<group>"; };
EDFBE2482B10534300EFB793 /* FeedViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedViewController.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */

/* Begin PBXFrameworksBuildPhase section */
Expand Down Expand Up @@ -161,6 +189,7 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
EDFBE2422B1046C600EFB793 /* EssentialFeed.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
Expand Down Expand Up @@ -194,6 +223,7 @@
ED0EDEBC2B0FB681004857B7 /* EssentialFeediOS */,
ED0EDEC62B0FB681004857B7 /* EssentialFeediOSTests */,
1438189729A135C0005B41C9 /* Products */,
EDFBE2412B1046C600EFB793 /* Frameworks */,
);
sourceTree = "<group>";
};
Expand Down Expand Up @@ -264,13 +294,15 @@
ED0EDEBC2B0FB681004857B7 /* EssentialFeediOS */ = {
isa = PBXGroup;
children = (
EDFBE2482B10534300EFB793 /* FeedViewController.swift */,
);
path = EssentialFeediOS;
sourceTree = "<group>";
};
ED0EDEC62B0FB681004857B7 /* EssentialFeediOSTests */ = {
isa = PBXGroup;
children = (
EDFBE23F2B10409C00EFB793 /* FeedViewControllerTests.swift */,
);
path = EssentialFeediOSTests;
sourceTree = "<group>";
Expand Down Expand Up @@ -380,6 +412,13 @@
path = FeedCache;
sourceTree = "<group>";
};
EDFBE2412B1046C600EFB793 /* Frameworks */ = {
isa = PBXGroup;
children = (
);
name = Frameworks;
sourceTree = "<group>";
};
/* End PBXGroup section */

/* Begin PBXHeadersBuildPhase section */
Expand Down Expand Up @@ -463,10 +502,12 @@
ED0EDEB72B0FB680004857B7 /* Sources */,
ED0EDEB82B0FB680004857B7 /* Frameworks */,
ED0EDEB92B0FB680004857B7 /* Resources */,
EDFBE2462B1046C700EFB793 /* Embed Frameworks */,
);
buildRules = (
);
dependencies = (
EDFBE2452B1046C600EFB793 /* PBXTargetDependency */,
);
name = EssentialFeediOS;
productName = EssentialFeediOS;
Expand Down Expand Up @@ -530,9 +571,11 @@
};
ED0EDEBA2B0FB680004857B7 = {
CreatedOnToolsVersion = 14.3.1;
LastSwiftMigration = 1430;
};
ED0EDEC12B0FB681004857B7 = {
CreatedOnToolsVersion = 14.3.1;
LastSwiftMigration = 1430;
};
ED555CD12A50045300447E9B = {
CreatedOnToolsVersion = 14.3;
Expand Down Expand Up @@ -669,13 +712,16 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
EDFBE2492B10534300EFB793 /* FeedViewController.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
ED0EDEBE2B0FB681004857B7 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
EDFBE2472B1048CF00EFB793 /* XCTTestCase + MemoryLeakTracking.swift in Sources */,
EDFBE2402B10409C00EFB793 /* FeedViewControllerTests.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
Expand Down Expand Up @@ -711,6 +757,12 @@
target = 1438189529A135C0005B41C9 /* EssentialFeed */;
targetProxy = ED555CD72A50045300447E9B /* PBXContainerItemProxy */;
};
EDFBE2452B1046C600EFB793 /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
platformFilter = ios;
target = 1438189529A135C0005B41C9 /* EssentialFeed */;
targetProxy = EDFBE2442B1046C600EFB793 /* PBXContainerItemProxy */;
};
/* End PBXTargetDependency section */

/* Begin XCBuildConfiguration section */
Expand Down Expand Up @@ -861,6 +913,7 @@
"@executable_path/../Frameworks",
"@loader_path/Frameworks",
);
MACOSX_DEPLOYMENT_TARGET = 12.4;
MARKETING_VERSION = 1.0;
MODULE_VERIFIER_SUPPORTED_LANGUAGES = "objective-c objective-c++";
MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu11 gnu++20";
Expand Down Expand Up @@ -895,6 +948,7 @@
"@executable_path/../Frameworks",
"@loader_path/Frameworks",
);
MACOSX_DEPLOYMENT_TARGET = 12.4;
MARKETING_VERSION = 1.0;
MODULE_VERIFIER_SUPPORTED_LANGUAGES = "objective-c objective-c++";
MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu11 gnu++20";
Expand Down Expand Up @@ -980,6 +1034,7 @@
ED0EDECB2B0FB681004857B7 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEFINES_MODULE = YES;
Expand All @@ -1005,6 +1060,7 @@
SDKROOT = iphoneos;
SKIP_INSTALL = YES;
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
};
Expand All @@ -1013,6 +1069,7 @@
ED0EDECC2B0FB681004857B7 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEFINES_MODULE = YES;
Expand Down Expand Up @@ -1047,6 +1104,8 @@
ED0EDECE2B0FB681004857B7 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = WNFP74LNV2;
Expand All @@ -1057,6 +1116,7 @@
PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = iphoneos;
SWIFT_EMIT_LOC_STRINGS = NO;
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
};
Expand All @@ -1065,6 +1125,8 @@
ED0EDECF2B0FB681004857B7 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = WNFP74LNV2;
Expand Down
33 changes: 33 additions & 0 deletions EssentialFeed/EssentialFeediOS/FeedViewController.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
//
// FeedViewController.swift
// EssentialFeediOS
//
// Created by Oluwaseun Adebanwo on 24/11/2023.
//

import UIKit
import EssentialFeed

final public class FeedViewController: UITableViewController {
private var loader: FeedLoader?

public convenience init(loader: FeedLoader) {
self.init()
self.loader = loader
}

public override func viewDidLoad() {
super.viewDidLoad()

refreshControl = UIRefreshControl()
refreshControl?.addTarget(self, action: #selector(load), for: .valueChanged)
load()
}

@objc private func load() {
refreshControl?.beginRefreshing()
loader?.load { [weak self] _ in
self?.refreshControl?.endRefreshing()
}
}
}
93 changes: 93 additions & 0 deletions EssentialFeed/EssentialFeediOSTests/FeedViewControllerTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
//
// FeedViewControllerTests.swift
// EssentialFeediOSTests
//
// Created by Oluwaseun Adebanwo on 24/11/2023.
//

import XCTest
import UIKit
import EssentialFeed
import EssentialFeediOS

final class FeedViewControllerTests: XCTestCase {

func test_loadFeedActions_requestFeedFromLoader() {
let (sut, loader) = makeSUT()

XCTAssertEqual(loader.loadCallCount, 0, "Expected no loading requests before view is loaded")

sut.loadViewIfNeeded()

XCTAssertEqual(loader.loadCallCount, 1, "Expected a loading request once view is loaded")

sut.simulateUserInitiatedFeedReload()
XCTAssertEqual(loader.loadCallCount, 2, "Expected another loading request once user initiates a reload")

sut.simulateUserInitiatedFeedReload()
XCTAssertEqual(loader.loadCallCount, 3, "Expected yet another loading request once user initiates another reload")
}

func test_loadingFeedIndicator_isVisibleWhileLoadingFeed() {
let (sut, loader) = makeSUT()

sut.loadViewIfNeeded()
XCTAssertTrue(sut.isShowingLoadingIndicator, "Expected loading indicator once view is loaded")

loader.completeFeedLoading(at: 0)
XCTAssertFalse(sut.isShowingLoadingIndicator, "Expected no loading indicator once loading is completed")

sut.simulateUserInitiatedFeedReload()
XCTAssertTrue(sut.isShowingLoadingIndicator, "Expected loading indicator once user initiates a reload")

loader.completeFeedLoading(at: 1)
XCTAssertFalse(sut.isShowingLoadingIndicator, "Expected no loading indicator once user initiated loading is completed")
}

// MARK: - Helpers

private func makeSUT(file: StaticString = #file, line: UInt = #line) -> (sut: FeedViewController, loader: LoaderSpy) {
let loader = LoaderSpy()
let sut = FeedViewController(loader: loader)
trackForMemoryLeaks(loader, file: file, line: line)
trackForMemoryLeaks(sut, file: file, line: line)
return (sut, loader)
}

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

var loadCallCount: Int {
return completions.count
}

func load(completion: @escaping (FeedLoader.Result) -> Void) {
completions.append(completion)
}

func completeFeedLoading(at index: Int) {
completions[index](.success([]))
}
}

}

private extension FeedViewController {
func simulateUserInitiatedFeedReload() {
refreshControl?.simulatePullToRefresh()
}

var isShowingLoadingIndicator: Bool {
return refreshControl?.isRefreshing == true
}
}

private extension UIRefreshControl {
func simulatePullToRefresh() {
allTargets.forEach { target in
actions(forTarget: target, forControlEvent: .valueChanged)?.forEach {
(target as NSObject).perform(Selector($0))
}
}
}
}
7 changes: 7 additions & 0 deletions Prototype/Prototype/Base.lproj/Main.storyboard
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,7 @@ Location</string>
</tableViewCellContentView>
<connections>
<outlet property="descriptionLabel" destination="yO0-i3-0sk" id="Glw-Ya-AzV"/>
<outlet property="feedImageContainer" destination="QIx-nZ-8ow" id="lq9-sx-Anm"/>
<outlet property="feedImageView" destination="XIW-mQ-UHo" id="EqY-bf-evu"/>
<outlet property="locationContainer" destination="zi1-l0-PUS" id="Sty-bL-VKV"/>
<outlet property="locationLabel" destination="cec-Ly-ymI" id="UZI-cm-cVC"/>
Expand All @@ -127,6 +128,12 @@ Location</string>
</connections>
</tableView>
<navigationItem key="navigationItem" title="My Feed" id="Y8p-eb-UyB"/>
<refreshControl key="refreshControl" opaque="NO" multipleTouchEnabled="YES" contentMode="center" insetsLayoutMarginsFromSafeArea="NO" enabled="NO" contentHorizontalAlignment="center" contentVerticalAlignment="center" id="mu9-S9-Yda">
<autoresizingMask key="autoresizingMask"/>
<connections>
<action selector="refresh" destination="zKj-AM-8g9" eventType="valueChanged" id="wjp-V2-Xel"/>
</connections>
</refreshControl>
</tableViewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="bm8-KC-aUT" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
</objects>
Expand Down
Loading