Skip to content

Commit 662df85

Browse files
authored
BN-4308 [FEATURE] Connect reactive and async/await APIs
1 parent bc282c1 commit 662df85

File tree

6 files changed

+205
-48
lines changed

6 files changed

+205
-48
lines changed

NetworkModuleSampleApp/NetworkModuleSampleApp.xcodeproj/project.pbxproj

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
2290A8D92994CDD900067FFC /* ViewStyles.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2290A8D82994CDD900067FFC /* ViewStyles.swift */; };
1616
2290A8DB2994EAC500067FFC /* Constants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2290A8DA2994EAC500067FFC /* Constants.swift */; };
1717
2290A8E12995118F00067FFC /* StringExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2290A8E02995118F00067FFC /* StringExtensions.swift */; };
18+
2290A8E6299A62AE00067FFC /* Logging.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2290A8E5299A62AE00067FFC /* Logging.swift */; };
1819
AD06FC26297FE93900A05C0F /* EpisodeHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD06FC25297FE93900A05C0F /* EpisodeHeaderView.swift */; };
1920
AD073F02297EC3DA007B8C89 /* NetworkModuleSampleAppApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD073F01297EC3DA007B8C89 /* NetworkModuleSampleAppApp.swift */; };
2021
AD073F06297EC3DC007B8C89 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = AD073F05297EC3DC007B8C89 /* Assets.xcassets */; };
@@ -48,6 +49,7 @@
4849
2290A8D82994CDD900067FFC /* ViewStyles.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewStyles.swift; sourceTree = "<group>"; };
4950
2290A8DA2994EAC500067FFC /* Constants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Constants.swift; sourceTree = "<group>"; };
5051
2290A8E02995118F00067FFC /* StringExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StringExtensions.swift; sourceTree = "<group>"; };
52+
2290A8E5299A62AE00067FFC /* Logging.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Logging.swift; sourceTree = "<group>"; };
5153
AD06FC25297FE93900A05C0F /* EpisodeHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EpisodeHeaderView.swift; sourceTree = "<group>"; };
5254
AD073EFE297EC3DA007B8C89 /* NetworkModuleSampleApp.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = NetworkModuleSampleApp.app; sourceTree = BUILT_PRODUCTS_DIR; };
5355
AD073F01297EC3DA007B8C89 /* NetworkModuleSampleAppApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkModuleSampleAppApp.swift; sourceTree = "<group>"; };
@@ -120,6 +122,14 @@
120122
path = Extensions;
121123
sourceTree = "<group>";
122124
};
125+
2290A8E4299A629A00067FFC /* Logging */ = {
126+
isa = PBXGroup;
127+
children = (
128+
2290A8E5299A62AE00067FFC /* Logging.swift */,
129+
);
130+
path = Logging;
131+
sourceTree = "<group>";
132+
};
123133
AD073EF5297EC3DA007B8C89 = {
124134
isa = PBXGroup;
125135
children = (
@@ -337,6 +347,7 @@
337347
ADE5A5462982687B003B3AE6 /* Common */ = {
338348
isa = PBXGroup;
339349
children = (
350+
2290A8E4299A629A00067FFC /* Logging */,
340351
2290A8DF2995117C00067FFC /* Extensions */,
341352
2260DF2129938BF500ED9386 /* Style */,
342353
2260DF1A29926EF900ED9386 /* Networking */,
@@ -458,6 +469,7 @@
458469
AD073F22297EC637007B8C89 /* EpisodeListRowView.swift in Sources */,
459470
AD06FC26297FE93900A05C0F /* EpisodeHeaderView.swift in Sources */,
460471
ADC19E572981634400E26D25 /* EpisodeViewModel.swift in Sources */,
472+
2290A8E6299A62AE00067FFC /* Logging.swift in Sources */,
461473
ADC19E59298163A100E26D25 /* EpisodeViewModelProtocol.swift in Sources */,
462474
ADE5A54929826893003B3AE6 /* ImagePlaceHolder.swift in Sources */,
463475
2260DF2529938E6F00ED9386 /* CircularProgressView.swift in Sources */,
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
//
2+
// Logging.swift
3+
// Netguru iOS Network Module
4+
//
5+
6+
import Foundation
7+
import os.log
8+
9+
extension OSLog {
10+
private static var subsystem = Bundle.main.bundleIdentifier ?? "NgNetworkModuleSampleApp"
11+
12+
/// Networking logs.
13+
static let networking = OSLog(subsystem: subsystem, category: "networking")
14+
}
15+
16+
func logNetworkInfo(_ message: String) {
17+
log(message: message, category: .networking)
18+
}
19+
20+
func logNetworkError(_ message: String) {
21+
log(message: message, category: .networking, type: .error)
22+
}
23+
24+
private func log(message: String, category: OSLog, type: OSLogType = .info) {
25+
os_log("%{public}@", log: category, type: type, message)
26+
}

NetworkModuleSampleApp/NetworkModuleSampleApp/PreviewMocks/PreviewMocks.swift

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -48,9 +48,9 @@ extension PreviewMocks {
4848
final class EpisodeViewModelStub: EpisodeViewModelProtocol {
4949
var selectedNetworkingAPI: NetworkModuleApiType = .classic
5050

51-
@Published var viewState: EpisodeViewStates = .loading(PreviewMocks.mockEpisodeModel)
52-
var viewStatePublished: Published<EpisodeViewStates> { _viewState }
53-
var viewStatePublisher: Published<EpisodeViewStates>.Publisher { $viewState }
51+
@Published var viewState: EpisodeViewState = .loading(PreviewMocks.mockEpisodeModel)
52+
var viewStatePublished: Published<EpisodeViewState> { _viewState }
53+
var viewStatePublisher: Published<EpisodeViewState>.Publisher { $viewState }
5454

5555
@Published var characters: [CharacterModel] = []
5656
var charactersPublished: Published<[CharacterModel]> { _characters }

NetworkModuleSampleApp/NetworkModuleSampleApp/Scenes/Episode/ViewModel/EpisodeViewModel.swift

Lines changed: 101 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -5,18 +5,23 @@
55

66
import Foundation
77
import NgNetworkModuleCore
8+
import ReactiveNgNetworkModule
9+
import Combine
810

911
final class EpisodeViewModel: EpisodeViewModelProtocol {
1012

1113
let selectedNetworkingAPI: NetworkModuleApiType
1214
let episode: EpisodeModel
1315
let networkModule: NetworkModule
1416

15-
@Published var viewState: EpisodeViewStates
16-
var viewStatePublished: Published<EpisodeViewStates> { _viewState }
17-
var viewStatePublisher: Published<EpisodeViewStates>.Publisher { $viewState }
17+
@Published var viewState: EpisodeViewState
18+
var viewStatePublished: Published<EpisodeViewState> { _viewState }
19+
var viewStatePublisher: Published<EpisodeViewState>.Publisher { $viewState }
1820

1921
private var characters: [CharacterModel] = []
22+
private var cancellables: Set<AnyCancellable> = []
23+
private var tasks: [Task<Void, Never>] = []
24+
private var urlTasks: [URLSessionTask] = []
2025

2126
init(
2227
selectedNetworkingAPI: NetworkModuleApiType,
@@ -30,54 +35,120 @@ final class EpisodeViewModel: EpisodeViewModelProtocol {
3035
}
3136

3237
func fetchData() {
33-
switch selectedNetworkingAPI {
34-
case .classic:
35-
classicNetworkRequest()
36-
case .combine:
37-
combineNetworkRequest()
38-
case .asyncAwait:
39-
asyncAwaitNetworkRequest()
38+
cleanUp()
39+
for characterLink in episode.characters {
40+
guard let url = URL(string: characterLink) else {
41+
viewState = .error(episode, NetworkError.requestParsingFailed)
42+
return
43+
}
44+
45+
let urlRequest = URLRequest(url: url)
46+
call(api: selectedNetworkingAPI, urlRequest: urlRequest)
4047
}
4148
}
4249
}
4350

4451
private extension EpisodeViewModel {
4552

46-
func classicNetworkRequest() {
47-
characters = []
48-
for characterLink in episode.characters {
49-
fetchCharacters(with: characterLink)
53+
func call(api: NetworkModuleApiType, urlRequest: URLRequest) {
54+
switch selectedNetworkingAPI {
55+
case .classic:
56+
fetchCharacterDataUsingClassicApi(urlRequest: urlRequest)
57+
case .combine:
58+
fetchCharacterDataUsingReactiveApi(urlRequest: urlRequest)
59+
case .asyncAwait:
60+
let task = Task<Void, Never> { [weak self] in
61+
await self?.fetchCharacterDataUsingAsyncAwaitApi(urlRequest: urlRequest)
62+
}
63+
tasks.append(task)
5064
}
5165
}
5266

53-
func combineNetworkRequest() {
54-
// TODO: Make Combine Network request
55-
}
56-
57-
func asyncAwaitNetworkRequest() {
58-
// TODO: Make Async/Await Network request
59-
}
60-
61-
func fetchCharacters(with link: String) {
62-
guard let url = URL(string: link) else {
63-
viewState = .error(episode, NetworkError.requestParsingFailed)
64-
return
65-
}
66-
67-
let urlRequest = URLRequest(url: url)
68-
networkModule.performAndDecode(
67+
func fetchCharacterDataUsingClassicApi(urlRequest: URLRequest) {
68+
logNetworkInfo("--- [Classic] Request started: \(urlRequest.requestPath)")
69+
let task = networkModule.performAndDecode(
6970
urlRequest: urlRequest,
7071
responseType: CharacterModel.self
7172
) { [weak self] result in
7273
guard let self else { return }
7374

7475
switch result {
7576
case let .success(character):
77+
logNetworkInfo("--- [Classic] Request completed: \(urlRequest.requestPath)")
7678
self.characters.append(character)
7779
self.viewState = .loaded(self.episode, self.characters)
7880
case let .failure(error):
81+
logNetworkError("--- [Classic] Request error: \(error.localizedDescription)")
7982
self.viewState = .error(self.episode, error)
8083
}
8184
}
85+
urlTasks.append(task)
86+
}
87+
88+
func fetchCharacterDataUsingReactiveApi(urlRequest: URLRequest) {
89+
logNetworkInfo("--- [Reactive] Request started: \(urlRequest.requestPath)")
90+
networkModule
91+
.performAndDecode(urlRequest: urlRequest, responseType: CharacterModel.self, decoder: JSONDecoder())
92+
.sink(
93+
receiveCompletion: { [weak self, episode] completion in
94+
switch completion {
95+
case .finished:
96+
logNetworkInfo("--- [Reactive] Subscription finished for request \(urlRequest.requestPath)")
97+
case let .failure(error):
98+
logNetworkError("--- [Reactive] Request error: \(error.localizedDescription)")
99+
self?.viewState = .error(episode, error)
100+
}
101+
},
102+
receiveValue: { [weak self, episode] characterModel in
103+
logNetworkInfo("--- [Reactive] Request completed: \(urlRequest.requestPath)")
104+
self?.characters.append(characterModel)
105+
self?.viewState = .loaded(episode, self?.characters ?? [])
106+
}
107+
)
108+
.store(in: &cancellables)
109+
}
110+
111+
func fetchCharacterDataUsingAsyncAwaitApi(urlRequest: URLRequest) async {
112+
logNetworkInfo("--- [Async/Await] Request started: \(urlRequest.requestPath)")
113+
do {
114+
let characterModel = try await networkModule.performAndDecode(
115+
urlRequest: urlRequest,
116+
responseType: CharacterModel.self
117+
)
118+
characters.append(characterModel)
119+
logNetworkInfo("--- [Async/Await] Request completed: \(urlRequest.requestPath)")
120+
await updateByMainActor(viewState: .loaded(episode, characters))
121+
} catch {
122+
logNetworkError("--- [Async/Await] Request error: \(error.localizedDescription)")
123+
await updateByMainActor(viewState: .error(episode, error))
124+
}
125+
}
126+
127+
func cleanUp() {
128+
characters = []
129+
cancellables = []
130+
tasks.forEach { task in
131+
task.cancel()
132+
}
133+
urlTasks.forEach { task in
134+
task.cancel()
135+
}
136+
tasks = []
137+
urlTasks = []
138+
}
139+
140+
func updateByMainActor(viewState: EpisodeViewState) async {
141+
// Discussion: Alternatively, you can just annotate `viewState` with @MainActor ...
142+
// ... to ensure it is updated on the Main Thread.
143+
await MainActor.run { [weak self] in
144+
self?.viewState = viewState
145+
}
146+
}
147+
}
148+
149+
private extension URLRequest {
150+
151+
var requestPath: String {
152+
url?.absoluteString ?? ""
82153
}
83154
}

NetworkModuleSampleApp/NetworkModuleSampleApp/Scenes/Episode/ViewModel/EpisodeViewModelProtocol.swift

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,12 @@
55

66
import SwiftUI
77

8-
enum EpisodeViewStates: Equatable {
9-
// case loading(EpisodeModel, Int)
8+
enum EpisodeViewState: Equatable {
109
case loading(EpisodeModel)
1110
case loaded(EpisodeModel, [CharacterModel])
1211
case error(EpisodeModel, Error)
1312

14-
static func == (lhs: EpisodeViewStates, rhs: EpisodeViewStates) -> Bool {
13+
static func == (lhs: EpisodeViewState, rhs: EpisodeViewState) -> Bool {
1514
switch (lhs, rhs) {
1615
case let (.error(data1, e1), .error(data2, e2)):
1716
return data1.id == data2.id && e1.localizedDescription == e2.localizedDescription
@@ -29,9 +28,9 @@ protocol EpisodeViewModelProtocol: AnyObject, ObservableObject {
2928
var selectedNetworkingAPI: NetworkModuleApiType { get }
3029

3130
/// Episode Publisher Properties
32-
var viewState: EpisodeViewStates { get }
33-
var viewStatePublished: Published<EpisodeViewStates> { get }
34-
var viewStatePublisher: Published<EpisodeViewStates>.Publisher { get }
31+
var viewState: EpisodeViewState { get }
32+
var viewStatePublished: Published<EpisodeViewState> { get }
33+
var viewStatePublisher: Published<EpisodeViewState>.Publisher { get }
3534

3635
func fetchData()
3736
}

NetworkModuleSampleApp/NetworkModuleSampleApp/Scenes/EpisodeList/ViewModels/EpisodeListViewModel.swift

Lines changed: 58 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,10 @@
44
//
55

66
import Foundation
7+
import Combine
78
import NgNetworkModuleCore
9+
import ReactiveNgNetworkModule
10+
import ConcurrentNgNetworkModule
811

912
final class EpisodeListViewModel: EpisodeListViewModelProtocol {
1013
@Published private(set) var viewState: EpisodeListViewState = .loading
@@ -14,6 +17,9 @@ final class EpisodeListViewModel: EpisodeListViewModelProtocol {
1417
let selectedNetworkApi: NetworkModuleApiType
1518
let networkModule: NetworkModule
1619

20+
private var task: Task<Void, Never>?
21+
private var urlTask: URLSessionTask?
22+
1723
init(
1824
requestType: NetworkModuleApiType,
1925
networkModule: NetworkModule
@@ -25,33 +31,76 @@ final class EpisodeListViewModel: EpisodeListViewModelProtocol {
2531
func fetchData() {
2632
switch selectedNetworkApi {
2733
case .classic:
28-
classicNetworkRequest()
34+
fetchEpisodesListUsingClassicApi()
2935
case .combine:
30-
combineNetworkRequest()
36+
fetchEpisodesListUsingReactiveApi()
3137
case .asyncAwait:
32-
asynAwaitNetworkRequest()
38+
task?.cancel()
39+
task = Task { [weak self] in
40+
await self?.fetchEpisodesListUsingAsyncAwaitApi()
41+
}
3342
}
3443
}
3544
}
3645

3746
private extension EpisodeListViewModel {
38-
func classicNetworkRequest() {
47+
48+
func fetchEpisodesListUsingClassicApi() {
3949
let request = GetEpisodesListRequest()
40-
networkModule.performAndDecode(request: request, responseType: [EpisodeModel].self) { [weak self] result in
50+
logNetworkInfo("--- [Classic] Request started: \(request.path)")
51+
urlTask?.cancel()
52+
urlTask = networkModule.performAndDecode(
53+
request: request,
54+
responseType: [EpisodeModel].self
55+
) { [weak self] result in
4156
switch result {
4257
case let .success(episodesList):
58+
logNetworkInfo("--- [Classic] Request completed: \(request.path)")
4359
self?.viewState = .loaded(episodesList)
4460
case let .failure(error):
61+
logNetworkError("--- [Classic] Request error: \(error.localizedDescription)")
4562
self?.viewState = .error(error)
4663
}
4764
}
4865
}
4966

50-
func combineNetworkRequest() {
51-
// TODO: Make Combine Network request
67+
func fetchEpisodesListUsingReactiveApi() {
68+
let request = GetEpisodesListRequest()
69+
logNetworkInfo("--- [Reactive] Request started: \(request.path)")
70+
networkModule
71+
.performAndDecode(request: request, responseType: [EpisodeModel].self, decoder: JSONDecoder())
72+
.map { episodeModels in
73+
logNetworkInfo("--- [Reactive] Request completed: \(request.path)")
74+
return EpisodeListViewState.loaded(episodeModels)
75+
}
76+
.catch {
77+
logNetworkError("--- [Reactive] Request error: \($0.localizedDescription)")
78+
return Just(EpisodeListViewState.error($0))
79+
}
80+
.assign(to: &$viewState)
5281
}
5382

54-
func asynAwaitNetworkRequest() {
55-
// TODO: Make Async/Await Network request
83+
func fetchEpisodesListUsingAsyncAwaitApi() async {
84+
let request = GetEpisodesListRequest()
85+
logNetworkInfo("--- [Async/Await] Request started: \(request.path)")
86+
do {
87+
let episodes = try await networkModule.performAndDecode(
88+
request: request,
89+
responseType: [EpisodeModel].self
90+
)
91+
logNetworkInfo("--- Async/AwaitRequest completed: \(request.path)")
92+
await updateByMainActor(viewState: .loaded(episodes))
93+
} catch {
94+
logNetworkError("--- Async/AwaitRequest error: \(error.localizedDescription)")
95+
await updateByMainActor(viewState: .error(error))
96+
}
97+
}
98+
99+
func updateByMainActor(viewState: EpisodeListViewState) async {
100+
// Discussion: Alternatively, you can just annotate `viewState` with @MainActor ...
101+
// ... to ensure it is updated on the Main Thread.
102+
await MainActor.run { [weak self] in
103+
self?.viewState = viewState
104+
}
56105
}
57106
}

0 commit comments

Comments
 (0)