Skip to content

Commit a1b9435

Browse files
authored
Merge pull request #94 from essentialdevelopercom/swift-concurrency
Migrate Image Comments composition to async/await
2 parents 7228618 + 08b72c3 commit a1b9435

File tree

4 files changed

+65
-70
lines changed

4 files changed

+65
-70
lines changed

EssentialApp/EssentialApp/CommentsUIComposer.swift

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,18 +3,17 @@
33
//
44

55
import UIKit
6-
import Combine
76
import EssentialFeed
87
import EssentialFeediOS
98

109
@MainActor
1110
public final class CommentsUIComposer {
1211
private init() {}
1312

14-
private typealias CommentsPresentationAdapter = LoadResourcePresentationAdapter<[ImageComment], CommentsViewAdapter>
13+
private typealias CommentsPresentationAdapter = AsyncLoadResourcePresentationAdapter<[ImageComment], CommentsViewAdapter>
1514

1615
public static func commentsComposedWith(
17-
commentsLoader: @escaping () -> AnyPublisher<[ImageComment], Error>
16+
commentsLoader: @escaping () async throws -> [ImageComment]
1817
) -> ListViewController {
1918
let presentationAdapter = CommentsPresentationAdapter(loader: commentsLoader)
2019

EssentialApp/EssentialApp/SceneDelegate.swift

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
import os
66
import UIKit
77
import CoreData
8-
import Combine
98
import EssentialFeed
109

1110
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
@@ -83,16 +82,14 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate {
8382

8483
private func showComments(for image: FeedImage) {
8584
let url = ImageCommentsEndpoint.get(image.id).url(baseURL: baseURL)
86-
let comments = CommentsUIComposer.commentsComposedWith(commentsLoader: makeRemoteCommentsLoader(url: url))
85+
let comments = CommentsUIComposer.commentsComposedWith(commentsLoader: loadComments(url: url))
8786
navigationController.pushViewController(comments, animated: true)
8887
}
8988

90-
private func makeRemoteCommentsLoader(url: URL) -> () -> AnyPublisher<[ImageComment], Error> {
89+
private func loadComments(url: URL) -> () async throws -> [ImageComment] {
9190
return { [httpClient] in
92-
return httpClient
93-
.getPublisher(url: url)
94-
.tryMap(ImageCommentsMapper.map)
95-
.eraseToAnyPublisher()
91+
let (data, response) = try await httpClient.get(from: url)
92+
return try ImageCommentsMapper.map(data, from: response)
9693
}
9794
}
9895

EssentialApp/EssentialAppTests/CommentsUIIntegrationTests.swift

Lines changed: 58 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
//
44

55
import XCTest
6-
import Combine
76
import UIKit
87
import EssentialApp
98
import EssentialFeed
@@ -12,15 +11,15 @@ import EssentialFeediOS
1211
@MainActor
1312
class CommentsUIIntegrationTests: XCTestCase {
1413

15-
func test_commentsView_hasTitle() {
14+
func test_commentsView_hasTitle() async {
1615
let (sut, _) = makeSUT()
1716

1817
sut.simulateAppearance()
1918

2019
XCTAssertEqual(sut.title, commentsTitle)
2120
}
2221

23-
func test_loadCommentsActions_requestCommentsFromLoader() {
22+
func test_loadCommentsActions_requestCommentsFromLoader() async {
2423
let (sut, loader) = makeSUT()
2524
XCTAssertEqual(loader.loadCommentsCallCount, 0, "Expected no loading requests before view appears")
2625

@@ -30,16 +29,16 @@ class CommentsUIIntegrationTests: XCTestCase {
3029
sut.simulateUserInitiatedReload()
3130
XCTAssertEqual(loader.loadCommentsCallCount, 1, "Expected no request until previous completes")
3231

33-
loader.completeCommentsLoading(at: 0)
32+
await loader.completeCommentsLoading(at: 0)
3433
sut.simulateUserInitiatedReload()
3534
XCTAssertEqual(loader.loadCommentsCallCount, 2, "Expected another loading request once user initiates a reload")
3635

37-
loader.completeCommentsLoading(at: 1)
36+
await loader.completeCommentsLoading(at: 1)
3837
sut.simulateUserInitiatedReload()
3938
XCTAssertEqual(loader.loadCommentsCallCount, 3, "Expected yet another loading request once user initiates another reload")
4039
}
4140

42-
func test_loadCommentsActions_runsAutomaticallyOnlyOnFirstAppearance() {
41+
func test_loadCommentsActions_runsAutomaticallyOnlyOnFirstAppearance() async {
4342
let (sut, loader) = makeSUT()
4443
XCTAssertEqual(loader.loadCommentsCallCount, 0, "Expected no loading requests before view appears")
4544

@@ -50,120 +49,121 @@ class CommentsUIIntegrationTests: XCTestCase {
5049
XCTAssertEqual(loader.loadCommentsCallCount, 1, "Expected no loading request the second time view appears")
5150
}
5251

53-
func test_loadingCommentsIndicator_isVisibleWhileLoadingComments() {
52+
func test_loadingCommentsIndicator_isVisibleWhileLoadingComments() async {
5453
let (sut, loader) = makeSUT()
5554

5655
sut.simulateAppearance()
5756
XCTAssertTrue(sut.isShowingLoadingIndicator, "Expected loading indicator once view appears")
5857

59-
loader.completeCommentsLoading(at: 0)
58+
await loader.completeCommentsLoading(at: 0)
6059
XCTAssertFalse(sut.isShowingLoadingIndicator, "Expected no loading indicator once loading completes successfully")
6160

6261
sut.simulateUserInitiatedReload()
6362
XCTAssertTrue(sut.isShowingLoadingIndicator, "Expected loading indicator once user initiates a reload")
6463

65-
loader.completeCommentsLoadingWithError(at: 1)
64+
await loader.completeCommentsLoadingWithError(at: 1)
6665
XCTAssertFalse(sut.isShowingLoadingIndicator, "Expected no loading indicator once user initiated loading completes with error")
6766
}
6867

69-
func test_loadCommentsCompletion_rendersSuccessfullyLoadedComments() {
68+
func test_loadCommentsCompletion_rendersSuccessfullyLoadedComments() async {
7069
let comment0 = makeComment(message: "a message", username: "a username")
7170
let comment1 = makeComment(message: "another message", username: "another username")
7271
let (sut, loader) = makeSUT()
7372

7473
sut.simulateAppearance()
7574
assertThat(sut, isRendering: [ImageComment]())
7675

77-
loader.completeCommentsLoading(with: [comment0], at: 0)
76+
await loader.completeCommentsLoading(with: [comment0], at: 0)
7877
assertThat(sut, isRendering: [comment0])
7978

8079
sut.simulateUserInitiatedReload()
81-
loader.completeCommentsLoading(with: [comment0, comment1], at: 1)
80+
await loader.completeCommentsLoading(with: [comment0, comment1], at: 1)
8281
assertThat(sut, isRendering: [comment0, comment1])
8382
}
8483

85-
func test_loadCommentsCompletion_rendersSuccessfullyLoadedEmptyCommentsAfterNonEmptyComments() {
84+
func test_loadCommentsCompletion_rendersSuccessfullyLoadedEmptyCommentsAfterNonEmptyComments() async {
8685
let comment = makeComment()
8786
let (sut, loader) = makeSUT()
8887

8988
sut.simulateAppearance()
90-
loader.completeCommentsLoading(with: [comment], at: 0)
89+
await loader.completeCommentsLoading(with: [comment], at: 0)
9190
assertThat(sut, isRendering: [comment])
9291

9392
sut.simulateUserInitiatedReload()
94-
loader.completeCommentsLoading(with: [], at: 1)
93+
await loader.completeCommentsLoading(with: [], at: 1)
9594
assertThat(sut, isRendering: [ImageComment]())
9695
}
9796

98-
func test_loadCommentsCompletion_doesNotAlterCurrentRenderingStateOnError() {
97+
func test_loadCommentsCompletion_doesNotAlterCurrentRenderingStateOnError() async {
9998
let comment = makeComment()
10099
let (sut, loader) = makeSUT()
101100

102101
sut.simulateAppearance()
103-
loader.completeCommentsLoading(with: [comment], at: 0)
102+
await loader.completeCommentsLoading(with: [comment], at: 0)
104103
assertThat(sut, isRendering: [comment])
105104

106105
sut.simulateUserInitiatedReload()
107-
loader.completeCommentsLoadingWithError(at: 1)
106+
await loader.completeCommentsLoadingWithError(at: 1)
108107
assertThat(sut, isRendering: [comment])
109108
}
110109

111-
func test_loadCommentsCompletion_rendersErrorMessageOnErrorUntilNextReload() {
110+
func test_loadCommentsCompletion_rendersErrorMessageOnErrorUntilNextReload() async {
112111
let (sut, loader) = makeSUT()
113112

114113
sut.simulateAppearance()
115114
XCTAssertEqual(sut.errorMessage, nil)
116115

117-
loader.completeCommentsLoadingWithError(at: 0)
116+
await loader.completeCommentsLoadingWithError(at: 0)
118117
XCTAssertEqual(sut.errorMessage, loadError)
119118

120119
sut.simulateUserInitiatedReload()
121120
XCTAssertEqual(sut.errorMessage, nil)
122121
}
123122

124-
func test_tapOnErrorView_hidesErrorMessage() {
123+
func test_tapOnErrorView_hidesErrorMessage() async {
125124
let (sut, loader) = makeSUT()
126125

127126
sut.simulateAppearance()
128127
XCTAssertEqual(sut.errorMessage, nil)
129128

130-
loader.completeCommentsLoadingWithError(at: 0)
129+
await loader.completeCommentsLoadingWithError(at: 0)
131130
XCTAssertEqual(sut.errorMessage, loadError)
132131

133132
sut.simulateErrorViewTap()
134133
XCTAssertEqual(sut.errorMessage, nil)
135134
}
136135

137-
func test_deinit_cancelsRunningRequest() {
138-
var cancelCallCount = 0
139-
136+
func test_deinit_cancelsRunningRequest() async throws {
137+
let loader = LoaderSpy<Void, [ImageComment]>()
140138
var sut: ListViewController?
141139

142140
autoreleasepool {
143-
sut = CommentsUIComposer.commentsComposedWith(commentsLoader: {
144-
PassthroughSubject<[ImageComment], Error>()
145-
.handleEvents(receiveCancel: {
146-
cancelCallCount += 1
147-
}).eraseToAnyPublisher()
148-
})
149-
141+
sut = CommentsUIComposer.commentsComposedWith(commentsLoader: loader.loadComments)
142+
150143
sut?.simulateAppearance()
151144
}
152145

153-
XCTAssertEqual(cancelCallCount, 0)
146+
XCTAssertEqual(loader.cancelledCommentsRequestsCount, 0)
154147

155148
sut = nil
149+
let result = try await loader.result(at: 0)
156150

157-
XCTAssertEqual(cancelCallCount, 1)
151+
XCTAssertEqual(result, .cancelled)
152+
XCTAssertEqual(loader.cancelledCommentsRequestsCount, 1)
158153
}
159154

160155
// MARK: - Helpers
161156

162-
private func makeSUT(file: StaticString = #filePath, line: UInt = #line) -> (sut: ListViewController, loader: LoaderSpy) {
163-
let loader = LoaderSpy()
164-
let sut = CommentsUIComposer.commentsComposedWith(commentsLoader: loader.loadPublisher)
157+
private func makeSUT(file: StaticString = #filePath, line: UInt = #line) -> (sut: ListViewController, loader: LoaderSpy<Void, [ImageComment]>) {
158+
let loader = LoaderSpy<Void, [ImageComment]>()
159+
let sut = CommentsUIComposer.commentsComposedWith(commentsLoader: loader.loadComments)
165160
trackForMemoryLeaks(loader, file: file, line: line)
166161
trackForMemoryLeaks(sut, file: file, line: line)
162+
163+
addTeardownBlock { [weak loader] in
164+
try await loader?.cancelPendingRequests()
165+
}
166+
167167
return (sut, loader)
168168
}
169169

@@ -182,28 +182,27 @@ class CommentsUIIntegrationTests: XCTestCase {
182182
XCTAssertEqual(sut.commentUsername(at: index), comment.username, "username at \(index)", file: file, line: line)
183183
}
184184
}
185+
}
186+
187+
private extension LoaderSpy where Param == Void, Resource == [ImageComment] {
188+
var loadCommentsCallCount: Int {
189+
return requests.count
190+
}
185191

186-
private class LoaderSpy {
187-
private var requests = [PassthroughSubject<[ImageComment], Error>]()
188-
189-
var loadCommentsCallCount: Int {
190-
return requests.count
191-
}
192-
193-
func loadPublisher() -> AnyPublisher<[ImageComment], Error> {
194-
let publisher = PassthroughSubject<[ImageComment], Error>()
195-
requests.append(publisher)
196-
return publisher.eraseToAnyPublisher()
197-
}
198-
199-
func completeCommentsLoading(with comments: [ImageComment] = [], at index: Int = 0) {
200-
requests[index].send(comments)
201-
requests[index].send(completion: .finished)
202-
}
203-
204-
func completeCommentsLoadingWithError(at index: Int = 0) {
205-
let error = NSError(domain: "an error", code: 0)
206-
requests[index].send(completion: .failure(error))
207-
}
192+
var cancelledCommentsRequestsCount: Int {
193+
requests.count { $0.result == .cancelled }
194+
}
195+
196+
func loadComments() async throws -> [ImageComment] {
197+
try await load(())
198+
}
199+
200+
func completeCommentsLoading(with comments: [ImageComment] = [], at index: Int = 0) async {
201+
await complete(with: comments, at: index)
202+
}
203+
204+
func completeCommentsLoadingWithError(at index: Int = 0) async {
205+
let error = NSError(domain: "an error", code: 0)
206+
await fail(with: error, at: index)
208207
}
209208
}

EssentialFeed/EssentialFeed/Image Comments Feature/ImageComment.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
import Foundation
66

7-
public struct ImageComment: Equatable {
7+
public struct ImageComment: Equatable, Sendable {
88
public let id: UUID
99
public let message: String
1010
public let createdAt: Date

0 commit comments

Comments
 (0)