From b80908d1c3b9790e8c0679b308c4f5d0226ade79 Mon Sep 17 00:00:00 2001 From: Octavio Rojas Date: Wed, 13 Nov 2024 10:03:48 -0600 Subject: [PATCH 01/14] Replace custom `LoadFeedResult` enum with the standard `Swift.Result`. --- EssentialFeed/EssentialFeed/Feed Feature/FeedLoader.swift | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/EssentialFeed/EssentialFeed/Feed Feature/FeedLoader.swift b/EssentialFeed/EssentialFeed/Feed Feature/FeedLoader.swift index acd3d92..861aa06 100644 --- a/EssentialFeed/EssentialFeed/Feed Feature/FeedLoader.swift +++ b/EssentialFeed/EssentialFeed/Feed Feature/FeedLoader.swift @@ -4,10 +4,7 @@ import Foundation -public enum LoadFeedResult { - case success([FeedImage]) - case failure(Error) -} +public typealias LoadFeedResult = Result<[FeedImage], Error> public protocol FeedLoader { func load(completion: @escaping (LoadFeedResult) -> Void) From f7e4a3bf6b4651b1afe25a66adf37af0135fabb4 Mon Sep 17 00:00:00 2001 From: Octavio Rojas Date: Wed, 13 Nov 2024 10:07:14 -0600 Subject: [PATCH 02/14] Nest `LoadFeedResult` into the `FeedLoader` protocol as `FeedLoader.Result` since they're closely related. --- EssentialFeed/EssentialFeed/Feed API/RemoteFeedLoader.swift | 2 +- EssentialFeed/EssentialFeed/Feed Cache/LocalFeedLoader.swift | 2 +- EssentialFeed/EssentialFeed/Feed Feature/FeedLoader.swift | 5 +++-- .../EssentialFeedAPIEndToEndTests.swift | 4 ++-- 4 files changed, 7 insertions(+), 6 deletions(-) diff --git a/EssentialFeed/EssentialFeed/Feed API/RemoteFeedLoader.swift b/EssentialFeed/EssentialFeed/Feed API/RemoteFeedLoader.swift index 6f80d0a..ca7db32 100644 --- a/EssentialFeed/EssentialFeed/Feed API/RemoteFeedLoader.swift +++ b/EssentialFeed/EssentialFeed/Feed API/RemoteFeedLoader.swift @@ -21,7 +21,7 @@ public final class RemoteFeedLoader: FeedLoader { case invalidData } - public typealias Result = LoadFeedResult + public typealias Result = FeedLoader.Result public func load(completion: @escaping (Result) -> Void) { client.get(from: url) { [weak self] result in diff --git a/EssentialFeed/EssentialFeed/Feed Cache/LocalFeedLoader.swift b/EssentialFeed/EssentialFeed/Feed Cache/LocalFeedLoader.swift index 4efbe77..0bab9f1 100644 --- a/EssentialFeed/EssentialFeed/Feed Cache/LocalFeedLoader.swift +++ b/EssentialFeed/EssentialFeed/Feed Cache/LocalFeedLoader.swift @@ -42,7 +42,7 @@ extension LocalFeedLoader { } extension LocalFeedLoader: FeedLoader { - public typealias LoadResult = LoadFeedResult + public typealias LoadResult = FeedLoader.Result public func load(completion: @escaping (LoadResult) -> Void) { store.retrieve { [weak self] result in diff --git a/EssentialFeed/EssentialFeed/Feed Feature/FeedLoader.swift b/EssentialFeed/EssentialFeed/Feed Feature/FeedLoader.swift index 861aa06..5114e96 100644 --- a/EssentialFeed/EssentialFeed/Feed Feature/FeedLoader.swift +++ b/EssentialFeed/EssentialFeed/Feed Feature/FeedLoader.swift @@ -4,8 +4,9 @@ import Foundation -public typealias LoadFeedResult = Result<[FeedImage], Error> public protocol FeedLoader { - func load(completion: @escaping (LoadFeedResult) -> Void) + typealias Result = Swift.Result<[FeedImage], Error> + + func load(completion: @escaping (FeedLoader.Result) -> Void) } diff --git a/EssentialFeed/EssentialFeedAPIEndToEndTests/EssentialFeedAPIEndToEndTests.swift b/EssentialFeed/EssentialFeedAPIEndToEndTests/EssentialFeedAPIEndToEndTests.swift index 5c4bf0a..a37c5b2 100644 --- a/EssentialFeed/EssentialFeedAPIEndToEndTests/EssentialFeedAPIEndToEndTests.swift +++ b/EssentialFeed/EssentialFeedAPIEndToEndTests/EssentialFeedAPIEndToEndTests.swift @@ -30,7 +30,7 @@ class EssentialFeedAPIEndToEndTests: XCTestCase { } } - private func getFeedResult(file: StaticString = #filePath, line: UInt = #line) -> LoadFeedResult? { + private func getFeedResult(file: StaticString = #filePath, line: UInt = #line) -> FeedLoader.Result? { let testServerURL = URL(string: "https://essentialdeveloper.com/feed-case-study/test-api/feed")! let client = URLSessionHTTPClient(session: URLSession(configuration: .ephemeral)) let loader = RemoteFeedLoader(url: testServerURL, client: client) @@ -39,7 +39,7 @@ class EssentialFeedAPIEndToEndTests: XCTestCase { let exp = expectation(description: "Wait for load completion") - var receivedResult: LoadFeedResult? + var receivedResult: FeedLoader.Result? loader.load { result in receivedResult = result exp.fulfill() From 3944c8e60a21e9ce1a9454ac4f8becdff083caee Mon Sep 17 00:00:00 2001 From: Octavio Rojas Date: Wed, 13 Nov 2024 10:12:50 -0600 Subject: [PATCH 03/14] Replace custom `HTTPClientResult` enum with a nested typealias over the standard `Swift.Result`. --- EssentialFeed/EssentialFeed/Feed API/HTTPClient.swift | 7 ++----- .../EssentialFeed/Feed API/URLSessionHTTPClient.swift | 4 ++-- .../Feed API /LoadFeedFromRemoteUseCaseTests.swift | 6 +++--- .../Feed API /URLSessionHTTPClientTests.swift | 10 +++++----- 4 files changed, 12 insertions(+), 15 deletions(-) diff --git a/EssentialFeed/EssentialFeed/Feed API/HTTPClient.swift b/EssentialFeed/EssentialFeed/Feed API/HTTPClient.swift index b2722f1..5b96488 100644 --- a/EssentialFeed/EssentialFeed/Feed API/HTTPClient.swift +++ b/EssentialFeed/EssentialFeed/Feed API/HTTPClient.swift @@ -7,13 +7,10 @@ import Foundation -public enum HTTPClientResult { - case success(Data, HTTPURLResponse) - case failure(Error) -} public protocol HTTPClient { + typealias Result = Swift.Result<(Data, HTTPURLResponse), Error> /// The completion handler can be invoked in any thread. /// Clients are responsible to dispatch to appropriate threads if needed. - func get(from url: URL, completion: @escaping (HTTPClientResult) -> Void) + func get(from url: URL, completion: @escaping (Result) -> Void) } diff --git a/EssentialFeed/EssentialFeed/Feed API/URLSessionHTTPClient.swift b/EssentialFeed/EssentialFeed/Feed API/URLSessionHTTPClient.swift index f9c0fcc..f767e43 100644 --- a/EssentialFeed/EssentialFeed/Feed API/URLSessionHTTPClient.swift +++ b/EssentialFeed/EssentialFeed/Feed API/URLSessionHTTPClient.swift @@ -16,12 +16,12 @@ public class URLSessionHTTPClient: HTTPClient { private struct UnexpectedValuesRepresentation: Error {} - public func get(from url: URL, completion: @escaping (HTTPClientResult) -> Void) { + public func get(from url: URL, completion: @escaping (HTTPClient.Result) -> Void) { session.dataTask(with: url) { data, response, error in if let error = error { completion(.failure(error)) } else if let data = data, let response = response as? HTTPURLResponse { - completion(.success(data, response)) + completion(.success((data, response))) } else { completion(.failure(UnexpectedValuesRepresentation())) } diff --git a/EssentialFeed/EssentialFeedTests/Feed API /LoadFeedFromRemoteUseCaseTests.swift b/EssentialFeed/EssentialFeedTests/Feed API /LoadFeedFromRemoteUseCaseTests.swift index d8d7659..55dd753 100644 --- a/EssentialFeed/EssentialFeedTests/Feed API /LoadFeedFromRemoteUseCaseTests.swift +++ b/EssentialFeed/EssentialFeedTests/Feed API /LoadFeedFromRemoteUseCaseTests.swift @@ -165,13 +165,13 @@ final class LoadFeedFromRemoteUseCaseTests: XCTestCase { } private class HTTPClientSpy: HTTPClient { - private var messages = [(url: URL, completion: (HTTPClientResult) -> Void)]() + private var messages = [(url: URL, completion: (HTTPClient.Result) -> Void)]() var requestedURLs: [URL] { return messages.map { $0.url } } - func get(from url: URL, completion: @escaping (HTTPClientResult) -> Void) { + func get(from url: URL, completion: @escaping (HTTPClient.Result) -> Void) { messages.append((url, completion)) } @@ -186,7 +186,7 @@ final class LoadFeedFromRemoteUseCaseTests: XCTestCase { httpVersion: nil, headerFields: nil )! - messages[index].completion(.success(data, response)) + messages[index].completion(.success((data, response))) } } diff --git a/EssentialFeed/EssentialFeedTests/Feed API /URLSessionHTTPClientTests.swift b/EssentialFeed/EssentialFeedTests/Feed API /URLSessionHTTPClientTests.swift index 0cbd962..4178bdf 100644 --- a/EssentialFeed/EssentialFeedTests/Feed API /URLSessionHTTPClientTests.swift +++ b/EssentialFeed/EssentialFeedTests/Feed API /URLSessionHTTPClientTests.swift @@ -89,18 +89,18 @@ class URLSessionHTTPClientTests: XCTestCase { case let .failure(error): return error default: - XCTFail("Expected failure, got \(result) instead", file: file, line: line) + XCTFail("Expected failure, got \(String(describing: result)) instead", file: file, line: line) return nil } } - private func resultFor(data: Data?, response: URLResponse?, error: Error?, file: StaticString = #filePath, line: UInt = #line) -> HTTPClientResult? { + private func resultFor(data: Data?, response: URLResponse?, error: Error?, file: StaticString = #filePath, line: UInt = #line) -> HTTPClient.Result? { URLProtocolStub.stub(data: data, response: response, error: error) let sut = makeSUT(file: file, line: line) let exp = expectation(description: "Wait for completion") - var receivedResult: HTTPClientResult! + var receivedResult: HTTPClient.Result! sut.get(from: anyURL()) { result in receivedResult = result @@ -115,10 +115,10 @@ class URLSessionHTTPClientTests: XCTestCase { let result = resultFor(data: data, response: response, error: error, file: file, line: line) switch result { - case let .success(data, response): + case let .success((data, response)): return (data, response) default: - XCTFail("Expected success, got \(result) instead", file: file, line: line) + XCTFail("Expected success, got \(String(describing: result)) instead", file: file, line: line) return nil } } From f05c7b92ccfb51dcca9324c12f784b2a4c9ce905 Mon Sep 17 00:00:00 2001 From: Octavio Rojas Date: Wed, 13 Nov 2024 12:33:51 -0600 Subject: [PATCH 04/14] Replace custom `RetrieveCachedFeedResult` enum with a nested typealias over the standard `Swift.Result` (`FeedStore.RetrieveResult`). --- .../EssentialFeed/Feed Cache/FeedStore.swift | 8 ++++--- .../CoreData/CoreDataFeedStore.swift | 4 ++-- .../Feed Cache/LocalFeedLoader.swift | 8 +++---- ...estCase+FailableDeleteFeedStoreSpecs.swift | 2 +- ...estCase+FailableInsertFeedStoreSpecs.swift | 2 +- .../XCTestCase+FeedStoreSpecs.swift | 22 +++++++++---------- .../Feed Cache/Helpers/FeedStoreSpy.swift | 4 ++-- 7 files changed, 26 insertions(+), 24 deletions(-) diff --git a/EssentialFeed/EssentialFeed/Feed Cache/FeedStore.swift b/EssentialFeed/EssentialFeed/Feed Cache/FeedStore.swift index 6fd86ba..61977d4 100644 --- a/EssentialFeed/EssentialFeed/Feed Cache/FeedStore.swift +++ b/EssentialFeed/EssentialFeed/Feed Cache/FeedStore.swift @@ -7,16 +7,18 @@ import Foundation -public enum RetrieveCachedFeedResult { + +public enum CachedFeed { case empty case found(feed: [LocalFeedImage], timestamp: Date) - case failure(Error) } public protocol FeedStore { typealias DeletionCompletion = (Error?) -> Void typealias InsertionCompletion = (Error?) -> Void - typealias RetrievalCompletion = (RetrieveCachedFeedResult) -> Void + + typealias RetrievalResult = Result + typealias RetrievalCompletion = (RetrievalResult) -> Void /// The completion handler can be invoked in any thread. /// Clients are responsible to dispatch to appropriate threads, if needed. diff --git a/EssentialFeed/EssentialFeed/Feed Cache/Infrastructure/CoreData/CoreDataFeedStore.swift b/EssentialFeed/EssentialFeed/Feed Cache/Infrastructure/CoreData/CoreDataFeedStore.swift index 13348de..72bf1a2 100644 --- a/EssentialFeed/EssentialFeed/Feed Cache/Infrastructure/CoreData/CoreDataFeedStore.swift +++ b/EssentialFeed/EssentialFeed/Feed Cache/Infrastructure/CoreData/CoreDataFeedStore.swift @@ -21,9 +21,9 @@ public final class CoreDataFeedStore: FeedStore { perform { context in do { if let cache = try ManagedCache.find(in: context) { - completion(.found(feed: cache.localFeed, timestamp: cache.timestamp)) + completion(.success(.found(feed: cache.localFeed, timestamp: cache.timestamp))) } else { - completion(.empty) + completion(.success(.empty)) } } catch { completion(.failure(error)) diff --git a/EssentialFeed/EssentialFeed/Feed Cache/LocalFeedLoader.swift b/EssentialFeed/EssentialFeed/Feed Cache/LocalFeedLoader.swift index 0bab9f1..2b77bc1 100644 --- a/EssentialFeed/EssentialFeed/Feed Cache/LocalFeedLoader.swift +++ b/EssentialFeed/EssentialFeed/Feed Cache/LocalFeedLoader.swift @@ -52,10 +52,10 @@ extension LocalFeedLoader: FeedLoader { case let .failure(error): completion(.failure(error)) - case let .found(feed, timestamp) where FeedCachePolicy.validate(timestamp, against: self.currentDate()): + case let .success(.found(feed, timestamp)) where FeedCachePolicy.validate(timestamp, against: self.currentDate()): completion(.success(feed.toModels())) - case .found, .empty: + case .success: completion(.success([])) } } @@ -70,9 +70,9 @@ extension LocalFeedLoader { switch result { case .failure: self.store.deleteCachedFeed{ _ in } - case let .found(_, timestamp) where !FeedCachePolicy.validate(timestamp, against: self.currentDate()): + case let .success(.found(_, timestamp)) where !FeedCachePolicy.validate(timestamp, against: self.currentDate()): self.store.deleteCachedFeed{ _ in } - case .empty, .found: break + case .success: break } } } diff --git a/EssentialFeed/EssentialFeedTests/Feed Cache/FeedStoreSpecs/XCTestCase+FailableDeleteFeedStoreSpecs.swift b/EssentialFeed/EssentialFeedTests/Feed Cache/FeedStoreSpecs/XCTestCase+FailableDeleteFeedStoreSpecs.swift index 4fcae75..e88c034 100644 --- a/EssentialFeed/EssentialFeedTests/Feed Cache/FeedStoreSpecs/XCTestCase+FailableDeleteFeedStoreSpecs.swift +++ b/EssentialFeed/EssentialFeedTests/Feed Cache/FeedStoreSpecs/XCTestCase+FailableDeleteFeedStoreSpecs.swift @@ -18,6 +18,6 @@ extension FailableDeleteFeedStoreSpecs where Self: XCTestCase { func assertThatDeleteHasNoSideEffectsOnDeletionError(on sut: FeedStore, file: StaticString = #file, line: UInt = #line) { deleteCache(from: sut) - expect(sut, toRetrieve: .empty, file: file, line: line) + expect(sut, toRetrieve: .success(.empty), file: file, line: line) } } diff --git a/EssentialFeed/EssentialFeedTests/Feed Cache/FeedStoreSpecs/XCTestCase+FailableInsertFeedStoreSpecs.swift b/EssentialFeed/EssentialFeedTests/Feed Cache/FeedStoreSpecs/XCTestCase+FailableInsertFeedStoreSpecs.swift index f2a4645..cff69ca 100644 --- a/EssentialFeed/EssentialFeedTests/Feed Cache/FeedStoreSpecs/XCTestCase+FailableInsertFeedStoreSpecs.swift +++ b/EssentialFeed/EssentialFeedTests/Feed Cache/FeedStoreSpecs/XCTestCase+FailableInsertFeedStoreSpecs.swift @@ -18,6 +18,6 @@ extension FailableInsertFeedStoreSpecs where Self: XCTestCase { func assertThatInsertHasNoSideEffectsOnInsertionError(on sut: FeedStore, file: StaticString = #file, line: UInt = #line) { insert((uniqueImageFeed().local, Date()), to: sut) - expect(sut, toRetrieve: .empty, file: file, line: line) + expect(sut, toRetrieve: .success(.empty), file: file, line: line) } } diff --git a/EssentialFeed/EssentialFeedTests/Feed Cache/FeedStoreSpecs/XCTestCase+FeedStoreSpecs.swift b/EssentialFeed/EssentialFeedTests/Feed Cache/FeedStoreSpecs/XCTestCase+FeedStoreSpecs.swift index f91303b..48fe15a 100644 --- a/EssentialFeed/EssentialFeedTests/Feed Cache/FeedStoreSpecs/XCTestCase+FeedStoreSpecs.swift +++ b/EssentialFeed/EssentialFeedTests/Feed Cache/FeedStoreSpecs/XCTestCase+FeedStoreSpecs.swift @@ -11,11 +11,11 @@ import EssentialFeed extension FeedStoreSpecs where Self: XCTestCase { func assertThatRetrieveDeliversEmptyOnEmptyCache(on sut: FeedStore, file: StaticString = #file, line: UInt = #line) { - expect(sut, toRetrieve: .empty, file: file, line: line) + expect(sut, toRetrieve: .success(.empty), file: file, line: line) } func assertThatRetrieveHasNoSideEffectsOnEmptyCache(on sut: FeedStore, file: StaticString = #file, line: UInt = #line) { - expect(sut, toRetrieveTwice: .empty, file: file, line: line) + expect(sut, toRetrieveTwice: .success(.empty), file: file, line: line) } func assertThatRetrieveDeliversFoundValuesOnNonEmptyCache(on sut: FeedStore, file: StaticString = #file, line: UInt = #line) { @@ -24,7 +24,7 @@ extension FeedStoreSpecs where Self: XCTestCase { insert((feed, timestamp), to: sut) - expect(sut, toRetrieve: .found(feed: feed, timestamp: timestamp), file: file, line: line) + expect(sut, toRetrieve: .success(.found(feed: feed, timestamp: timestamp)), file: file, line: line) } func assertThatRetrieveHasNoSideEffectsOnNonEmptyCache(on sut: FeedStore, file: StaticString = #file, line: UInt = #line) { @@ -33,7 +33,7 @@ extension FeedStoreSpecs where Self: XCTestCase { insert((feed, timestamp), to: sut) - expect(sut, toRetrieveTwice: .found(feed: feed, timestamp: timestamp), file: file, line: line) + expect(sut, toRetrieveTwice: .success(.found(feed: feed, timestamp: timestamp)), file: file, line: line) } func assertThatInsertDeliversNoErrorOnEmptyCache(on sut: FeedStore, file: StaticString = #file, line: UInt = #line) { @@ -57,7 +57,7 @@ extension FeedStoreSpecs where Self: XCTestCase { let latestTimestamp = Date() insert((latestFeed, latestTimestamp), to: sut) - expect(sut, toRetrieve: .found(feed: latestFeed, timestamp: latestTimestamp), file: file, line: line) + expect(sut, toRetrieve: .success(.found(feed: latestFeed, timestamp: latestTimestamp)), file: file, line: line) } func assertThatDeleteDeliversNoErrorOnEmptyCache(on sut: FeedStore, file: StaticString = #file, line: UInt = #line) { @@ -69,7 +69,7 @@ extension FeedStoreSpecs where Self: XCTestCase { func assertThatDeleteHasNoSideEffectsOnEmptyCache(on sut: FeedStore, file: StaticString = #file, line: UInt = #line) { deleteCache(from: sut) - expect(sut, toRetrieve: .empty, file: file, line: line) + expect(sut, toRetrieve: .success(.empty), file: file, line: line) } func assertThatDeleteDeliversNoErrorOnNonEmptyCache(on sut: FeedStore, file: StaticString = #file, line: UInt = #line) { @@ -85,7 +85,7 @@ extension FeedStoreSpecs where Self: XCTestCase { deleteCache(from: sut) - expect(sut, toRetrieve: .empty, file: file, line: line) + expect(sut, toRetrieve: .success(.empty), file: file, line: line) } func assertThatSideEffectsRunSerially(on sut: FeedStore, file: StaticString = #file, line: UInt = #line) { @@ -141,21 +141,21 @@ extension FeedStoreSpecs where Self: XCTestCase { return deletionError } - func expect(_ sut: FeedStore, toRetrieveTwice expectedResult: RetrieveCachedFeedResult, file: StaticString = #file, line: UInt = #line) { + func expect(_ sut: FeedStore, toRetrieveTwice expectedResult: FeedStore.RetrievalResult, file: StaticString = #file, line: UInt = #line) { expect(sut, toRetrieve: expectedResult, file: file, line: line) expect(sut, toRetrieve: expectedResult, file: file, line: line) } - func expect(_ sut: FeedStore, toRetrieve expectedResult: RetrieveCachedFeedResult, file: StaticString = #file, line: UInt = #line) { + func expect(_ sut: FeedStore, toRetrieve expectedResult: FeedStore.RetrievalResult, file: StaticString = #file, line: UInt = #line) { let exp = expectation(description: "Wait for cache retrieval") sut.retrieve { retrievedResult in switch (expectedResult, retrievedResult) { - case (.empty, .empty), + case (.success(.empty), .success(.empty)), (.failure, .failure): break - case let (.found(expectedFeed, expectedTimestamp), .found(retrievedFeed, retrievedTimestamp)): + case let (.success(.found(expectedFeed, expectedTimestamp)), .success(.found(retrievedFeed, retrievedTimestamp))): XCTAssertEqual(retrievedFeed, expectedFeed, file: file, line: line) XCTAssertEqual(retrievedTimestamp, expectedTimestamp, file: file, line: line) diff --git a/EssentialFeed/EssentialFeedTests/Feed Cache/Helpers/FeedStoreSpy.swift b/EssentialFeed/EssentialFeedTests/Feed Cache/Helpers/FeedStoreSpy.swift index f50d484..9bf2ced 100644 --- a/EssentialFeed/EssentialFeedTests/Feed Cache/Helpers/FeedStoreSpy.swift +++ b/EssentialFeed/EssentialFeedTests/Feed Cache/Helpers/FeedStoreSpy.swift @@ -57,10 +57,10 @@ class FeedStoreSpy: FeedStore { } func completeRetrievalWithEmptyCache(at index: Int = 0) { - retrievalCompletions[index](.empty) + retrievalCompletions[index](.success(.empty)) } func completeRetrieval(with feed: [LocalFeedImage], timestamp: Date, at index: Int = 0) { - retrievalCompletions[index](.found(feed: feed, timestamp: timestamp)) + retrievalCompletions[index](.success(.found(feed: feed, timestamp: timestamp))) } } From e9b41d77a037bb00d2a84120d6ff64e855736afe Mon Sep 17 00:00:00 2001 From: Octavio Rojas Date: Wed, 13 Nov 2024 12:53:39 -0600 Subject: [PATCH 05/14] Refactor `CachedFeed` type from `enum` to `tuple` since we can represent the absence of a value with an `Optional`. --- .../EssentialFeed/Feed Cache/FeedStore.swift | 7 ++---- .../CoreData/CoreDataFeedStore.swift | 4 ++-- .../Feed Cache/LocalFeedLoader.swift | 6 ++--- ...estCase+FailableDeleteFeedStoreSpecs.swift | 2 +- ...estCase+FailableInsertFeedStoreSpecs.swift | 2 +- .../XCTestCase+FeedStoreSpecs.swift | 22 +++++++++---------- .../Feed Cache/Helpers/FeedStoreSpy.swift | 4 ++-- 7 files changed, 22 insertions(+), 25 deletions(-) diff --git a/EssentialFeed/EssentialFeed/Feed Cache/FeedStore.swift b/EssentialFeed/EssentialFeed/Feed Cache/FeedStore.swift index 61977d4..87fc491 100644 --- a/EssentialFeed/EssentialFeed/Feed Cache/FeedStore.swift +++ b/EssentialFeed/EssentialFeed/Feed Cache/FeedStore.swift @@ -8,16 +8,13 @@ import Foundation -public enum CachedFeed { - case empty - case found(feed: [LocalFeedImage], timestamp: Date) -} +public typealias CachedFeed = (feed: [LocalFeedImage], timestamp: Date) public protocol FeedStore { typealias DeletionCompletion = (Error?) -> Void typealias InsertionCompletion = (Error?) -> Void - typealias RetrievalResult = Result + typealias RetrievalResult = Result typealias RetrievalCompletion = (RetrievalResult) -> Void /// The completion handler can be invoked in any thread. diff --git a/EssentialFeed/EssentialFeed/Feed Cache/Infrastructure/CoreData/CoreDataFeedStore.swift b/EssentialFeed/EssentialFeed/Feed Cache/Infrastructure/CoreData/CoreDataFeedStore.swift index 72bf1a2..201ff8a 100644 --- a/EssentialFeed/EssentialFeed/Feed Cache/Infrastructure/CoreData/CoreDataFeedStore.swift +++ b/EssentialFeed/EssentialFeed/Feed Cache/Infrastructure/CoreData/CoreDataFeedStore.swift @@ -21,9 +21,9 @@ public final class CoreDataFeedStore: FeedStore { perform { context in do { if let cache = try ManagedCache.find(in: context) { - completion(.success(.found(feed: cache.localFeed, timestamp: cache.timestamp))) + completion(.success(CachedFeed(feed: cache.localFeed, timestamp: cache.timestamp))) } else { - completion(.success(.empty)) + completion(.success(.none)) } } catch { completion(.failure(error)) diff --git a/EssentialFeed/EssentialFeed/Feed Cache/LocalFeedLoader.swift b/EssentialFeed/EssentialFeed/Feed Cache/LocalFeedLoader.swift index 2b77bc1..4da8de3 100644 --- a/EssentialFeed/EssentialFeed/Feed Cache/LocalFeedLoader.swift +++ b/EssentialFeed/EssentialFeed/Feed Cache/LocalFeedLoader.swift @@ -52,8 +52,8 @@ extension LocalFeedLoader: FeedLoader { case let .failure(error): completion(.failure(error)) - case let .success(.found(feed, timestamp)) where FeedCachePolicy.validate(timestamp, against: self.currentDate()): - completion(.success(feed.toModels())) + case let .success(.some(cache)) where FeedCachePolicy.validate(cache.timestamp, against: self.currentDate()): + completion(.success(cache.feed.toModels())) case .success: completion(.success([])) @@ -70,7 +70,7 @@ extension LocalFeedLoader { switch result { case .failure: self.store.deleteCachedFeed{ _ in } - case let .success(.found(_, timestamp)) where !FeedCachePolicy.validate(timestamp, against: self.currentDate()): + case let .success(.some(cache)) where !FeedCachePolicy.validate(cache.timestamp, against: self.currentDate()): self.store.deleteCachedFeed{ _ in } case .success: break } diff --git a/EssentialFeed/EssentialFeedTests/Feed Cache/FeedStoreSpecs/XCTestCase+FailableDeleteFeedStoreSpecs.swift b/EssentialFeed/EssentialFeedTests/Feed Cache/FeedStoreSpecs/XCTestCase+FailableDeleteFeedStoreSpecs.swift index e88c034..13785e1 100644 --- a/EssentialFeed/EssentialFeedTests/Feed Cache/FeedStoreSpecs/XCTestCase+FailableDeleteFeedStoreSpecs.swift +++ b/EssentialFeed/EssentialFeedTests/Feed Cache/FeedStoreSpecs/XCTestCase+FailableDeleteFeedStoreSpecs.swift @@ -18,6 +18,6 @@ extension FailableDeleteFeedStoreSpecs where Self: XCTestCase { func assertThatDeleteHasNoSideEffectsOnDeletionError(on sut: FeedStore, file: StaticString = #file, line: UInt = #line) { deleteCache(from: sut) - expect(sut, toRetrieve: .success(.empty), file: file, line: line) + expect(sut, toRetrieve: .success(.none), file: file, line: line) } } diff --git a/EssentialFeed/EssentialFeedTests/Feed Cache/FeedStoreSpecs/XCTestCase+FailableInsertFeedStoreSpecs.swift b/EssentialFeed/EssentialFeedTests/Feed Cache/FeedStoreSpecs/XCTestCase+FailableInsertFeedStoreSpecs.swift index cff69ca..086ab55 100644 --- a/EssentialFeed/EssentialFeedTests/Feed Cache/FeedStoreSpecs/XCTestCase+FailableInsertFeedStoreSpecs.swift +++ b/EssentialFeed/EssentialFeedTests/Feed Cache/FeedStoreSpecs/XCTestCase+FailableInsertFeedStoreSpecs.swift @@ -18,6 +18,6 @@ extension FailableInsertFeedStoreSpecs where Self: XCTestCase { func assertThatInsertHasNoSideEffectsOnInsertionError(on sut: FeedStore, file: StaticString = #file, line: UInt = #line) { insert((uniqueImageFeed().local, Date()), to: sut) - expect(sut, toRetrieve: .success(.empty), file: file, line: line) + expect(sut, toRetrieve: .success(.none), file: file, line: line) } } diff --git a/EssentialFeed/EssentialFeedTests/Feed Cache/FeedStoreSpecs/XCTestCase+FeedStoreSpecs.swift b/EssentialFeed/EssentialFeedTests/Feed Cache/FeedStoreSpecs/XCTestCase+FeedStoreSpecs.swift index 48fe15a..9616475 100644 --- a/EssentialFeed/EssentialFeedTests/Feed Cache/FeedStoreSpecs/XCTestCase+FeedStoreSpecs.swift +++ b/EssentialFeed/EssentialFeedTests/Feed Cache/FeedStoreSpecs/XCTestCase+FeedStoreSpecs.swift @@ -11,11 +11,11 @@ import EssentialFeed extension FeedStoreSpecs where Self: XCTestCase { func assertThatRetrieveDeliversEmptyOnEmptyCache(on sut: FeedStore, file: StaticString = #file, line: UInt = #line) { - expect(sut, toRetrieve: .success(.empty), file: file, line: line) + expect(sut, toRetrieve: .success(.none), file: file, line: line) } func assertThatRetrieveHasNoSideEffectsOnEmptyCache(on sut: FeedStore, file: StaticString = #file, line: UInt = #line) { - expect(sut, toRetrieveTwice: .success(.empty), file: file, line: line) + expect(sut, toRetrieveTwice: .success(.none), file: file, line: line) } func assertThatRetrieveDeliversFoundValuesOnNonEmptyCache(on sut: FeedStore, file: StaticString = #file, line: UInt = #line) { @@ -24,7 +24,7 @@ extension FeedStoreSpecs where Self: XCTestCase { insert((feed, timestamp), to: sut) - expect(sut, toRetrieve: .success(.found(feed: feed, timestamp: timestamp)), file: file, line: line) + expect(sut, toRetrieve: .success(.some(CachedFeed(feed: feed, timestamp: timestamp))), file: file, line: line) } func assertThatRetrieveHasNoSideEffectsOnNonEmptyCache(on sut: FeedStore, file: StaticString = #file, line: UInt = #line) { @@ -33,7 +33,7 @@ extension FeedStoreSpecs where Self: XCTestCase { insert((feed, timestamp), to: sut) - expect(sut, toRetrieveTwice: .success(.found(feed: feed, timestamp: timestamp)), file: file, line: line) + expect(sut, toRetrieveTwice: .success(.some(CachedFeed(feed: feed, timestamp: timestamp))), file: file, line: line) } func assertThatInsertDeliversNoErrorOnEmptyCache(on sut: FeedStore, file: StaticString = #file, line: UInt = #line) { @@ -57,7 +57,7 @@ extension FeedStoreSpecs where Self: XCTestCase { let latestTimestamp = Date() insert((latestFeed, latestTimestamp), to: sut) - expect(sut, toRetrieve: .success(.found(feed: latestFeed, timestamp: latestTimestamp)), file: file, line: line) + expect(sut, toRetrieve: .success(.some(CachedFeed(feed: latestFeed, timestamp: latestTimestamp))), file: file, line: line) } func assertThatDeleteDeliversNoErrorOnEmptyCache(on sut: FeedStore, file: StaticString = #file, line: UInt = #line) { @@ -69,7 +69,7 @@ extension FeedStoreSpecs where Self: XCTestCase { func assertThatDeleteHasNoSideEffectsOnEmptyCache(on sut: FeedStore, file: StaticString = #file, line: UInt = #line) { deleteCache(from: sut) - expect(sut, toRetrieve: .success(.empty), file: file, line: line) + expect(sut, toRetrieve: .success(.none), file: file, line: line) } func assertThatDeleteDeliversNoErrorOnNonEmptyCache(on sut: FeedStore, file: StaticString = #file, line: UInt = #line) { @@ -85,7 +85,7 @@ extension FeedStoreSpecs where Self: XCTestCase { deleteCache(from: sut) - expect(sut, toRetrieve: .success(.empty), file: file, line: line) + expect(sut, toRetrieve: .success(.none), file: file, line: line) } func assertThatSideEffectsRunSerially(on sut: FeedStore, file: StaticString = #file, line: UInt = #line) { @@ -151,13 +151,13 @@ extension FeedStoreSpecs where Self: XCTestCase { sut.retrieve { retrievedResult in switch (expectedResult, retrievedResult) { - case (.success(.empty), .success(.empty)), + case (.success(.none), .success(.none)), (.failure, .failure): break - case let (.success(.found(expectedFeed, expectedTimestamp)), .success(.found(retrievedFeed, retrievedTimestamp))): - XCTAssertEqual(retrievedFeed, expectedFeed, file: file, line: line) - XCTAssertEqual(retrievedTimestamp, expectedTimestamp, file: file, line: line) + case let (.success(.some(expected)), .success(.some(retrieved))): + XCTAssertEqual(retrieved.feed, expected.feed, file: file, line: line) + XCTAssertEqual(retrieved.timestamp, expected.timestamp, file: file, line: line) default: XCTFail("Expected to retrieve \(expectedResult), got \(retrievedResult) instead", file: file, line: line) diff --git a/EssentialFeed/EssentialFeedTests/Feed Cache/Helpers/FeedStoreSpy.swift b/EssentialFeed/EssentialFeedTests/Feed Cache/Helpers/FeedStoreSpy.swift index 9bf2ced..ace9256 100644 --- a/EssentialFeed/EssentialFeedTests/Feed Cache/Helpers/FeedStoreSpy.swift +++ b/EssentialFeed/EssentialFeedTests/Feed Cache/Helpers/FeedStoreSpy.swift @@ -57,10 +57,10 @@ class FeedStoreSpy: FeedStore { } func completeRetrievalWithEmptyCache(at index: Int = 0) { - retrievalCompletions[index](.success(.empty)) + retrievalCompletions[index](.success(.none)) } func completeRetrieval(with feed: [LocalFeedImage], timestamp: Date, at index: Int = 0) { - retrievalCompletions[index](.success(.found(feed: feed, timestamp: timestamp))) + retrievalCompletions[index](.success(.some(CachedFeed(feed: feed, timestamp: timestamp)))) } } From 6889738d4949b713ec83bacdceca1b3d153806b9 Mon Sep 17 00:00:00 2001 From: Octavio Rojas Date: Wed, 13 Nov 2024 12:56:33 -0600 Subject: [PATCH 06/14] Add typealiases for `FeedStore.DeletionResult` and `FeedStore.InsertionResult`. --- EssentialFeed/EssentialFeed/Feed Cache/FeedStore.swift | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/EssentialFeed/EssentialFeed/Feed Cache/FeedStore.swift b/EssentialFeed/EssentialFeed/Feed Cache/FeedStore.swift index 87fc491..5b21572 100644 --- a/EssentialFeed/EssentialFeed/Feed Cache/FeedStore.swift +++ b/EssentialFeed/EssentialFeed/Feed Cache/FeedStore.swift @@ -11,8 +11,11 @@ import Foundation public typealias CachedFeed = (feed: [LocalFeedImage], timestamp: Date) public protocol FeedStore { - typealias DeletionCompletion = (Error?) -> Void - typealias InsertionCompletion = (Error?) -> Void + typealias DeletionResult = Error? + typealias DeletionCompletion = (DeletionResult) -> Void + + typealias InsertionResult = Error? + typealias InsertionCompletion = (InsertionResult) -> Void typealias RetrievalResult = Result typealias RetrievalCompletion = (RetrievalResult) -> Void From f648a12c3e5d7bc2b6d6f5790802f663077de982 Mon Sep 17 00:00:00 2001 From: Octavio Rojas Date: Wed, 13 Nov 2024 13:19:08 -0600 Subject: [PATCH 07/14] Replace occurrences of Error? for representing operation success/failure with `Result` --- .../EssentialFeed/Feed Cache/FeedStore.swift | 4 ++-- .../Infrastructure/CoreData/CoreDataFeedStore.swift | 8 ++++---- .../EssentialFeed/Feed Cache/LocalFeedLoader.swift | 11 ++++++----- .../EssentialFeedCacheIntegrationTests.swift | 7 +++++-- .../Feed Cache/CacheFeedUseCaseTests.swift | 7 +++++-- .../FeedStoreSpecs/XCTestCase+FeedStoreSpecs.swift | 13 +++++++++---- .../Feed Cache/Helpers/FeedStoreSpy.swift | 12 ++++++------ 7 files changed, 37 insertions(+), 25 deletions(-) diff --git a/EssentialFeed/EssentialFeed/Feed Cache/FeedStore.swift b/EssentialFeed/EssentialFeed/Feed Cache/FeedStore.swift index 5b21572..7a88fea 100644 --- a/EssentialFeed/EssentialFeed/Feed Cache/FeedStore.swift +++ b/EssentialFeed/EssentialFeed/Feed Cache/FeedStore.swift @@ -11,10 +11,10 @@ import Foundation public typealias CachedFeed = (feed: [LocalFeedImage], timestamp: Date) public protocol FeedStore { - typealias DeletionResult = Error? + typealias DeletionResult = Result typealias DeletionCompletion = (DeletionResult) -> Void - typealias InsertionResult = Error? + typealias InsertionResult = Result typealias InsertionCompletion = (InsertionResult) -> Void typealias RetrievalResult = Result diff --git a/EssentialFeed/EssentialFeed/Feed Cache/Infrastructure/CoreData/CoreDataFeedStore.swift b/EssentialFeed/EssentialFeed/Feed Cache/Infrastructure/CoreData/CoreDataFeedStore.swift index 201ff8a..6557d2d 100644 --- a/EssentialFeed/EssentialFeed/Feed Cache/Infrastructure/CoreData/CoreDataFeedStore.swift +++ b/EssentialFeed/EssentialFeed/Feed Cache/Infrastructure/CoreData/CoreDataFeedStore.swift @@ -39,9 +39,9 @@ public final class CoreDataFeedStore: FeedStore { managedCache.feed = ManagedFeedImage.images(from: feed, in: context) try context.save() - completion(nil) + completion(.success(())) } catch { - completion(error) + completion(.failure(error)) } } } @@ -50,9 +50,9 @@ public final class CoreDataFeedStore: FeedStore { perform { context in do { try ManagedCache.find(in: context).map(context.delete).map(context.save) - completion(nil) + completion(.success(())) } catch { - completion(error) + completion(.failure(error)) } } } diff --git a/EssentialFeed/EssentialFeed/Feed Cache/LocalFeedLoader.swift b/EssentialFeed/EssentialFeed/Feed Cache/LocalFeedLoader.swift index 4da8de3..50980fa 100644 --- a/EssentialFeed/EssentialFeed/Feed Cache/LocalFeedLoader.swift +++ b/EssentialFeed/EssentialFeed/Feed Cache/LocalFeedLoader.swift @@ -18,16 +18,17 @@ public final class LocalFeedLoader { } extension LocalFeedLoader { - public typealias SaveResult = Error? + public typealias SaveResult = Result public func save(_ feed: [FeedImage], completion: @escaping (SaveResult) -> Void) { - store.deleteCachedFeed { [weak self] error in + store.deleteCachedFeed { [weak self] deletionResult in guard let self = self else { return } - if let cachedDeletionError = error { - completion(cachedDeletionError) - } else { + switch deletionResult { + case .success: self.cache(feed, with: completion) + case .failure(let error): + completion(.failure(error)) } } } diff --git a/EssentialFeed/EssentialFeedCacheIntegrationTests/EssentialFeedCacheIntegrationTests.swift b/EssentialFeed/EssentialFeedCacheIntegrationTests/EssentialFeedCacheIntegrationTests.swift index 61554aa..c2d8591 100644 --- a/EssentialFeed/EssentialFeedCacheIntegrationTests/EssentialFeedCacheIntegrationTests.swift +++ b/EssentialFeed/EssentialFeedCacheIntegrationTests/EssentialFeedCacheIntegrationTests.swift @@ -80,8 +80,11 @@ class EssentialFeedCacheIntegrationTests: XCTestCase { private func save(_ feed: [FeedImage], with loader: LocalFeedLoader, file: StaticString = #filePath, line: UInt = #line) { let exp = expectation(description: "Wait for save completion") - loader.save(feed) { saveError in - XCTAssertNil(saveError, "Expected to save feed successfully") + loader.save(feed) { result in + if case let Result.failure(error) = result { + XCTAssertNil(error, "Expected to save feed successfully", file: file, line: line) + } + exp.fulfill() } wait(for: [exp], timeout: 1.0) diff --git a/EssentialFeed/EssentialFeedTests/Feed Cache/CacheFeedUseCaseTests.swift b/EssentialFeed/EssentialFeedTests/Feed Cache/CacheFeedUseCaseTests.swift index dde9a90..60ecce0 100644 --- a/EssentialFeed/EssentialFeedTests/Feed Cache/CacheFeedUseCaseTests.swift +++ b/EssentialFeed/EssentialFeedTests/Feed Cache/CacheFeedUseCaseTests.swift @@ -113,8 +113,11 @@ class CacheFeedUseCaseTests: XCTestCase { let exp = expectation(description: "Wait for save completion") var receivedError: Error? - sut.save(uniqueImageFeed().models) { error in - receivedError = error + sut.save(uniqueImageFeed().models) { result in + if case let Result.failure(error) = result { + receivedError = error + } + exp.fulfill() } diff --git a/EssentialFeed/EssentialFeedTests/Feed Cache/FeedStoreSpecs/XCTestCase+FeedStoreSpecs.swift b/EssentialFeed/EssentialFeedTests/Feed Cache/FeedStoreSpecs/XCTestCase+FeedStoreSpecs.swift index 9616475..dde6f0c 100644 --- a/EssentialFeed/EssentialFeedTests/Feed Cache/FeedStoreSpecs/XCTestCase+FeedStoreSpecs.swift +++ b/EssentialFeed/EssentialFeedTests/Feed Cache/FeedStoreSpecs/XCTestCase+FeedStoreSpecs.swift @@ -121,8 +121,10 @@ extension FeedStoreSpecs where Self: XCTestCase { func insert(_ cache: (feed: [LocalFeedImage], timestamp: Date), to sut: FeedStore) -> Error? { let exp = expectation(description: "Wait for cache insertion") var insertionError: Error? - sut.insert(cache.feed, timestamp: cache.timestamp) { receivedInsertionError in - insertionError = receivedInsertionError + sut.insert(cache.feed, timestamp: cache.timestamp) { result in + if case let Result.failure(error) = result { + insertionError = error + } exp.fulfill() } wait(for: [exp], timeout: 1.0) @@ -133,8 +135,11 @@ extension FeedStoreSpecs where Self: XCTestCase { func deleteCache(from sut: FeedStore) -> Error? { let exp = expectation(description: "Wait for cache deletion") var deletionError: Error? - sut.deleteCachedFeed { receivedDeletionError in - deletionError = receivedDeletionError + sut.deleteCachedFeed { result in + if case let Result.failure(error) = result { + deletionError = error + } + exp.fulfill() } wait(for: [exp], timeout: 1.0) diff --git a/EssentialFeed/EssentialFeedTests/Feed Cache/Helpers/FeedStoreSpy.swift b/EssentialFeed/EssentialFeedTests/Feed Cache/Helpers/FeedStoreSpy.swift index ace9256..b21baf0 100644 --- a/EssentialFeed/EssentialFeedTests/Feed Cache/Helpers/FeedStoreSpy.swift +++ b/EssentialFeed/EssentialFeedTests/Feed Cache/Helpers/FeedStoreSpy.swift @@ -26,12 +26,12 @@ class FeedStoreSpy: FeedStore { receivedMessages.append(.deleteCachedFeed) } - func completeDeletion(with error: Error?, at index: Int = 0) { - deletionCompletions[index](error) + func completeDeletion(with error: Error, at index: Int = 0) { + deletionCompletions[index](.failure(error)) } func completeDeletionSuccessfully(at index: Int = 0) { - deletionCompletions[index](nil) + deletionCompletions[index](.success(())) } func insert(_ feed: [LocalFeedImage], timestamp: Date, completion: @escaping InsertionCompletion) { @@ -39,12 +39,12 @@ class FeedStoreSpy: FeedStore { receivedMessages.append(.insert(feed, timestamp)) } - func completeInsertion(with error: Error?, at index: Int = 0) { - insertionCompletions[index](error) + func completeInsertion(with error: Error, at index: Int = 0) { + insertionCompletions[index](.failure(error)) } func completeInsertionSuccessfully(at index: Int = 0) { - insertionCompletions[index](nil) + insertionCompletions[index](.success(())) } func retrieve(completion: @escaping RetrievalCompletion) { From abe2d833e258fff7f7889d1b6070397be6df1752 Mon Sep 17 00:00:00 2001 From: Octavio Rojas Date: Wed, 13 Nov 2024 13:37:48 -0600 Subject: [PATCH 08/14] Simplify CoreDataFeedStore completion code with the new Result APIs. --- .../CoreData/CoreDataFeedStore.swift | 26 ++++++------------- 1 file changed, 8 insertions(+), 18 deletions(-) diff --git a/EssentialFeed/EssentialFeed/Feed Cache/Infrastructure/CoreData/CoreDataFeedStore.swift b/EssentialFeed/EssentialFeed/Feed Cache/Infrastructure/CoreData/CoreDataFeedStore.swift index 6557d2d..70f83c9 100644 --- a/EssentialFeed/EssentialFeed/Feed Cache/Infrastructure/CoreData/CoreDataFeedStore.swift +++ b/EssentialFeed/EssentialFeed/Feed Cache/Infrastructure/CoreData/CoreDataFeedStore.swift @@ -19,41 +19,31 @@ public final class CoreDataFeedStore: FeedStore { public func retrieve(completion: @escaping RetrievalCompletion) { perform { context in - do { - if let cache = try ManagedCache.find(in: context) { - completion(.success(CachedFeed(feed: cache.localFeed, timestamp: cache.timestamp))) - } else { - completion(.success(.none)) + completion(Result { + try ManagedCache.find(in: context).map { + return CachedFeed(feed: $0.localFeed, timestamp: $0.timestamp) } - } catch { - completion(.failure(error)) - } + }) } } public func insert(_ feed: [LocalFeedImage], timestamp: Date, completion: @escaping InsertionCompletion) { perform { context in - do { + completion(Result { let managedCache = try ManagedCache.newUniqueInstance(in: context) managedCache.timestamp = timestamp managedCache.feed = ManagedFeedImage.images(from: feed, in: context) try context.save() - completion(.success(())) - } catch { - completion(.failure(error)) - } + }) } } public func deleteCachedFeed(completion: @escaping DeletionCompletion) { perform { context in - do { + completion(Result { try ManagedCache.find(in: context).map(context.delete).map(context.save) - completion(.success(())) - } catch { - completion(.failure(error)) - } + }) } } From 90482b619b60d66d51441c50433283fcf290638b Mon Sep 17 00:00:00 2001 From: Octavio Rojas Date: Wed, 13 Nov 2024 13:40:17 -0600 Subject: [PATCH 09/14] Simplify `URLSessionHTTPClient` completion code with the new `Result` APIs --- .../Feed API/URLSessionHTTPClient.swift | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/EssentialFeed/EssentialFeed/Feed API/URLSessionHTTPClient.swift b/EssentialFeed/EssentialFeed/Feed API/URLSessionHTTPClient.swift index f767e43..d6472c6 100644 --- a/EssentialFeed/EssentialFeed/Feed API/URLSessionHTTPClient.swift +++ b/EssentialFeed/EssentialFeed/Feed API/URLSessionHTTPClient.swift @@ -18,13 +18,15 @@ public class URLSessionHTTPClient: HTTPClient { public func get(from url: URL, completion: @escaping (HTTPClient.Result) -> Void) { session.dataTask(with: url) { data, response, error in - if let error = error { - completion(.failure(error)) - } else if let data = data, let response = response as? HTTPURLResponse { - completion(.success((data, response))) - } else { - completion(.failure(UnexpectedValuesRepresentation())) - } + completion(Result { + if let error = error { + throw error + } else if let data = data, let response = response as? HTTPURLResponse { + return (data, response) + } else { + throw UnexpectedValuesRepresentation() + } + }) }.resume() } } From b4e642f6cd11da73df97bb96917e8fa4ca87cef4 Mon Sep 17 00:00:00 2001 From: Octavio Rojas Date: Wed, 13 Nov 2024 15:40:20 -0600 Subject: [PATCH 10/14] Make `EssentialFeed` and `EssentialFeedTests` targets support macOS and iOS since they're platform-independent (can run on any platform!). --- EssentialFeed/EssentialFeed.xcodeproj/project.pbxproj | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/EssentialFeed/EssentialFeed.xcodeproj/project.pbxproj b/EssentialFeed/EssentialFeed.xcodeproj/project.pbxproj index 9f1b55a..d96fe89 100644 --- a/EssentialFeed/EssentialFeed.xcodeproj/project.pbxproj +++ b/EssentialFeed/EssentialFeed.xcodeproj/project.pbxproj @@ -737,6 +737,7 @@ PRODUCT_BUNDLE_IDENTIFIER = com.essentialdeveloper.EssentialFeed; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; SKIP_INSTALL = YES; + SUPPORTED_PLATFORMS = "macosx iphoneos iphonesimulator"; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; }; name = Debug; @@ -767,6 +768,7 @@ PRODUCT_BUNDLE_IDENTIFIER = com.essentialdeveloper.EssentialFeed; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; SKIP_INSTALL = YES; + SUPPORTED_PLATFORMS = "macosx iphoneos iphonesimulator"; }; name = Release; }; @@ -787,6 +789,7 @@ ); PRODUCT_BUNDLE_IDENTIFIER = com.essentialdeveloper.EssentialFeedTests; PRODUCT_NAME = "$(TARGET_NAME)"; + SUPPORTED_PLATFORMS = "macosx iphoneos iphonesimulator"; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; }; @@ -809,6 +812,7 @@ ); PRODUCT_BUNDLE_IDENTIFIER = com.essentialdeveloper.EssentialFeedTests; PRODUCT_NAME = "$(TARGET_NAME)"; + SUPPORTED_PLATFORMS = "macosx iphoneos iphonesimulator"; SWIFT_VERSION = 5.0; }; name = Release; From c3f5a73fda75334257b5ee802f339c89a883c8be Mon Sep 17 00:00:00 2001 From: Octavio Rojas Date: Wed, 13 Nov 2024 15:49:29 -0600 Subject: [PATCH 11/14] Make `EssentialFeedAPIEndToEndTests` target support macOS and iOS since it's platform-independent (can run on any platform). --- .../EssentialFeed.xcodeproj/project.pbxproj | 4 +++ .../EssentialFeedAPIEndToEndTests.xcscheme | 9 +++++-- .../EssentialFeedAPIEndToEndTests.xctestplan | 25 +++++++++++++++++++ 3 files changed, 36 insertions(+), 2 deletions(-) create mode 100644 EssentialFeed/EssentialFeedAPIEndToEndTests.xctestplan diff --git a/EssentialFeed/EssentialFeed.xcodeproj/project.pbxproj b/EssentialFeed/EssentialFeed.xcodeproj/project.pbxproj index d96fe89..5d9b182 100644 --- a/EssentialFeed/EssentialFeed.xcodeproj/project.pbxproj +++ b/EssentialFeed/EssentialFeed.xcodeproj/project.pbxproj @@ -88,6 +88,7 @@ 6E1E4CDF2CD19DB400D9C6C7 /* LocalFeedImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalFeedImage.swift; sourceTree = ""; }; 6E1E4CE12CD2C48C00D9C6C7 /* LoadFeedFromCacheUseCaseTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadFeedFromCacheUseCaseTests.swift; sourceTree = ""; }; 6E1E4CE42CD2C68600D9C6C7 /* FeedStoreSpy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedStoreSpy.swift; sourceTree = ""; }; + 6E2085BB2CE5554F00A98690 /* EssentialFeedAPIEndToEndTests.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = EssentialFeedAPIEndToEndTests.xctestplan; sourceTree = ""; }; 6E4771592CD6D94B00F55DB2 /* ValidateFeedCacheUseCaseTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ValidateFeedCacheUseCaseTests.swift; sourceTree = ""; }; 6E47715B2CD6DD5C00F55DB2 /* FeedCacheTestHelpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedCacheTestHelpers.swift; sourceTree = ""; }; 6E47715D2CD6DE6B00F55DB2 /* SharedTestHelpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SharedTestHelpers.swift; sourceTree = ""; }; @@ -157,6 +158,7 @@ 080EDEE721B6DA7E00813479 = { isa = PBXGroup; children = ( + 6E2085BB2CE5554F00A98690 /* EssentialFeedAPIEndToEndTests.xctestplan */, 6E6DA66A2CCD782400B0E31E /* README.md */, 6E5254102CC3E8F600A5B68E /* CI.xctestplan */, 080EDEF321B6DA7E00813479 /* EssentialFeed */, @@ -870,6 +872,7 @@ MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = com.tattvamusic.EssentialFeedAPIEndToEndTests; PRODUCT_NAME = "$(TARGET_NAME)"; + SUPPORTED_PLATFORMS = "macosx iphoneos iphonesimulator"; SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; SWIFT_EMIT_LOC_STRINGS = NO; SWIFT_VERSION = 5.0; @@ -890,6 +893,7 @@ MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = com.tattvamusic.EssentialFeedAPIEndToEndTests; PRODUCT_NAME = "$(TARGET_NAME)"; + SUPPORTED_PLATFORMS = "macosx iphoneos iphonesimulator"; SWIFT_EMIT_LOC_STRINGS = NO; SWIFT_VERSION = 5.0; }; diff --git a/EssentialFeed/EssentialFeed.xcodeproj/xcshareddata/xcschemes/EssentialFeedAPIEndToEndTests.xcscheme b/EssentialFeed/EssentialFeed.xcodeproj/xcshareddata/xcschemes/EssentialFeedAPIEndToEndTests.xcscheme index 251da09..57177b5 100644 --- a/EssentialFeed/EssentialFeed.xcodeproj/xcshareddata/xcschemes/EssentialFeedAPIEndToEndTests.xcscheme +++ b/EssentialFeed/EssentialFeed.xcodeproj/xcshareddata/xcschemes/EssentialFeedAPIEndToEndTests.xcscheme @@ -11,8 +11,13 @@ buildConfiguration = "Debug" selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" - shouldUseLaunchSchemeArgsEnv = "YES" - shouldAutocreateTestPlan = "YES"> + shouldUseLaunchSchemeArgsEnv = "YES"> + + + + Date: Wed, 13 Nov 2024 15:51:24 -0600 Subject: [PATCH 12/14] Make `EssentialFeedCacheIntegrationTests` target support macOS and iOS since it's platform-independent (can run on any platform). --- EssentialFeed/EssentialFeed.xcodeproj/project.pbxproj | 2 ++ 1 file changed, 2 insertions(+) diff --git a/EssentialFeed/EssentialFeed.xcodeproj/project.pbxproj b/EssentialFeed/EssentialFeed.xcodeproj/project.pbxproj index 5d9b182..561b4cb 100644 --- a/EssentialFeed/EssentialFeed.xcodeproj/project.pbxproj +++ b/EssentialFeed/EssentialFeed.xcodeproj/project.pbxproj @@ -833,6 +833,7 @@ MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = com.octavio.rojas.EssentialFeedCacheIntegrationTests; PRODUCT_NAME = "$(TARGET_NAME)"; + SUPPORTED_PLATFORMS = "macosx iphoneos iphonesimulator"; SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; SWIFT_EMIT_LOC_STRINGS = NO; SWIFT_VERSION = 5.0; @@ -853,6 +854,7 @@ MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = com.octavio.rojas.EssentialFeedCacheIntegrationTests; PRODUCT_NAME = "$(TARGET_NAME)"; + SUPPORTED_PLATFORMS = "macosx iphoneos iphonesimulator"; SWIFT_EMIT_LOC_STRINGS = NO; SWIFT_VERSION = 5.0; }; From c70f1f67fd83006f626a823975fe037ed2195c0e Mon Sep 17 00:00:00 2001 From: Octavio Rojas Date: Wed, 13 Nov 2024 16:32:21 -0600 Subject: [PATCH 13/14] Add `EssentialFeediOS` framework (prod and test) targets for iOS platform-specific components. --- .../EssentialFeed.xcodeproj/project.pbxproj | 273 ++++++++++++++++++ .../xcschemes/EssentialFeediOS.xcscheme | 85 ++++++ EssentialFeed/EssentialFeediOS.xctestplan | 33 +++ 3 files changed, 391 insertions(+) create mode 100644 EssentialFeed/EssentialFeed.xcodeproj/xcshareddata/xcschemes/EssentialFeediOS.xcscheme create mode 100644 EssentialFeed/EssentialFeediOS.xctestplan diff --git a/EssentialFeed/EssentialFeed.xcodeproj/project.pbxproj b/EssentialFeed/EssentialFeed.xcodeproj/project.pbxproj index 561b4cb..09dc1b5 100644 --- a/EssentialFeed/EssentialFeed.xcodeproj/project.pbxproj +++ b/EssentialFeed/EssentialFeed.xcodeproj/project.pbxproj @@ -16,6 +16,7 @@ 6E1E4CE02CD19DB900D9C6C7 /* LocalFeedImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E1E4CDF2CD19DB400D9C6C7 /* LocalFeedImage.swift */; }; 6E1E4CE22CD2C49500D9C6C7 /* LoadFeedFromCacheUseCaseTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E1E4CE12CD2C48C00D9C6C7 /* LoadFeedFromCacheUseCaseTests.swift */; }; 6E1E4CE52CD2C68C00D9C6C7 /* FeedStoreSpy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E1E4CE42CD2C68600D9C6C7 /* FeedStoreSpy.swift */; }; + 6E2085CB2CE55DD000A98690 /* EssentialFeediOS.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6E2085C12CE55DCF00A98690 /* EssentialFeediOS.framework */; }; 6E47715A2CD6D95300F55DB2 /* ValidateFeedCacheUseCaseTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E4771592CD6D94B00F55DB2 /* ValidateFeedCacheUseCaseTests.swift */; }; 6E47715C2CD6DD6300F55DB2 /* FeedCacheTestHelpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E47715B2CD6DD5C00F55DB2 /* FeedCacheTestHelpers.swift */; }; 6E47715E2CD6DE7000F55DB2 /* SharedTestHelpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E47715D2CD6DE6B00F55DB2 /* SharedTestHelpers.swift */; }; @@ -59,6 +60,13 @@ remoteGlobalIDString = 080EDEF021B6DA7E00813479; remoteInfo = EssentialFeed; }; + 6E2085CC2CE55DD000A98690 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 080EDEE821B6DA7E00813479 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 6E2085C02CE55DCF00A98690; + remoteInfo = EssentialFeediOS; + }; 6E4771AB2CDEA4B300F55DB2 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 080EDEE821B6DA7E00813479 /* Project object */; @@ -89,6 +97,9 @@ 6E1E4CE12CD2C48C00D9C6C7 /* LoadFeedFromCacheUseCaseTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadFeedFromCacheUseCaseTests.swift; sourceTree = ""; }; 6E1E4CE42CD2C68600D9C6C7 /* FeedStoreSpy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedStoreSpy.swift; sourceTree = ""; }; 6E2085BB2CE5554F00A98690 /* EssentialFeedAPIEndToEndTests.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = EssentialFeedAPIEndToEndTests.xctestplan; sourceTree = ""; }; + 6E2085C12CE55DCF00A98690 /* EssentialFeediOS.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = EssentialFeediOS.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 6E2085CA2CE55DD000A98690 /* EssentialFeediOSTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = EssentialFeediOSTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 6E2085E12CE55E6500A98690 /* EssentialFeediOS.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = EssentialFeediOS.xctestplan; sourceTree = ""; }; 6E4771592CD6D94B00F55DB2 /* ValidateFeedCacheUseCaseTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ValidateFeedCacheUseCaseTests.swift; sourceTree = ""; }; 6E47715B2CD6DD5C00F55DB2 /* FeedCacheTestHelpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedCacheTestHelpers.swift; sourceTree = ""; }; 6E47715D2CD6DE6B00F55DB2 /* SharedTestHelpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SharedTestHelpers.swift; sourceTree = ""; }; @@ -136,6 +147,21 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + 6E2085BE2CE55DCF00A98690 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 6E2085C72CE55DD000A98690 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 6E2085CB2CE55DD000A98690 /* EssentialFeediOS.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; 6E4771A32CDEA4B300F55DB2 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; @@ -158,6 +184,7 @@ 080EDEE721B6DA7E00813479 = { isa = PBXGroup; children = ( + 6E2085E12CE55E6500A98690 /* EssentialFeediOS.xctestplan */, 6E2085BB2CE5554F00A98690 /* EssentialFeedAPIEndToEndTests.xctestplan */, 6E6DA66A2CCD782400B0E31E /* README.md */, 6E5254102CC3E8F600A5B68E /* CI.xctestplan */, @@ -165,6 +192,8 @@ 080EDEFE21B6DA7E00813479 /* EssentialFeedTests */, 6E6DA6772CCE7F9A00B0E31E /* EssentialFeedAPIEndToEndTests */, 6E4771B12CDEA4BA00F55DB2 /* EssentialFeedCacheIntegrationTests */, + 6E2085DB2CE55E0500A98690 /* EssentialFeediOS */, + 6E2085DF2CE55E0900A98690 /* EssentialFeediOSTests */, 080EDEF221B6DA7E00813479 /* Products */, ); sourceTree = ""; @@ -176,6 +205,8 @@ 080EDEFA21B6DA7E00813479 /* EssentialFeedTests.xctest */, 6E5625732CC34F4800F339F1 /* EssentialFeedAPIEndToEndTests.xctest */, 6E4771A62CDEA4B300F55DB2 /* EssentialFeedCacheIntegrationTests.xctest */, + 6E2085C12CE55DCF00A98690 /* EssentialFeediOS.framework */, + 6E2085CA2CE55DD000A98690 /* EssentialFeediOSTests.xctest */, ); name = Products; sourceTree = ""; @@ -232,6 +263,20 @@ path = Helpers; sourceTree = ""; }; + 6E2085DB2CE55E0500A98690 /* EssentialFeediOS */ = { + isa = PBXGroup; + children = ( + ); + path = EssentialFeediOS; + sourceTree = ""; + }; + 6E2085DF2CE55E0900A98690 /* EssentialFeediOSTests */ = { + isa = PBXGroup; + children = ( + ); + path = EssentialFeediOSTests; + sourceTree = ""; + }; 6E4771942CDE5B1200F55DB2 /* FeedStoreSpecs */ = { isa = PBXGroup; children = ( @@ -333,6 +378,13 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + 6E2085BC2CE55DCF00A98690 /* Headers */ = { + isa = PBXHeadersBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXHeadersBuildPhase section */ /* Begin PBXNativeTarget section */ @@ -372,6 +424,46 @@ productReference = 080EDEFA21B6DA7E00813479 /* EssentialFeedTests.xctest */; productType = "com.apple.product-type.bundle.unit-test"; }; + 6E2085C02CE55DCF00A98690 /* EssentialFeediOS */ = { + isa = PBXNativeTarget; + buildConfigurationList = 6E2085D32CE55DD000A98690 /* Build configuration list for PBXNativeTarget "EssentialFeediOS" */; + buildPhases = ( + 6E2085BC2CE55DCF00A98690 /* Headers */, + 6E2085BD2CE55DCF00A98690 /* Sources */, + 6E2085BE2CE55DCF00A98690 /* Frameworks */, + 6E2085BF2CE55DCF00A98690 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = EssentialFeediOS; + packageProductDependencies = ( + ); + productName = EssentialFeediOS; + productReference = 6E2085C12CE55DCF00A98690 /* EssentialFeediOS.framework */; + productType = "com.apple.product-type.framework"; + }; + 6E2085C92CE55DD000A98690 /* EssentialFeediOSTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 6E2085D62CE55DD000A98690 /* Build configuration list for PBXNativeTarget "EssentialFeediOSTests" */; + buildPhases = ( + 6E2085C62CE55DD000A98690 /* Sources */, + 6E2085C72CE55DD000A98690 /* Frameworks */, + 6E2085C82CE55DD000A98690 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 6E2085CD2CE55DD000A98690 /* PBXTargetDependency */, + ); + name = EssentialFeediOSTests; + packageProductDependencies = ( + ); + productName = EssentialFeediOSTests; + productReference = 6E2085CA2CE55DD000A98690 /* EssentialFeediOSTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; 6E4771A52CDEA4B300F55DB2 /* EssentialFeedCacheIntegrationTests */ = { isa = PBXNativeTarget; buildConfigurationList = 6E4771AD2CDEA4B300F55DB2 /* Build configuration list for PBXNativeTarget "EssentialFeedCacheIntegrationTests" */; @@ -431,6 +523,12 @@ CreatedOnToolsVersion = 10.1; LastSwiftMigration = 1540; }; + 6E2085C02CE55DCF00A98690 = { + CreatedOnToolsVersion = 16.0; + }; + 6E2085C92CE55DD000A98690 = { + CreatedOnToolsVersion = 16.0; + }; 6E4771A52CDEA4B300F55DB2 = { CreatedOnToolsVersion = 16.0; }; @@ -456,6 +554,8 @@ 080EDEF921B6DA7E00813479 /* EssentialFeedTests */, 6E5625722CC34F4800F339F1 /* EssentialFeedAPIEndToEndTests */, 6E4771A52CDEA4B300F55DB2 /* EssentialFeedCacheIntegrationTests */, + 6E2085C02CE55DCF00A98690 /* EssentialFeediOS */, + 6E2085C92CE55DD000A98690 /* EssentialFeediOSTests */, ); }; /* End PBXProject section */ @@ -476,6 +576,20 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + 6E2085BF2CE55DCF00A98690 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 6E2085C82CE55DD000A98690 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; 6E4771A42CDEA4B300F55DB2 /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; @@ -538,6 +652,20 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + 6E2085BD2CE55DCF00A98690 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 6E2085C62CE55DD000A98690 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; 6E4771A22CDEA4B300F55DB2 /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; @@ -567,6 +695,11 @@ target = 080EDEF021B6DA7E00813479 /* EssentialFeed */; targetProxy = 080EDEFC21B6DA7E00813479 /* PBXContainerItemProxy */; }; + 6E2085CD2CE55DD000A98690 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 6E2085C02CE55DCF00A98690 /* EssentialFeediOS */; + targetProxy = 6E2085CC2CE55DD000A98690 /* PBXContainerItemProxy */; + }; 6E4771AC2CDEA4B300F55DB2 /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = 080EDEF021B6DA7E00813479 /* EssentialFeed */; @@ -819,6 +952,128 @@ }; name = Release; }; + 6E2085D42CE55DD000A98690 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUILD_LIBRARY_FOR_DISTRIBUTION = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = MZ9Q3WBZFB; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + ENABLE_MODULE_VERIFIER = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_NSHumanReadableCopyright = ""; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + IPHONEOS_DEPLOYMENT_TARGET = 18.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MARKETING_VERSION = 1.0; + MODULE_VERIFIER_SUPPORTED_LANGUAGES = "objective-c objective-c++"; + MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu17 gnu++20"; + PRODUCT_BUNDLE_IDENTIFIER = com.octavio.rojas.EssentialFeediOS; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + SDKROOT = iphoneos; + SKIP_INSTALL = YES; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_INSTALL_OBJC_HEADER = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 6E2085D52CE55DD000A98690 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUILD_LIBRARY_FOR_DISTRIBUTION = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = MZ9Q3WBZFB; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + ENABLE_MODULE_VERIFIER = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_NSHumanReadableCopyright = ""; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + IPHONEOS_DEPLOYMENT_TARGET = 18.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MARKETING_VERSION = 1.0; + MODULE_VERIFIER_SUPPORTED_LANGUAGES = "objective-c objective-c++"; + MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu17 gnu++20"; + PRODUCT_BUNDLE_IDENTIFIER = com.octavio.rojas.EssentialFeediOS; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + SDKROOT = iphoneos; + SKIP_INSTALL = YES; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_INSTALL_OBJC_HEADER = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 6E2085D72CE55DD000A98690 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = MZ9Q3WBZFB; + GCC_C_LANGUAGE_STANDARD = gnu17; + GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 18.0; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.octavio.rojas.EssentialFeediOSTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = iphoneos; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 6E2085D82CE55DD000A98690 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = MZ9Q3WBZFB; + GCC_C_LANGUAGE_STANDARD = gnu17; + GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 18.0; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.octavio.rojas.EssentialFeediOSTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = iphoneos; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; 6E4771AE2CDEA4B300F55DB2 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { @@ -931,6 +1186,24 @@ defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; + 6E2085D32CE55DD000A98690 /* Build configuration list for PBXNativeTarget "EssentialFeediOS" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 6E2085D42CE55DD000A98690 /* Debug */, + 6E2085D52CE55DD000A98690 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 6E2085D62CE55DD000A98690 /* Build configuration list for PBXNativeTarget "EssentialFeediOSTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 6E2085D72CE55DD000A98690 /* Debug */, + 6E2085D82CE55DD000A98690 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; 6E4771AD2CDEA4B300F55DB2 /* Build configuration list for PBXNativeTarget "EssentialFeedCacheIntegrationTests" */ = { isa = XCConfigurationList; buildConfigurations = ( diff --git a/EssentialFeed/EssentialFeed.xcodeproj/xcshareddata/xcschemes/EssentialFeediOS.xcscheme b/EssentialFeed/EssentialFeed.xcodeproj/xcshareddata/xcschemes/EssentialFeediOS.xcscheme new file mode 100644 index 0000000..e81ca35 --- /dev/null +++ b/EssentialFeed/EssentialFeed.xcodeproj/xcshareddata/xcschemes/EssentialFeediOS.xcscheme @@ -0,0 +1,85 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/EssentialFeed/EssentialFeediOS.xctestplan b/EssentialFeed/EssentialFeediOS.xctestplan new file mode 100644 index 0000000..cb6742f --- /dev/null +++ b/EssentialFeed/EssentialFeediOS.xctestplan @@ -0,0 +1,33 @@ +{ + "configurations" : [ + { + "id" : "5271EEC7-1FDB-438A-A49A-867AFD80A5F1", + "name" : "Test Scheme Action", + "options" : { + "testExecutionOrdering" : "random" + } + } + ], + "defaultOptions" : { + "codeCoverage" : { + "targets" : [ + { + "containerPath" : "container:EssentialFeed.xcodeproj", + "identifier" : "6E2085C02CE55DCF00A98690", + "name" : "EssentialFeediOS" + } + ] + } + }, + "testTargets" : [ + { + "parallelizable" : true, + "target" : { + "containerPath" : "container:EssentialFeed.xcodeproj", + "identifier" : "6E2085C92CE55DD000A98690", + "name" : "EssentialFeediOSTests" + } + } + ], + "version" : 1 +} From c6037e33d78324fb3a2652a7a71dd17ca2e17837 Mon Sep 17 00:00:00 2001 From: Octavio Rojas Date: Wed, 13 Nov 2024 16:45:26 -0600 Subject: [PATCH 14/14] Add separate CI schemes for macOS and iOS as we now have an iOS-specific target that should not be tested on macOS. --- .github/workflows/ci-macos.yml | 4 +- EssentialFeed/CI.xctestplan | 11 ++- .../{CI.xcscheme => CI_IOS.xcscheme} | 0 .../xcshareddata/xcschemes/CI_MACOS.xcscheme | 94 +++++++++++++++++++ 4 files changed, 106 insertions(+), 3 deletions(-) rename EssentialFeed/EssentialFeed.xcodeproj/xcshareddata/xcschemes/{CI.xcscheme => CI_IOS.xcscheme} (100%) create mode 100644 EssentialFeed/EssentialFeed.xcodeproj/xcshareddata/xcschemes/CI_MACOS.xcscheme diff --git a/.github/workflows/ci-macos.yml b/.github/workflows/ci-macos.yml index 3a294f5..c85ef6b 100644 --- a/.github/workflows/ci-macos.yml +++ b/.github/workflows/ci-macos.yml @@ -28,4 +28,6 @@ jobs: run: /usr/bin/xcodebuild -version - name: Build and Test - run: xcodebuild clean build test -project EssentialFeed/EssentialFeed.xcodeproj -scheme "CI" CODE_SIGN_IDENTITY="" CODE_SIGNING_REQUIRED=NO -sdk macosx -destination "platform=macOS,arch=arm64" ONLY_ACTIVE_ARCH=YES + run:| + xcodebuild clean build test -project EssentialFeed/EssentialFeed.xcodeproj -scheme "CI_macOS" CODE_SIGN_IDENTITY="" CODE_SIGNING_REQUIRED=NO -sdk macosx -destination "platform=macOS,arch=arm64" ONLY_ACTIVE_ARCH=YES + xcodebuild clean build test -project EssentialFeed/EssentialFeed.xcodeproj -scheme "CI_IOS" CODE_SIGN_IDENTITY="" CODE_SIGNING_REQUIRED=NO -sdk iphonesimulator -destination "platform=iOS Simulator,name=iPhone 14" ONLY_ACTIVE_ARCH=YES diff --git a/EssentialFeed/CI.xctestplan b/EssentialFeed/CI.xctestplan index 75667ea..856888b 100644 --- a/EssentialFeed/CI.xctestplan +++ b/EssentialFeed/CI.xctestplan @@ -13,6 +13,13 @@ "threadSanitizerEnabled" : true }, "testTargets" : [ + { + "target" : { + "containerPath" : "container:EssentialFeed.xcodeproj", + "identifier" : "6E5625722CC34F4800F339F1", + "name" : "EssentialFeedAPIEndToEndTests" + } + }, { "target" : { "containerPath" : "container:EssentialFeed.xcodeproj", @@ -30,8 +37,8 @@ { "target" : { "containerPath" : "container:EssentialFeed.xcodeproj", - "identifier" : "6E5625722CC34F4800F339F1", - "name" : "EssentialFeedAPIEndToEndTests" + "identifier" : "6E2085C92CE55DD000A98690", + "name" : "EssentialFeediOSTests" } } ], diff --git a/EssentialFeed/EssentialFeed.xcodeproj/xcshareddata/xcschemes/CI.xcscheme b/EssentialFeed/EssentialFeed.xcodeproj/xcshareddata/xcschemes/CI_IOS.xcscheme similarity index 100% rename from EssentialFeed/EssentialFeed.xcodeproj/xcshareddata/xcschemes/CI.xcscheme rename to EssentialFeed/EssentialFeed.xcodeproj/xcshareddata/xcschemes/CI_IOS.xcscheme diff --git a/EssentialFeed/EssentialFeed.xcodeproj/xcshareddata/xcschemes/CI_MACOS.xcscheme b/EssentialFeed/EssentialFeed.xcodeproj/xcshareddata/xcschemes/CI_MACOS.xcscheme new file mode 100644 index 0000000..5481b40 --- /dev/null +++ b/EssentialFeed/EssentialFeed.xcodeproj/xcshareddata/xcschemes/CI_MACOS.xcscheme @@ -0,0 +1,94 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +