Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
047b233
Add `UIRefreshControl` to prototype to simulate async loading of the …
AndSanG Jan 15, 2026
9b9e7f2
Add shimmering animation while loading image in the prototype app
AndSanG Jan 15, 2026
51c3214
Does not load feed on init (before the view is loaded)
AndSanG Jan 15, 2026
98d834f
Load feed on view did load
AndSanG Jan 16, 2026
5733561
Replace `FeedViewControllerTests.LoaderSpy` references in the product…
AndSanG Jan 16, 2026
5cb8a96
Extract system under test (sut) creation to a factory method
AndSanG Jan 16, 2026
9ba4549
Load feed on pull to refresh
AndSanG Jan 16, 2026
5569dbc
Extract pull to refresh simulation into a reusable extension on `UIRe…
AndSanG Jan 16, 2026
4413fa3
(iOS17 Update) Add lifecycle method viewIsAppearing to FeedViewContro…
AndSanG Jan 19, 2026
b60b930
(iOS17 Update) Extract appearance simulation to an extension
AndSanG Jan 19, 2026
fd67764
(iOS17 Update) Ensure that load is called only once since viewIsAppea…
AndSanG Jan 19, 2026
d59562a
Show loading indicator on view appearance
AndSanG Jan 19, 2026
d8204ba
Hide loading indicator on loader completion
AndSanG Jan 19, 2026
7bbe4ef
Show loading indicator on pull to refresh
AndSanG Jan 19, 2026
7192ed0
Hide loading indicator on pull to refresh
AndSanG Jan 19, 2026
9c981a8
Decouple tests from specific UI controls for user initiated reloads w…
AndSanG Jan 19, 2026
7c6ee8a
Decouple tests from specific UI loading indicators with a test-specif…
AndSanG Jan 19, 2026
e77edc0
Combine relevant tests to eliminate temporal coupling bugs
AndSanG Jan 19, 2026
7b841c1
Move `FeedViewController` to production target
AndSanG Jan 19, 2026
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
70 changes: 64 additions & 6 deletions EssentialFeed/EssentialFeed.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,11 @@
231DA90C2D28B3AE00A50156 /* RemoteFeedLoader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 231DA90B2D28B3AB00A50156 /* RemoteFeedLoader.swift */; };
2370B4092EEC68FE00737DAC /* HTTPClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2370B4082EEC68FE00737DAC /* HTTPClient.swift */; };
2370B40B2EEC6BE200737DAC /* FeedItemsMapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2370B40A2EEC6BE200737DAC /* FeedItemsMapper.swift */; };
2377131E2F199AAF00D1122A /* FeedViewControllerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2377131D2F199AAF00D1122A /* FeedViewControllerTests.swift */; };
237713262F19D19000D1122A /* EssentialFeed.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 080EDEF121B6DA7E00813479 /* EssentialFeed.framework */; platformFilter = ios; };
237713272F19D19000D1122A /* EssentialFeed.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 080EDEF121B6DA7E00813479 /* EssentialFeed.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
2377132B2F19D27200D1122A /* XCTestCase+MemoryLeakTracking.swift in Sources */ = {isa = PBXBuildFile; fileRef = 23FBD0BA2EF0F595005E9351 /* XCTestCase+MemoryLeakTracking.swift */; };
2377132D2F1EC9D800D1122A /* FeedViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2377132C2F1EC94D00D1122A /* FeedViewController.swift */; };
23883FA52EFC50C100C80E74 /* LocalFeedLoader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 23883FA42EFC50C100C80E74 /* LocalFeedLoader.swift */; };
23883FA72EFC51FC00C80E74 /* FeedStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 23883FA62EFC51F600C80E74 /* FeedStore.swift */; };
23883FA92EFF12CE00C80E74 /* RemoteFeedItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 23883FA82EFF12CE00C80E74 /* RemoteFeedItem.swift */; };
Expand Down Expand Up @@ -59,6 +64,13 @@
remoteGlobalIDString = 080EDEF021B6DA7E00813479;
remoteInfo = EssentialFeed;
};
237713282F19D19000D1122A /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 080EDEE821B6DA7E00813479 /* Project object */;
proxyType = 1;
remoteGlobalIDString = 080EDEF021B6DA7E00813479;
remoteInfo = EssentialFeed;
};
23F5D6E42F182DE900AC7D9F /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 080EDEE821B6DA7E00813479 /* Project object */;
Expand All @@ -82,6 +94,20 @@
};
/* End PBXContainerItemProxy section */

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

/* Begin PBXFileReference section */
080EDEF121B6DA7E00813479 /* EssentialFeed.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = EssentialFeed.framework; sourceTree = BUILT_PRODUCTS_DIR; };
080EDEF521B6DA7E00813479 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
Expand All @@ -92,6 +118,8 @@
231DA90B2D28B3AB00A50156 /* RemoteFeedLoader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoteFeedLoader.swift; sourceTree = "<group>"; };
2370B4082EEC68FE00737DAC /* HTTPClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HTTPClient.swift; sourceTree = "<group>"; };
2370B40A2EEC6BE200737DAC /* FeedItemsMapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedItemsMapper.swift; sourceTree = "<group>"; };
2377131D2F199AAF00D1122A /* FeedViewControllerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedViewControllerTests.swift; sourceTree = "<group>"; };
2377132C2F1EC94D00D1122A /* FeedViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedViewController.swift; sourceTree = "<group>"; };
23883FA42EFC50C100C80E74 /* LocalFeedLoader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalFeedLoader.swift; sourceTree = "<group>"; };
23883FA62EFC51F600C80E74 /* FeedStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedStore.swift; sourceTree = "<group>"; };
23883FA82EFF12CE00C80E74 /* RemoteFeedItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoteFeedItem.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -151,6 +179,7 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
237713262F19D19000D1122A /* EssentialFeed.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
Expand Down Expand Up @@ -190,6 +219,7 @@
23FDEB372F089193004D2CD0 /* EssentialFeedCacheIntegrationTests */,
23F5D6F02F182E5600AC7D9F /* EssentialFeediOS */,
23F5D6EF2F182E5600AC7D9F /* EssentialFeediOSTests */,
237713252F19D19000D1122A /* Frameworks */,
080EDEF221B6DA7E00813479 /* Products */,
);
sourceTree = "<group>";
Expand Down Expand Up @@ -251,6 +281,13 @@
path = "Feed API";
sourceTree = "<group>";
};
237713252F19D19000D1122A /* Frameworks */ = {
isa = PBXGroup;
children = (
);
name = Frameworks;
sourceTree = "<group>";
};
23883FA32EFC50A400C80E74 /* Feed Cache */ = {
isa = PBXGroup;
children = (
Expand All @@ -267,13 +304,15 @@
isa = PBXGroup;
children = (
23F5D6F12F182FC900AC7D9F /* EssentialFeediOS.xctestplan */,
2377131D2F199AAF00D1122A /* FeedViewControllerTests.swift */,
);
path = EssentialFeediOSTests;
sourceTree = "<group>";
};
23F5D6F02F182E5600AC7D9F /* EssentialFeediOS */ = {
isa = PBXGroup;
children = (
2377132C2F1EC94D00D1122A /* FeedViewController.swift */,
23F5D6FA2F18345C00AC7D9F /* CI_iOS.xctestplan */,
);
path = EssentialFeediOS;
Expand Down Expand Up @@ -433,10 +472,12 @@
23F5D6D62F182DE800AC7D9F /* Sources */,
23F5D6D72F182DE800AC7D9F /* Frameworks */,
23F5D6D82F182DE800AC7D9F /* Resources */,
2377132A2F19D19000D1122A /* Embed Frameworks */,
);
buildRules = (
);
dependencies = (
237713292F19D19000D1122A /* PBXTargetDependency */,
);
name = EssentialFeediOS;
packageProductDependencies = (
Expand Down Expand Up @@ -529,6 +570,7 @@
};
23F5D6E12F182DE900AC7D9F = {
CreatedOnToolsVersion = 26.0.1;
LastSwiftMigration = 2600;
};
23FBD0C52EF1BDE2005E9351 = {
CreatedOnToolsVersion = 26.0.1;
Expand Down Expand Up @@ -656,13 +698,16 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
2377132D2F1EC9D800D1122A /* FeedViewController.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
23F5D6DE2F182DE900AC7D9F /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
2377131E2F199AAF00D1122A /* FeedViewControllerTests.swift in Sources */,
2377132B2F19D27200D1122A /* XCTestCase+MemoryLeakTracking.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
Expand Down Expand Up @@ -695,6 +740,12 @@
target = 080EDEF021B6DA7E00813479 /* EssentialFeed */;
targetProxy = 080EDEFC21B6DA7E00813479 /* PBXContainerItemProxy */;
};
237713292F19D19000D1122A /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
platformFilter = ios;
target = 080EDEF021B6DA7E00813479 /* EssentialFeed */;
targetProxy = 237713282F19D19000D1122A /* PBXContainerItemProxy */;
};
23F5D6E52F182DE900AC7D9F /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
target = 23F5D6D92F182DE800AC7D9F /* EssentialFeediOS */;
Expand Down Expand Up @@ -870,6 +921,7 @@
"@executable_path/../Frameworks",
"@loader_path/Frameworks",
);
MACOSX_DEPLOYMENT_TARGET = 26.0;
MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu11 gnu++14";
PRODUCT_BUNDLE_IDENTIFIER = com.essentialdeveloper.EssentialFeed;
PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
Expand Down Expand Up @@ -901,6 +953,7 @@
"@executable_path/../Frameworks",
"@loader_path/Frameworks",
);
MACOSX_DEPLOYMENT_TARGET = 26.0;
MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu11 gnu++14";
PRODUCT_BUNDLE_IDENTIFIER = com.essentialdeveloper.EssentialFeed;
PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
Expand All @@ -924,6 +977,7 @@
"@executable_path/../Frameworks",
"@loader_path/../Frameworks",
);
MACOSX_DEPLOYMENT_TARGET = 26.0;
PRODUCT_BUNDLE_IDENTIFIER = com.essentialdeveloper.EssentialFeedTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SUPPORTED_PLATFORMS = "macosx iphoneos iphonesimulator";
Expand All @@ -947,6 +1001,7 @@
"@executable_path/../Frameworks",
"@loader_path/../Frameworks",
);
MACOSX_DEPLOYMENT_TARGET = 26.0;
PRODUCT_BUNDLE_IDENTIFIER = com.essentialdeveloper.EssentialFeedTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SUPPORTED_PLATFORMS = "macosx iphoneos iphonesimulator";
Expand All @@ -957,7 +1012,7 @@
23F5D6E92F182DE900AC7D9F /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
BUILD_LIBRARY_FOR_DISTRIBUTION = YES;
BUILD_LIBRARY_FOR_DISTRIBUTION = NO;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
Expand All @@ -970,7 +1025,7 @@
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_KEY_NSHumanReadableCopyright = "";
INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks";
IPHONEOS_DEPLOYMENT_TARGET = 26.0;
IPHONEOS_DEPLOYMENT_TARGET = 26.1;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
Expand Down Expand Up @@ -999,7 +1054,7 @@
23F5D6EA2F182DE900AC7D9F /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
BUILD_LIBRARY_FOR_DISTRIBUTION = YES;
BUILD_LIBRARY_FOR_DISTRIBUTION = NO;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
Expand All @@ -1012,7 +1067,7 @@
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_KEY_NSHumanReadableCopyright = "";
INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks";
IPHONEOS_DEPLOYMENT_TARGET = 26.0;
IPHONEOS_DEPLOYMENT_TARGET = 26.1;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
Expand Down Expand Up @@ -1042,12 +1097,13 @@
isa = XCBuildConfiguration;
buildSettings = {
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = YLLF7E9854;
GCC_C_LANGUAGE_STANDARD = gnu17;
GENERATE_INFOPLIST_FILE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 26.0;
IPHONEOS_DEPLOYMENT_TARGET = 26.1;
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.dapeter.EssentialFeediOSTests;
Expand All @@ -1057,6 +1113,7 @@
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)";
SWIFT_APPROACHABLE_CONCURRENCY = YES;
SWIFT_EMIT_LOC_STRINGS = NO;
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
Expand All @@ -1067,12 +1124,13 @@
isa = XCBuildConfiguration;
buildSettings = {
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = YLLF7E9854;
GCC_C_LANGUAGE_STANDARD = gnu17;
GENERATE_INFOPLIST_FILE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 26.0;
IPHONEOS_DEPLOYMENT_TARGET = 26.1;
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.dapeter.EssentialFeediOSTests;
Expand Down
42 changes: 42 additions & 0 deletions EssentialFeed/EssentialFeediOS/FeedViewController.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
//
// FeedViewController.swift
// EssentialFeed
//
// Created by Andres Sanchez on 19/1/26.
//

import UIKit
import EssentialFeed

final public class FeedViewController: UITableViewController {
private var loader: FeedLoader?
private var onViewIsAppearing: ((FeedViewController) -> Void)?

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)
onViewIsAppearing = { vc in
vc.onViewIsAppearing = nil
vc.load()
}
}

public override func viewIsAppearing(_ animated: Bool) {
super.viewIsAppearing(animated)
onViewIsAppearing?(self)
}

@objc private func load() {
refreshControl?.beginRefreshing()
loader?.load { [weak self] _ in
self?.refreshControl?.endRefreshing()
}
}
}
Loading