-
Notifications
You must be signed in to change notification settings - Fork 492
Expand file tree
/
Copy pathSceneDelegate.swift
More file actions
203 lines (170 loc) · 6.18 KB
/
SceneDelegate.swift
File metadata and controls
203 lines (170 loc) · 6.18 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
//
// Copyright © Essential Developer. All rights reserved.
//
import os
import UIKit
import CoreData
import Combine
import EssentialFeed
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
var window: UIWindow?
private lazy var scheduler: AnyDispatchQueueScheduler = {
if let store = store as? CoreDataFeedStore {
return .scheduler(for: store)
}
return DispatchQueue(
label: "com.essentialdeveloper.infra.queue",
qos: .userInitiated
).eraseToAnyScheduler()
}()
private lazy var httpClient: HTTPClient = {
URLSessionHTTPClient(session: URLSession(configuration: .ephemeral))
}()
private lazy var logger = Logger(subsystem: "com.essentialdeveloper.EssentialAppCaseStudy", category: "main")
private lazy var store: FeedStore & FeedImageDataStore & StoreScheduler & Sendable = {
do {
return try CoreDataFeedStore(
storeURL: NSPersistentContainer
.defaultDirectoryURL()
.appendingPathComponent("feed-store.sqlite"))
} catch {
assertionFailure("Failed to instantiate CoreData store with error: \(error.localizedDescription)")
logger.fault("Failed to instantiate CoreData store with error: \(error.localizedDescription)")
return InMemoryFeedStore()
}
}()
private lazy var localFeedLoader: LocalFeedLoader = {
LocalFeedLoader(store: store, currentDate: Date.init)
}()
private lazy var baseURL = URL(string: "https://ile-api.essentialdeveloper.com/essential-feed")!
private lazy var navigationController = UINavigationController(
rootViewController: FeedUIComposer.feedComposedWith(
feedLoader: loadRemoteFeedWithLocalFallback,
imageLoader: loadLocalImageWithRemoteFallback,
selection: showComments))
convenience init(httpClient: HTTPClient, store: FeedStore & FeedImageDataStore & StoreScheduler & Sendable) {
self.init()
self.httpClient = httpClient
self.store = store
}
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
guard let scene = (scene as? UIWindowScene) else { return }
window = UIWindow(windowScene: scene)
configureWindow()
}
func configureWindow() {
window?.rootViewController = navigationController
window?.makeKeyAndVisible()
}
func sceneWillResignActive(_ scene: UIScene) {
scheduler.schedule { [localFeedLoader, logger] in
do {
try localFeedLoader.validateCache()
} catch {
logger.error("Failed to validate cache with error: \(error.localizedDescription)")
}
}
}
private func showComments(for image: FeedImage) {
let url = ImageCommentsEndpoint.get(image.id).url(baseURL: baseURL)
let comments = CommentsUIComposer.commentsComposedWith(commentsLoader: makeRemoteCommentsLoader(url: url))
navigationController.pushViewController(comments, animated: true)
}
private func makeRemoteCommentsLoader(url: URL) -> () -> AnyPublisher<[ImageComment], Error> {
return { [httpClient] in
return httpClient
.getPublisher(url: url)
.tryMap(ImageCommentsMapper.map)
.eraseToAnyPublisher()
}
}
private func loadRemoteFeedWithLocalFallback() async throws -> Paginated<FeedImage> {
do {
let feed = try await loadAndCacheRemoteFeed()
return makeFirstPage(items: feed)
} catch {
let feed = try await loadLocalFeed()
return makeFirstPage(items: feed)
}
}
private func loadAndCacheRemoteFeed() async throws -> [FeedImage] {
let feed = try await loadRemoteFeed()
await store.schedule { [store] in
let localFeedLoader = LocalFeedLoader(store: store, currentDate: Date.init)
try? localFeedLoader.save(feed)
}
return feed
}
private func loadLocalFeed() async throws -> [FeedImage] {
try await store.schedule { [store] in
let localFeedLoader = LocalFeedLoader(store: store, currentDate: Date.init)
return try localFeedLoader.load()
}
}
private func loadRemoteFeed(after: FeedImage? = nil) async throws -> [FeedImage] {
let url = FeedEndpoint.get(after: after).url(baseURL: baseURL)
let (data, response) = try await httpClient.get(from: url)
return try FeedItemsMapper.map(data, from: response)
}
private func loadMoreRemoteFeed(last: FeedImage?) async throws -> Paginated<FeedImage> {
async let cachedItems = try await loadLocalFeed()
async let newItems = try await loadRemoteFeed(after: last)
let items = try await cachedItems + newItems
await store.schedule { [store] in
let localFeedLoader = LocalFeedLoader(store: store, currentDate: Date.init)
try? localFeedLoader.save(items)
}
return try await makePage(items: items, last: newItems.last)
}
private func makeFirstPage(items: [FeedImage]) -> Paginated<FeedImage> {
makePage(items: items, last: items.last)
}
private func makePage(items: [FeedImage], last: FeedImage?) -> Paginated<FeedImage> {
Paginated(items: items, loadMore: last.map { last in
{ @MainActor @Sendable in try await self.loadMoreRemoteFeed(last: last) }
})
}
private func loadLocalImageWithRemoteFallback(url: URL) async throws -> Data {
do {
return try await loadLocalImage(url: url)
} catch {
return try await loadAndCacheRemoteImage(url: url)
}
}
private func loadLocalImage(url: URL) async throws -> Data {
try await store.schedule { [store] in
let localImageLoader = LocalFeedImageDataLoader(store: store)
let imageData = try localImageLoader.loadImageData(from: url)
return imageData
}
}
private func loadAndCacheRemoteImage(url: URL) async throws -> Data {
let (data, response) = try await httpClient.get(from: url)
let imageData = try FeedImageDataMapper.map(data, from: response)
await store.schedule { [store] in
let localImageLoader = LocalFeedImageDataLoader(store: store)
try? localImageLoader.save(data, for: url)
}
return imageData
}
}
protocol StoreScheduler {
@MainActor
func schedule<T>(_ action: @escaping @Sendable () throws -> T) async rethrows -> T
}
extension CoreDataFeedStore: StoreScheduler {
@MainActor
func schedule<T>(_ action: @escaping @Sendable () throws -> T) async rethrows -> T {
if contextQueue == .main {
return try action()
} else {
return try await perform(action)
}
}
}
extension InMemoryFeedStore: StoreScheduler {
@MainActor
func schedule<T>(_ action: @escaping @Sendable () throws -> T) async rethrows -> T {
try action()
}
}