Skip to content

Commit 32022fd

Browse files
committed
Extract new FeedService class to simplify SceneDelegate
1 parent 395b4ef commit 32022fd

3 files changed

Lines changed: 175 additions & 155 deletions

File tree

EssentialApp/EssentialApp.xcodeproj/project.pbxproj

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
0895DAAC234B3F7E0031BB2D /* EssentialFeed.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 0895DAA9234B3F7E0031BB2D /* EssentialFeed.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
3838
0895DAAD234B3F7E0031BB2D /* EssentialFeediOS.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 0895DAAA234B3F7E0031BB2D /* EssentialFeediOS.framework */; };
3939
0895DAAE234B3F7E0031BB2D /* EssentialFeediOS.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 0895DAAA234B3F7E0031BB2D /* EssentialFeediOS.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
40+
08CD096A2EE6DB0600C2E75A /* FeedService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08CD09692EE6DB0600C2E75A /* FeedService.swift */; };
4041
/* End PBXBuildFile section */
4142

4243
/* Begin PBXContainerItemProxy section */
@@ -100,6 +101,7 @@
100101
08B5033725346BAC003FF218 /* el */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = el; path = el.lproj/LaunchScreen.strings; sourceTree = "<group>"; };
101102
08B5033925346BE1003FF218 /* pt-BR */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "pt-BR"; path = "pt-BR.lproj/LaunchScreen.strings"; sourceTree = "<group>"; };
102103
08B5033B25346BFE003FF218 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/LaunchScreen.strings; sourceTree = "<group>"; };
104+
08CD09692EE6DB0600C2E75A /* FeedService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedService.swift; sourceTree = "<group>"; };
103105
/* End PBXFileReference section */
104106

105107
/* Begin PBXFrameworksBuildPhase section */
@@ -167,6 +169,7 @@
167169
children = (
168170
0895DA86234B3B950031BB2D /* AppDelegate.swift */,
169171
0895DA88234B3B950031BB2D /* SceneDelegate.swift */,
172+
08CD09692EE6DB0600C2E75A /* FeedService.swift */,
170173
08073B42238D2DF900A75DC6 /* FeedUIComposer.swift */,
171174
088B441B25309B6E00D75AAD /* CommentsUIComposer.swift */,
172175
08073B40238D2DF900A75DC6 /* WeakRefVirtualProxy.swift */,
@@ -307,6 +310,7 @@
307310
buildActionMask = 2147483647;
308311
files = (
309312
08073B44238D2DFA00A75DC6 /* FeedUIComposer.swift in Sources */,
313+
08CD096A2EE6DB0600C2E75A /* FeedService.swift in Sources */,
310314
0895DA87234B3B950031BB2D /* AppDelegate.swift in Sources */,
311315
08073B45238D2DFA00A75DC6 /* LoadResourcePresentationAdapter.swift in Sources */,
312316
08073B48238D2DFA00A75DC6 /* WeakRefVirtualProxy.swift in Sources */,
Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
//
2+
// Copyright © Essential Developer. All rights reserved.
3+
//
4+
5+
import os
6+
import CoreData
7+
import EssentialFeed
8+
9+
@MainActor
10+
final class FeedService {
11+
12+
private lazy var httpClient: HTTPClient = {
13+
URLSessionHTTPClient(session: URLSession(configuration: .ephemeral))
14+
}()
15+
16+
private lazy var logger = Logger(subsystem: "com.essentialdeveloper.EssentialAppCaseStudy", category: "main")
17+
18+
private lazy var store: FeedStore & FeedImageDataStore & Scheduler & Sendable = {
19+
do {
20+
return try CoreDataFeedStore(
21+
storeURL: NSPersistentContainer
22+
.defaultDirectoryURL()
23+
.appendingPathComponent("feed-store.sqlite"))
24+
} catch {
25+
assertionFailure("Failed to instantiate CoreData store with error: \(error.localizedDescription)")
26+
logger.fault("Failed to instantiate CoreData store with error: \(error.localizedDescription)")
27+
return InMemoryFeedStore()
28+
}
29+
}()
30+
31+
private lazy var baseURL = URL(string: "https://ile-api.essentialdeveloper.com/essential-feed")!
32+
33+
convenience init(httpClient: HTTPClient, store: FeedStore & FeedImageDataStore & Scheduler & Sendable) {
34+
self.init()
35+
self.httpClient = httpClient
36+
self.store = store
37+
}
38+
39+
func validateCache() {
40+
Task.immediate { @MainActor in
41+
await store.schedule { [store, logger] in
42+
do {
43+
let localFeedLoader = LocalFeedLoader(store: store, currentDate: Date.init)
44+
try localFeedLoader.validateCache()
45+
} catch {
46+
logger.error("Failed to validate cache with error: \(error.localizedDescription)")
47+
}
48+
}
49+
}
50+
}
51+
52+
func loadComments(for image: FeedImage) -> () async throws -> [ImageComment] {
53+
return { [httpClient, baseURL] in
54+
let url = ImageCommentsEndpoint.get(image.id).url(baseURL: baseURL)
55+
let (data, response) = try await httpClient.get(from: url)
56+
return try ImageCommentsMapper.map(data, from: response)
57+
}
58+
}
59+
60+
func loadRemoteFeedWithLocalFallback() async throws -> Paginated<FeedImage> {
61+
do {
62+
let feed = try await loadAndCacheRemoteFeed()
63+
return makeFirstPage(items: feed)
64+
} catch {
65+
let feed = try await loadLocalFeed()
66+
return makeFirstPage(items: feed)
67+
}
68+
}
69+
70+
private func loadAndCacheRemoteFeed() async throws -> [FeedImage] {
71+
let feed = try await loadRemoteFeed()
72+
await store.schedule { [store] in
73+
let localFeedLoader = LocalFeedLoader(store: store, currentDate: Date.init)
74+
try? localFeedLoader.save(feed)
75+
}
76+
return feed
77+
}
78+
79+
private func loadLocalFeed() async throws -> [FeedImage] {
80+
try await store.schedule { [store] in
81+
let localFeedLoader = LocalFeedLoader(store: store, currentDate: Date.init)
82+
return try localFeedLoader.load()
83+
}
84+
}
85+
86+
private func loadRemoteFeed(after: FeedImage? = nil) async throws -> [FeedImage] {
87+
let url = FeedEndpoint.get(after: after).url(baseURL: baseURL)
88+
let (data, response) = try await httpClient.get(from: url)
89+
return try FeedItemsMapper.map(data, from: response)
90+
}
91+
92+
private func loadMoreRemoteFeed(last: FeedImage?) async throws -> Paginated<FeedImage> {
93+
async let cachedItems = try await loadLocalFeed()
94+
async let newItems = try await loadRemoteFeed(after: last)
95+
96+
let items = try await cachedItems + newItems
97+
98+
await store.schedule { [store] in
99+
let localFeedLoader = LocalFeedLoader(store: store, currentDate: Date.init)
100+
try? localFeedLoader.save(items)
101+
}
102+
103+
return try await makePage(items: items, last: newItems.last)
104+
}
105+
106+
private func makeFirstPage(items: [FeedImage]) -> Paginated<FeedImage> {
107+
makePage(items: items, last: items.last)
108+
}
109+
110+
private func makePage(items: [FeedImage], last: FeedImage?) -> Paginated<FeedImage> {
111+
Paginated(items: items, loadMore: last.map { last in
112+
{ @MainActor @Sendable in try await self.loadMoreRemoteFeed(last: last) }
113+
})
114+
}
115+
116+
func loadLocalImageWithRemoteFallback(url: URL) async throws -> Data {
117+
do {
118+
return try await loadLocalImage(url: url)
119+
} catch {
120+
return try await loadAndCacheRemoteImage(url: url)
121+
}
122+
}
123+
124+
private func loadLocalImage(url: URL) async throws -> Data {
125+
try await store.schedule { [store] in
126+
let localImageLoader = LocalFeedImageDataLoader(store: store)
127+
let imageData = try localImageLoader.loadImageData(from: url)
128+
return imageData
129+
}
130+
}
131+
132+
private func loadAndCacheRemoteImage(url: URL) async throws -> Data {
133+
let (data, response) = try await httpClient.get(from: url)
134+
let imageData = try FeedImageDataMapper.map(data, from: response)
135+
await store.schedule { [store] in
136+
let localImageLoader = LocalFeedImageDataLoader(store: store)
137+
try? localImageLoader.save(data, for: url)
138+
}
139+
return imageData
140+
}
141+
}
142+
143+
protocol Scheduler {
144+
@MainActor
145+
func schedule<T>(_ action: @escaping @Sendable () throws -> T) async rethrows -> T
146+
}
147+
148+
extension CoreDataFeedStore: Scheduler {
149+
@MainActor
150+
func schedule<T>(_ action: @escaping @Sendable () throws -> T) async rethrows -> T {
151+
if contextQueue == .main {
152+
return try action()
153+
} else {
154+
return try await perform(action)
155+
}
156+
}
157+
}
158+
159+
extension InMemoryFeedStore: Scheduler {
160+
@MainActor
161+
func schedule<T>(_ action: @escaping @Sendable () throws -> T) async rethrows -> T {
162+
try action()
163+
}
164+
}

EssentialApp/EssentialApp/SceneDelegate.swift

Lines changed: 7 additions & 155 deletions
Original file line numberDiff line numberDiff line change
@@ -2,45 +2,23 @@
22
// Copyright © Essential Developer. All rights reserved.
33
//
44

5-
import os
65
import UIKit
7-
import CoreData
86
import EssentialFeed
97

108
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
119
var window: UIWindow?
12-
13-
private lazy var httpClient: HTTPClient = {
14-
URLSessionHTTPClient(session: URLSession(configuration: .ephemeral))
15-
}()
16-
17-
private lazy var logger = Logger(subsystem: "com.essentialdeveloper.EssentialAppCaseStudy", category: "main")
1810

19-
private lazy var store: FeedStore & FeedImageDataStore & StoreScheduler & Sendable = {
20-
do {
21-
return try CoreDataFeedStore(
22-
storeURL: NSPersistentContainer
23-
.defaultDirectoryURL()
24-
.appendingPathComponent("feed-store.sqlite"))
25-
} catch {
26-
assertionFailure("Failed to instantiate CoreData store with error: \(error.localizedDescription)")
27-
logger.fault("Failed to instantiate CoreData store with error: \(error.localizedDescription)")
28-
return InMemoryFeedStore()
29-
}
30-
}()
31-
32-
private lazy var baseURL = URL(string: "https://ile-api.essentialdeveloper.com/essential-feed")!
11+
private lazy var feedService = FeedService()
3312

3413
private lazy var navigationController = UINavigationController(
3514
rootViewController: FeedUIComposer.feedComposedWith(
36-
feedLoader: loadRemoteFeedWithLocalFallback,
37-
imageLoader: loadLocalImageWithRemoteFallback,
15+
feedLoader: feedService.loadRemoteFeedWithLocalFallback,
16+
imageLoader: feedService.loadLocalImageWithRemoteFallback,
3817
selection: showComments))
3918

40-
convenience init(httpClient: HTTPClient, store: FeedStore & FeedImageDataStore & StoreScheduler & Sendable) {
19+
convenience init(httpClient: HTTPClient, store: FeedStore & FeedImageDataStore & Scheduler & Sendable) {
4120
self.init()
42-
self.httpClient = httpClient
43-
self.store = store
21+
self.feedService = FeedService(httpClient: httpClient, store: store)
4422
}
4523

4624
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
@@ -56,137 +34,11 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate {
5634
}
5735

5836
func sceneWillResignActive(_ scene: UIScene) {
59-
validateCache()
37+
feedService.validateCache()
6038
}
6139

6240
private func showComments(for image: FeedImage) {
63-
let url = ImageCommentsEndpoint.get(image.id).url(baseURL: baseURL)
64-
let comments = CommentsUIComposer.commentsComposedWith(commentsLoader: loadComments(url: url))
41+
let comments = CommentsUIComposer.commentsComposedWith(commentsLoader: feedService.loadComments(for: image))
6542
navigationController.pushViewController(comments, animated: true)
6643
}
67-
68-
private func validateCache() {
69-
Task.immediate { @MainActor in
70-
await store.schedule { [store, logger] in
71-
do {
72-
let localFeedLoader = LocalFeedLoader(store: store, currentDate: Date.init)
73-
try localFeedLoader.validateCache()
74-
} catch {
75-
logger.error("Failed to validate cache with error: \(error.localizedDescription)")
76-
}
77-
}
78-
}
79-
}
80-
81-
private func loadComments(url: URL) -> () async throws -> [ImageComment] {
82-
return { [httpClient] in
83-
let (data, response) = try await httpClient.get(from: url)
84-
return try ImageCommentsMapper.map(data, from: response)
85-
}
86-
}
87-
88-
private func loadRemoteFeedWithLocalFallback() async throws -> Paginated<FeedImage> {
89-
do {
90-
let feed = try await loadAndCacheRemoteFeed()
91-
return makeFirstPage(items: feed)
92-
} catch {
93-
let feed = try await loadLocalFeed()
94-
return makeFirstPage(items: feed)
95-
}
96-
}
97-
98-
private func loadAndCacheRemoteFeed() async throws -> [FeedImage] {
99-
let feed = try await loadRemoteFeed()
100-
await store.schedule { [store] in
101-
let localFeedLoader = LocalFeedLoader(store: store, currentDate: Date.init)
102-
try? localFeedLoader.save(feed)
103-
}
104-
return feed
105-
}
106-
107-
private func loadLocalFeed() async throws -> [FeedImage] {
108-
try await store.schedule { [store] in
109-
let localFeedLoader = LocalFeedLoader(store: store, currentDate: Date.init)
110-
return try localFeedLoader.load()
111-
}
112-
}
113-
114-
private func loadRemoteFeed(after: FeedImage? = nil) async throws -> [FeedImage] {
115-
let url = FeedEndpoint.get(after: after).url(baseURL: baseURL)
116-
let (data, response) = try await httpClient.get(from: url)
117-
return try FeedItemsMapper.map(data, from: response)
118-
}
119-
120-
private func loadMoreRemoteFeed(last: FeedImage?) async throws -> Paginated<FeedImage> {
121-
async let cachedItems = try await loadLocalFeed()
122-
async let newItems = try await loadRemoteFeed(after: last)
123-
124-
let items = try await cachedItems + newItems
125-
126-
await store.schedule { [store] in
127-
let localFeedLoader = LocalFeedLoader(store: store, currentDate: Date.init)
128-
try? localFeedLoader.save(items)
129-
}
130-
131-
return try await makePage(items: items, last: newItems.last)
132-
}
133-
134-
private func makeFirstPage(items: [FeedImage]) -> Paginated<FeedImage> {
135-
makePage(items: items, last: items.last)
136-
}
137-
138-
private func makePage(items: [FeedImage], last: FeedImage?) -> Paginated<FeedImage> {
139-
Paginated(items: items, loadMore: last.map { last in
140-
{ @MainActor @Sendable in try await self.loadMoreRemoteFeed(last: last) }
141-
})
142-
}
143-
144-
private func loadLocalImageWithRemoteFallback(url: URL) async throws -> Data {
145-
do {
146-
return try await loadLocalImage(url: url)
147-
} catch {
148-
return try await loadAndCacheRemoteImage(url: url)
149-
}
150-
}
151-
152-
private func loadLocalImage(url: URL) async throws -> Data {
153-
try await store.schedule { [store] in
154-
let localImageLoader = LocalFeedImageDataLoader(store: store)
155-
let imageData = try localImageLoader.loadImageData(from: url)
156-
return imageData
157-
}
158-
}
159-
160-
private func loadAndCacheRemoteImage(url: URL) async throws -> Data {
161-
let (data, response) = try await httpClient.get(from: url)
162-
let imageData = try FeedImageDataMapper.map(data, from: response)
163-
await store.schedule { [store] in
164-
let localImageLoader = LocalFeedImageDataLoader(store: store)
165-
try? localImageLoader.save(data, for: url)
166-
}
167-
return imageData
168-
}
169-
}
170-
171-
protocol StoreScheduler {
172-
@MainActor
173-
func schedule<T>(_ action: @escaping @Sendable () throws -> T) async rethrows -> T
174-
}
175-
176-
extension CoreDataFeedStore: StoreScheduler {
177-
@MainActor
178-
func schedule<T>(_ action: @escaping @Sendable () throws -> T) async rethrows -> T {
179-
if contextQueue == .main {
180-
return try action()
181-
} else {
182-
return try await perform(action)
183-
}
184-
}
185-
}
186-
187-
extension InMemoryFeedStore: StoreScheduler {
188-
@MainActor
189-
func schedule<T>(_ action: @escaping @Sendable () throws -> T) async rethrows -> T {
190-
try action()
191-
}
19244
}

0 commit comments

Comments
 (0)