22// Copyright © Essential Developer. All rights reserved.
33//
44
5- import os
65import UIKit
7- import CoreData
86import EssentialFeed
97
108class 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