Skip to content

Commit ca4b42f

Browse files
committed
Refactor ViewModel extensions
1 parent 6eede0e commit ca4b42f

File tree

11 files changed

+545
-649
lines changed

11 files changed

+545
-649
lines changed

.swiftlint.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ opt_in_rules: # some rules are only opt-in
8585
# - multiple_closures_with_trailing_closure
8686
- nesting
8787
- nimble_operator
88-
- no_extension_access_modifier
88+
# - no_extension_access_modifier
8989
- no_fallthrough_only
9090
# - no_grouping_extension
9191
- notification_center_detachment

CleanArchitecture.xcodeproj/project.pbxproj

Lines changed: 16 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,8 @@
1616
173865AA24D3BC6A0059E3AD /* APIOutput.swift in Sources */ = {isa = PBXBuildFile; fileRef = 173865A924D3BC6A0059E3AD /* APIOutput.swift */; };
1717
173865AC24D3BC840059E3AD /* API+Repo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 173865AB24D3BC840059E3AD /* API+Repo.swift */; };
1818
173865AE24D3BCBC0059E3AD /* APIUrls.swift in Sources */ = {isa = PBXBuildFile; fileRef = 173865AD24D3BCBC0059E3AD /* APIUrls.swift */; };
19-
173865B024D40FBE0059E3AD /* ViewModelType+GetItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 173865AF24D40FBE0059E3AD /* ViewModelType+GetItem.swift */; };
20-
173865B224D419700059E3AD /* ViewModelType+GetPage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 173865B124D419700059E3AD /* ViewModelType+GetPage.swift */; };
19+
173865B024D40FBE0059E3AD /* ViewModel+GetItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 173865AF24D40FBE0059E3AD /* ViewModel+GetItem.swift */; };
20+
173865B224D419700059E3AD /* ViewModel+GetPage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 173865B124D419700059E3AD /* ViewModel+GetPage.swift */; };
2121
1742B36324D7B00000C82D15 /* GettingRepos.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1742B36224D7B00000C82D15 /* GettingRepos.swift */; };
2222
1742B36524D7B01600C82D15 /* RepoGateway.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1742B36424D7B01600C82D15 /* RepoGateway.swift */; };
2323
1742B36B24D7B22B00C82D15 /* ReposAssembler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1742B36624D7B22B00C82D15 /* ReposAssembler.swift */; };
@@ -76,6 +76,7 @@
7676
1770EC3D24DBA7450022FBFE /* UserValidationError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1770EC3C24DBA7440022FBFE /* UserValidationError.swift */; };
7777
1770EC3F24DBA7670022FBFE /* ValidatingPassword.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1770EC3E24DBA7670022FBFE /* ValidatingPassword.swift */; };
7878
1770EC4124DBBB0D0022FBFE /* ValidationResult+.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1770EC4024DBBB0D0022FBFE /* ValidationResult+.swift */; };
79+
1770EC4324DD2F2F0022FBFE /* ScreenLoadingType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1770EC4224DD2F2F0022FBFE /* ScreenLoadingType.swift */; };
7980
1781CF3924BD8D58004ED1C6 /* GatewaysAssembler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1781CF3824BD8D58004ED1C6 /* GatewaysAssembler.swift */; };
8081
1781CF4024BD8FB8004ED1C6 /* ProductDetailAssembler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1781CF3B24BD8FB8004ED1C6 /* ProductDetailAssembler.swift */; };
8182
1781CF4124BD8FB8004ED1C6 /* ProductDetailNavigator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1781CF3C24BD8FB8004ED1C6 /* ProductDetailNavigator.swift */; };
@@ -112,7 +113,7 @@
112113
178809AA24C0412F00002215 /* Observable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 178809A924C0412F00002215 /* Observable.swift */; };
113114
178809AC24C0446B00002215 /* ErrorTracker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 178809AB24C0446B00002215 /* ErrorTracker.swift */; };
114115
178809AE24C04A7E00002215 /* ActivityTracker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 178809AD24C04A7E00002215 /* ActivityTracker.swift */; };
115-
178809B024C0520000002215 /* ViewModelType+GetList.swift in Sources */ = {isa = PBXBuildFile; fileRef = 178809AF24C0520000002215 /* ViewModelType+GetList.swift */; };
116+
178809B024C0520000002215 /* ViewModel+GetList.swift in Sources */ = {isa = PBXBuildFile; fileRef = 178809AF24C0520000002215 /* ViewModel+GetList.swift */; };
116117
17A5B1F224D804DA004BD209 /* UIViewController+.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17A5B1F124D804DA004BD209 /* UIViewController+.swift */; };
117118
17A5B1F424D8080B004BD209 /* UIViewController+Combine.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17A5B1F324D8080B004BD209 /* UIViewController+Combine.swift */; };
118119
17A5B1F624D81561004BD209 /* GenericSubscriber.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17A5B1F524D81561004BD209 /* GenericSubscriber.swift */; };
@@ -149,8 +150,8 @@
149150
173865A924D3BC6A0059E3AD /* APIOutput.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APIOutput.swift; sourceTree = "<group>"; };
150151
173865AB24D3BC840059E3AD /* API+Repo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "API+Repo.swift"; sourceTree = "<group>"; };
151152
173865AD24D3BCBC0059E3AD /* APIUrls.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APIUrls.swift; sourceTree = "<group>"; };
152-
173865AF24D40FBE0059E3AD /* ViewModelType+GetItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ViewModelType+GetItem.swift"; sourceTree = "<group>"; };
153-
173865B124D419700059E3AD /* ViewModelType+GetPage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ViewModelType+GetPage.swift"; sourceTree = "<group>"; };
153+
173865AF24D40FBE0059E3AD /* ViewModel+GetItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ViewModel+GetItem.swift"; sourceTree = "<group>"; };
154+
173865B124D419700059E3AD /* ViewModel+GetPage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ViewModel+GetPage.swift"; sourceTree = "<group>"; };
154155
1742B36224D7B00000C82D15 /* GettingRepos.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GettingRepos.swift; sourceTree = "<group>"; };
155156
1742B36424D7B01600C82D15 /* RepoGateway.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RepoGateway.swift; sourceTree = "<group>"; };
156157
1742B36624D7B22B00C82D15 /* ReposAssembler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReposAssembler.swift; sourceTree = "<group>"; };
@@ -203,6 +204,7 @@
203204
1770EC3C24DBA7440022FBFE /* UserValidationError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserValidationError.swift; sourceTree = "<group>"; };
204205
1770EC3E24DBA7670022FBFE /* ValidatingPassword.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ValidatingPassword.swift; sourceTree = "<group>"; };
205206
1770EC4024DBBB0D0022FBFE /* ValidationResult+.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ValidationResult+.swift"; sourceTree = "<group>"; };
207+
1770EC4224DD2F2F0022FBFE /* ScreenLoadingType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScreenLoadingType.swift; sourceTree = "<group>"; };
206208
1781CF3824BD8D58004ED1C6 /* GatewaysAssembler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GatewaysAssembler.swift; sourceTree = "<group>"; };
207209
1781CF3B24BD8FB8004ED1C6 /* ProductDetailAssembler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProductDetailAssembler.swift; sourceTree = "<group>"; };
208210
1781CF3C24BD8FB8004ED1C6 /* ProductDetailNavigator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProductDetailNavigator.swift; sourceTree = "<group>"; };
@@ -244,7 +246,7 @@
244246
178809A924C0412F00002215 /* Observable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Observable.swift; sourceTree = "<group>"; };
245247
178809AB24C0446B00002215 /* ErrorTracker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ErrorTracker.swift; sourceTree = "<group>"; };
246248
178809AD24C04A7E00002215 /* ActivityTracker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActivityTracker.swift; sourceTree = "<group>"; };
247-
178809AF24C0520000002215 /* ViewModelType+GetList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ViewModelType+GetList.swift"; sourceTree = "<group>"; };
249+
178809AF24C0520000002215 /* ViewModel+GetList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ViewModel+GetList.swift"; sourceTree = "<group>"; };
248250
17A5B1F124D804DA004BD209 /* UIViewController+.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIViewController+.swift"; sourceTree = "<group>"; };
249251
17A5B1F324D8080B004BD209 /* UIViewController+Combine.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIViewController+Combine.swift"; sourceTree = "<group>"; };
250252
17A5B1F524D81561004BD209 /* GenericSubscriber.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GenericSubscriber.swift; sourceTree = "<group>"; };
@@ -294,9 +296,10 @@
294296
178809AB24C0446B00002215 /* ErrorTracker.swift */,
295297
178809AD24C04A7E00002215 /* ActivityTracker.swift */,
296298
1787231B24BD53BF00D90F8E /* ViewModel.swift */,
297-
178809AF24C0520000002215 /* ViewModelType+GetList.swift */,
298-
173865AF24D40FBE0059E3AD /* ViewModelType+GetItem.swift */,
299-
173865B124D419700059E3AD /* ViewModelType+GetPage.swift */,
299+
1770EC4224DD2F2F0022FBFE /* ScreenLoadingType.swift */,
300+
178809AF24C0520000002215 /* ViewModel+GetList.swift */,
301+
173865AF24D40FBE0059E3AD /* ViewModel+GetItem.swift */,
302+
173865B124D419700059E3AD /* ViewModel+GetPage.swift */,
300303
1765B2CB24C68D940096D210 /* CancelBag.swift */,
301304
17DCF1B824BF01ED00D970B4 /* Publisher+.swift */,
302305
176BAA4724D130B800324D3C /* Bindable.swift */,
@@ -903,7 +906,7 @@
903906
17A5B1F424D8080B004BD209 /* UIViewController+Combine.swift in Sources */,
904907
17DCF1B624BEE76800D970B4 /* Driver.swift in Sources */,
905908
174A188F24C821B600AB39D9 /* LoadingView.swift in Sources */,
906-
173865B224D419700059E3AD /* ViewModelType+GetPage.swift in Sources */,
909+
173865B224D419700059E3AD /* ViewModel+GetPage.swift in Sources */,
907910
178809AA24C0412F00002215 /* Observable.swift in Sources */,
908911
1742B37B24D7E88900C82D15 /* AlertMessage.swift in Sources */,
909912
176BAA5C24D175C200324D3C /* AuthGateway.swift in Sources */,
@@ -940,6 +943,7 @@
940943
176BAA6D24D26CD900324D3C /* APIOutputBase.swift in Sources */,
941944
1787232D24BD547600D90F8E /* AppAssembler.swift in Sources */,
942945
173865AA24D3BC6A0059E3AD /* APIOutput.swift in Sources */,
946+
1770EC4324DD2F2F0022FBFE /* ScreenLoadingType.swift in Sources */,
943947
178722FB24BD537D00D90F8E /* ContentView.swift in Sources */,
944948
1781CF3924BD8D58004ED1C6 /* GatewaysAssembler.swift in Sources */,
945949
1787232E24BD547600D90F8E /* AppNavigator.swift in Sources */,
@@ -961,8 +965,8 @@
961965
176BAA4B24D1319C00324D3C /* MenuCell.swift in Sources */,
962966
176BAA5824D167A400324D3C /* LoggingIn.swift in Sources */,
963967
1787234D24BD560500D90F8E /* MainViewModel.swift in Sources */,
964-
178809B024C0520000002215 /* ViewModelType+GetList.swift in Sources */,
965-
173865B024D40FBE0059E3AD /* ViewModelType+GetItem.swift in Sources */,
968+
178809B024C0520000002215 /* ViewModel+GetList.swift in Sources */,
969+
173865B024D40FBE0059E3AD /* ViewModel+GetItem.swift in Sources */,
966970
1742B36B24D7B22B00C82D15 /* ReposAssembler.swift in Sources */,
967971
1765B2CC24C68D940096D210 /* CancelBag.swift in Sources */,
968972
1770EC4124DBBB0D0022FBFE /* ValidationResult+.swift in Sources */,

CleanArchitecture/Scene/Products/ProductsViewModel.swift

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -30,13 +30,11 @@ extension ProductsViewModel: ViewModel {
3030
}
3131

3232
func transform(_ input: Input, cancelBag: CancelBag) -> Output {
33-
let result = getList(
34-
loadTrigger: input.loadTrigger,
35-
reloadTrigger: input.reloadTrigger,
36-
getItems: useCase.getProducts
37-
)
38-
39-
let (products, error, isLoading, isReloading) = result.destructured
33+
let getListInput = GetListInput(loadTrigger: input.loadTrigger,
34+
reloadTrigger: input.reloadTrigger,
35+
getItems: useCase.getProducts)
36+
37+
let (products, error, isLoading, isReloading) = getList(input: getListInput).destructured
4038

4139
let output = Output()
4240

CleanArchitecture/Scene/Repos/ReposViewModel.swift

Lines changed: 6 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -34,19 +34,13 @@ extension ReposViewModel: ViewModel {
3434

3535
func transform(_ input: Input, cancelBag: CancelBag) -> Output {
3636
let output = Output()
37-
let pageSubject = CurrentValueSubject<PagingInfo<Repo>, Never>(PagingInfo())
38-
let errorTracker = ErrorTracker()
3937

40-
let getPageResult = getPage(
41-
pageSubject: pageSubject,
42-
errorTracker: errorTracker,
43-
loadTrigger: input.loadTrigger,
44-
reloadTrigger: input.reloadTrigger,
45-
loadMoreTrigger: input.loadMoreTrigger,
46-
getItems: { _, page in self.useCase.getRepos(page: page) },
47-
mapper: { $0 })
38+
let getPageInput = GetPageInput(loadTrigger: input.loadTrigger,
39+
reloadTrigger: input.reloadTrigger,
40+
loadMoreTrigger: input.loadMoreTrigger,
41+
getItems: useCase.getRepos)
4842

49-
let (page, error, isLoading, isReloading, isLoadingMore) = getPageResult.destructured
43+
let (page, error, isLoading, isReloading, isLoadingMore) = getPage(input: getPageInput).destructured
5044

5145
page
5246
.map { $0.items.map(RepoItemViewModel.init) }
@@ -55,7 +49,7 @@ extension ReposViewModel: ViewModel {
5549

5650
input.selectRepoTrigger
5751
.handleEvents(receiveOutput: { indexPath in
58-
let repo = pageSubject.value.items[indexPath.row]
52+
let repo = getPageInput.pageSubject.value.items[indexPath.row]
5953
self.navigator.toRepoDetail(repo: repo)
6054
})
6155
.sink()
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
//
2+
// ScreenLoadingType.swift
3+
// CleanArchitecture
4+
//
5+
// Created by Tuan Truong on 8/7/20.
6+
// Copyright © 2020 Tuan Truong. All rights reserved.
7+
//
8+
9+
public enum ScreenLoadingType<Input> {
10+
case loading(Input)
11+
case reloading(Input)
12+
}
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
//
2+
// ViewModel+GetItem.swift
3+
// CleanArchitecture
4+
//
5+
// Created by Tuan Truong on 7/31/20.
6+
// Copyright © 2020 Tuan Truong. All rights reserved.
7+
//
8+
9+
import Combine
10+
11+
public struct GetItemInput<TriggerInput, Item> {
12+
let errorTracker: ErrorTracker
13+
let loadTrigger: AnyPublisher<TriggerInput, Never>
14+
let reloadTrigger: AnyPublisher<TriggerInput, Never>
15+
let getItem: (TriggerInput) -> AnyPublisher<Item, Error>
16+
let reloadItem: (TriggerInput) -> AnyPublisher<Item, Error>
17+
18+
public init(errorTracker: ErrorTracker,
19+
loadTrigger: AnyPublisher<TriggerInput, Never>,
20+
getItem: @escaping (TriggerInput) -> AnyPublisher<Item, Error>,
21+
reloadTrigger: AnyPublisher<TriggerInput, Never>,
22+
reloadItem: @escaping (TriggerInput) -> AnyPublisher<Item, Error>) {
23+
self.errorTracker = errorTracker
24+
self.loadTrigger = loadTrigger
25+
self.reloadTrigger = reloadTrigger
26+
self.getItem = getItem
27+
self.reloadItem = reloadItem
28+
}
29+
}
30+
31+
public extension GetItemInput {
32+
init(errorTracker: ErrorTracker = ErrorTracker(),
33+
loadTrigger: AnyPublisher<TriggerInput, Never>,
34+
reloadTrigger: AnyPublisher<TriggerInput, Never>,
35+
getItem: @escaping (TriggerInput) -> AnyPublisher<Item, Error>) {
36+
self.init(errorTracker: errorTracker,
37+
loadTrigger: loadTrigger,
38+
getItem: getItem,
39+
reloadTrigger: reloadTrigger,
40+
reloadItem: getItem)
41+
}
42+
}
43+
44+
public struct GetItemResult<Item> {
45+
public var item: AnyPublisher<Item, Never>
46+
public var error: AnyPublisher<Error, Never>
47+
public var isLoading: AnyPublisher<Bool, Never>
48+
public var isReloading: AnyPublisher<Bool, Never>
49+
50+
// swiftlint:disable:next large_tuple
51+
public var destructured: (
52+
AnyPublisher<Item, Never>,
53+
AnyPublisher<Error, Never>,
54+
AnyPublisher<Bool, Never>,
55+
AnyPublisher<Bool, Never>) {
56+
return (item, error, isLoading, isReloading)
57+
}
58+
59+
public init(item: AnyPublisher<Item, Never>,
60+
error: AnyPublisher<Error, Never>,
61+
isLoading: AnyPublisher<Bool, Never>,
62+
isReloading: AnyPublisher<Bool, Never>) {
63+
self.item = item
64+
self.error = error
65+
self.isLoading = isLoading
66+
self.isReloading = isReloading
67+
}
68+
}
69+
70+
extension ViewModel {
71+
public func getItem<TriggerInput, Item>(input: GetItemInput<TriggerInput, Item>) -> GetItemResult<Item> {
72+
let loadingActivityTracker = ActivityTracker(false)
73+
let reloadingActivityTracker = ActivityTracker(false)
74+
75+
let item = Publishers.Merge(
76+
input.loadTrigger.map { ScreenLoadingType.loading($0) },
77+
input.reloadTrigger.map { ScreenLoadingType.reloading($0) }
78+
)
79+
.filter { _ in
80+
!loadingActivityTracker.value && !reloadingActivityTracker.value
81+
}
82+
.map { triggerType -> AnyPublisher<Item, Never> in
83+
switch triggerType {
84+
case .loading(let triggerInput):
85+
return input.getItem(triggerInput)
86+
.trackError(input.errorTracker)
87+
.trackActivity(loadingActivityTracker)
88+
.catch { _ in Empty() }
89+
.eraseToAnyPublisher()
90+
case .reloading(let triggerInput):
91+
return input.reloadItem(triggerInput)
92+
.trackError(input.errorTracker)
93+
.trackActivity(reloadingActivityTracker)
94+
.catch { _ in Empty() }
95+
.eraseToAnyPublisher()
96+
}
97+
}
98+
.switchToLatest()
99+
.eraseToAnyPublisher()
100+
101+
let error = input.errorTracker.eraseToAnyPublisher()
102+
let isLoading = loadingActivityTracker.eraseToAnyPublisher()
103+
let isReloading = reloadingActivityTracker.eraseToAnyPublisher()
104+
105+
return GetItemResult(
106+
item: item,
107+
error: error,
108+
isLoading: isLoading,
109+
isReloading: isReloading
110+
)
111+
}
112+
}

0 commit comments

Comments
 (0)