Skip to content

Commit dabb921

Browse files
author
Kamalifard, Mehran
committed
Improvement
1 parent bfe8fd9 commit dabb921

File tree

14 files changed

+125
-97
lines changed

14 files changed

+125
-97
lines changed

EasyCrypto.xcodeproj/project.pbxproj

+13-9
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,7 @@
9292
459B45D12A0BBCD3001C93BA /* Double + Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 459B45D02A0BBCD3001C93BA /* Double + Extension.swift */; };
9393
45B8DF022A0A3E8400BF2EB7 /* MockDataAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45B8DF012A0A3E8400BF2EB7 /* MockDataAction.swift */; };
9494
45B8DF062A0A6C7400BF2EB7 /* CacheStack.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45B8DF052A0A6C7400BF2EB7 /* CacheStack.swift */; };
95+
65239A462BCD0F3B001FC67D /* HandleViewModelStateModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 65239A452BCD0F3B001FC67D /* HandleViewModelStateModifier.swift */; };
9596
A803DFC4297E92D000357A7F /* Color.swift in Sources */ = {isa = PBXBuildFile; fileRef = A803DFC3297E92D000357A7F /* Color.swift */; };
9697
A803DFC9297E93B700357A7F /* FontManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = A803DFC5297E93B600357A7F /* FontManager.swift */; };
9798
A803DFD3297E943100357A7F /* Cancelable.swift in Sources */ = {isa = PBXBuildFile; fileRef = A803DFD2297E943100357A7F /* Cancelable.swift */; };
@@ -230,6 +231,7 @@
230231
459B45D02A0BBCD3001C93BA /* Double + Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Double + Extension.swift"; sourceTree = "<group>"; };
231232
45B8DF012A0A3E8400BF2EB7 /* MockDataAction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockDataAction.swift; sourceTree = "<group>"; };
232233
45B8DF052A0A6C7400BF2EB7 /* CacheStack.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CacheStack.swift; sourceTree = "<group>"; };
234+
65239A452BCD0F3B001FC67D /* HandleViewModelStateModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HandleViewModelStateModifier.swift; sourceTree = "<group>"; };
233235
A803DFBE297E91FF00357A7F /* Launch Screen.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = "Launch Screen.storyboard"; sourceTree = "<group>"; };
234236
A803DFC3297E92D000357A7F /* Color.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Color.swift; sourceTree = "<group>"; };
235237
A803DFC5297E93B600357A7F /* FontManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FontManager.swift; sourceTree = "<group>"; };
@@ -637,14 +639,6 @@
637639
path = TabItemView;
638640
sourceTree = "<group>";
639641
};
640-
457B87862A0F7ECE00B04D9E /* ViewDidLoadModifier */ = {
641-
isa = PBXGroup;
642-
children = (
643-
457B87872A0F7EE400B04D9E /* ViewDidLoadModifier.swift */,
644-
);
645-
path = ViewDidLoadModifier;
646-
sourceTree = "<group>";
647-
};
648642
457B87902A0F8E5E00B04D9E /* CoinDetail */ = {
649643
isa = PBXGroup;
650644
children = (
@@ -672,6 +666,15 @@
672666
path = Cache;
673667
sourceTree = "<group>";
674668
};
669+
65239A482BCD104F001FC67D /* ViewModifier */ = {
670+
isa = PBXGroup;
671+
children = (
672+
65239A452BCD0F3B001FC67D /* HandleViewModelStateModifier.swift */,
673+
457B87872A0F7EE400B04D9E /* ViewDidLoadModifier.swift */,
674+
);
675+
path = ViewModifier;
676+
sourceTree = "<group>";
677+
};
675678
A803DFB7297E918900357A7F /* Theme */ = {
676679
isa = PBXGroup;
677680
children = (
@@ -701,7 +704,6 @@
701704
A803DFBA297E91BF00357A7F /* Core */ = {
702705
isa = PBXGroup;
703706
children = (
704-
457B87862A0F7ECE00B04D9E /* ViewDidLoadModifier */,
705707
A803DFB9297E91AF00357A7F /* Components */,
706708
027A1B0629D08301008B10D2 /* Log */,
707709
02D8622229BBAE4500F77BC7 /* WorkScheduler */,
@@ -772,6 +774,7 @@
772774
A803DFCD297E93F600357A7F /* Support */ = {
773775
isa = PBXGroup;
774776
children = (
777+
65239A482BCD104F001FC67D /* ViewModifier */,
775778
A851A2342989155F007F4CF9 /* MessageHelper */,
776779
A851A22F29891214007F4CF9 /* Extension */,
777780
);
@@ -1203,6 +1206,7 @@
12031206
A851A22B29890C0B007F4CF9 /* Configuration.swift in Sources */,
12041207
A8D4A10E299E0E1300C11107 /* AppRouter.swift in Sources */,
12051208
02BC668029B882F400785196 /* PlaceholderModifier.swift in Sources */,
1209+
65239A462BCD0F3B001FC67D /* HandleViewModelStateModifier.swift in Sources */,
12061210
A8D4A109299E0A6F00C11107 /* CoinDetailCoordinator.swift in Sources */,
12071211
02593C53298E992500ADDA20 /* MarketPriceRepository.swift in Sources */,
12081212
A851A21029890B75007F4CF9 /* NetworkClientManager.swift in Sources */,

EasyCrypto/Base/BaseViewModel.swift

+20-32
Original file line numberDiff line numberDiff line change
@@ -7,71 +7,59 @@
77

88
import Foundation
99
import Combine
10+
import SwiftUI
1011

1112
enum ViewModelStatus: Equatable {
1213
case loadStart
1314
case dismissAlert
14-
case emptyStateHandler(title: String, isShow: Bool)
15+
case emptyStateHandler(title: String)
1516
}
1617

1718
protocol BaseViewModelEventSource: AnyObject {
1819
var loadingState: CurrentValueSubject<ViewModelStatus, Never> { get }
1920
}
2021

2122
protocol ViewModelService: AnyObject {
22-
func callWithProgress<ReturnType>(argument: AnyPublisher<ReturnType?,
23-
APIError>,
24-
callback: @escaping (_ data: ReturnType?) -> Void)
25-
func callWithoutProgress<ReturnType>(argument: AnyPublisher<ReturnType?,
26-
APIError>,
27-
callback: @escaping (_ data: ReturnType?) -> Void)
23+
func call<ReturnType>(callWithIndicator: Bool,
24+
argument: AnyPublisher<ReturnType?,
25+
APIError>,
26+
callback: @escaping (_ data: ReturnType?) -> Void)
2827
}
2928

3029
typealias BaseViewModel = BaseViewModelEventSource & ViewModelService
3130

3231
open class DefaultViewModel: BaseViewModel, ObservableObject {
33-
32+
3433
var loadingState = CurrentValueSubject<ViewModelStatus, Never>(.dismissAlert)
3534
let subscriber = Cancelable()
36-
37-
func callWithProgress<ReturnType>(argument: AnyPublisher<ReturnType?,
38-
APIError>,
39-
callback: @escaping (_ data: ReturnType?) -> Void) {
40-
self.loadingState.send(.loadStart)
41-
35+
36+
func call<ReturnType>(callWithIndicator: Bool = true,
37+
argument: AnyPublisher<ReturnType?,
38+
APIError>,
39+
callback: @escaping (_ data: ReturnType?) -> Void) {
40+
41+
if callWithIndicator {
42+
self.loadingState.send(.loadStart)
43+
}
44+
4245
let completionHandler: (Subscribers.Completion<APIError>) -> Void = { [weak self] completion in
4346
switch completion {
4447
case .failure(let error):
4548
self?.loadingState.send(.dismissAlert)
46-
self?.loadingState.send(.emptyStateHandler(title: error.desc, isShow: true))
49+
self?.loadingState.send(.emptyStateHandler(title: error.desc))
4750
case .finished:
4851
self?.loadingState.send(.dismissAlert)
4952
}
5053
}
51-
54+
5255
let resultValueHandler: (ReturnType?) -> Void = { data in
5356
callback(data)
5457
}
55-
58+
5659
argument
5760
.subscribe(on: WorkScheduler.backgroundWorkScheduler)
5861
.receive(on: WorkScheduler.mainScheduler)
5962
.sink(receiveCompletion: completionHandler, receiveValue: resultValueHandler)
6063
.store(in: subscriber)
6164
}
62-
63-
func callWithoutProgress<ReturnType>(argument: AnyPublisher<ReturnType?,
64-
APIError>,
65-
callback: @escaping (_ data: ReturnType?) -> Void) {
66-
67-
let resultValueHandler: (ReturnType?) -> Void = { data in
68-
callback(data)
69-
}
70-
71-
argument
72-
.subscribe(on: WorkScheduler.backgroundWorkScheduler)
73-
.receive(on: WorkScheduler.mainScheduler)
74-
.sink(receiveCompletion: {_ in }, receiveValue: resultValueHandler)
75-
.store(in: subscriber)
76-
}
7765
}

EasyCrypto/Core/Components/AlertView/CustomAlertView.swift

-1
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,6 @@ struct CustomAlertView: View {
2323
.resizable()
2424
.scaledToFit()
2525
.frame(width: 80, height: 80)
26-
// swiftlint:disable:next opening_brace
2726
} else if let title = title {
2827
Text(title)
2928
.font(.headline)

EasyCrypto/Core/Constants/Constants.swift

+1
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ enum Constants {
1414
enum Title {
1515
static let mainTitle: String = "EasyCrypto"
1616
static let detailTitle: String = "Coin Detail"
17+
static let errorTitle: String = "Error"
1718
}
1819
enum PlaceHolder {
1920
static let searchCoins: String = "Search coins"

EasyCrypto/Presentation/CoinDetail/View/CoinDetailView.swift

+7-18
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ struct CoinDetailView: Coordinatable {
1818

1919
@ObservedObject private(set) var viewModel: CoinDetailViewModel
2020
@State private var isLoading: Bool = false
21+
@State private var presentAlert = false
22+
@State private var alertMessage: String = ""
2123

2224
let subscriber = Cancelable()
2325
var id: String?
@@ -62,8 +64,12 @@ struct CoinDetailView: Coordinatable {
6264
.onAppear {
6365
self.viewModel.apply(.onAppear(id: self.id.orWhenNilOrEmpty("")))
6466
}
67+
.handleViewModelState(viewModel: viewModel,
68+
isLoading: $isLoading,
69+
alertMessage: $alertMessage,
70+
presentAlert: $presentAlert)
6571
}
66-
}.onAppear(perform: handleState)
72+
}
6773
}
6874
}
6975

@@ -73,23 +79,6 @@ extension CoinDetailView {
7379
}
7480
}
7581

76-
extension CoinDetailView {
77-
private func handleState() {
78-
self.viewModel.loadingState
79-
.receive(on: WorkScheduler.mainThread)
80-
.sink { state in
81-
switch state {
82-
case .loadStart:
83-
self.isLoading = true
84-
case .dismissAlert:
85-
self.isLoading = false
86-
case .emptyStateHandler(_, _):
87-
self.isLoading = false
88-
}
89-
}.store(in: subscriber)
90-
}
91-
}
92-
9382
struct CoinDetailView_Previews: PreviewProvider {
9483
static var previews: some View {
9584
CoinDetailView(viewModel: CoinDetailViewModel())

EasyCrypto/Presentation/CoinDetail/ViewModel/CoinDetailViewModel.swift

+1-1
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ extension CoinDetailViewModel: DataFlowProtocol {
5050

5151
func getCoinDetailData(id: String) {
5252
guard !String.isNilOrEmpty(string: id) else {return}
53-
self.callWithProgress(argument: self.coinDetailUsecase.execute(id: id)) { [weak self] data in
53+
self.call(argument: self.coinDetailUsecase.execute(id: id)) { [weak self] data in
5454
guard let data = data else {return}
5555
self?.coinData = data
5656
}

EasyCrypto/Presentation/Detail/View/DetailView.swift

+4
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,10 @@ struct DetailView: View {
2222
}
2323

2424
var body: some View {
25+
content
26+
}
27+
28+
var content: some View {
2529
ZStack {
2630
Color.darkBlue
2731
.edgesIgnoringSafeArea(.all)

EasyCrypto/Presentation/Main/Coordinator/MainCoordinator.swift

+3-3
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import SwiftUI
99
import Combine
1010

1111
struct MainCoordinator: CoordinatorProtocol {
12-
12+
1313
@StateObject var viewModel: MainViewModel
1414

1515
@State var activeRoute: Destination? = Destination(route: .first(item: MarketsPrice()))
@@ -38,9 +38,9 @@ struct MainCoordinator: CoordinatorProtocol {
3838

3939
extension MainCoordinator {
4040
struct Destination: DestinationProtocol {
41-
41+
4242
var route: MainView.Routes
43-
43+
4444
@ViewBuilder
4545
var content: some View {
4646
switch route {

EasyCrypto/Presentation/Main/View/MainView.swift

+19-29
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ struct MainView: Coordinatable {
1212

1313
typealias Route = Routes
1414

15-
@StateObject var viewModel: MainViewModel
15+
@ObservedObject var viewModel: MainViewModel
1616

1717
enum Constant {
1818
static let searchHeight: CGFloat = 55
@@ -24,12 +24,16 @@ struct MainView: Coordinatable {
2424
@State private var shouldShowDropdown = false
2525
@State private var searchText: String = .empty
2626
@State private var isLoading: Bool = false
27-
@State private var presentAlert = true
28-
@State private var alertMesagee: String = ""
27+
@State private var presentAlert = false
28+
@State private var alertMessage: String = ""
2929

3030
let subscriber = Cancelable()
3131

3232
var body: some View {
33+
content
34+
}
35+
36+
var content: some View {
3337
NavigationView {
3438
ZStack {
3539
Color.darkBlue
@@ -63,15 +67,15 @@ struct MainView: Coordinatable {
6367
.padding(.top, 20)
6468
TabView(selection: $tabIndex) {
6569
if tabIndex == 0 {
66-
coinsList()
70+
coinsList
6771
} else {
68-
whishList()
72+
whishList
6973
}
7074
}
7175
.tabViewStyle(PageTabViewStyle(indexDisplayMode: .never))
7276
Spacer()
73-
if !presentAlert {
74-
self.showAlert("Error", alertMesagee)
77+
if presentAlert {
78+
self.showAlert(viewModel.errorTitle, alertMessage)
7579
}
7680
}
7781
}
@@ -80,10 +84,14 @@ struct MainView: Coordinatable {
8084
.onViewDidLoad {
8185
self.viewModel.apply(.onAppear)
8286
}
87+
.handleViewModelState(viewModel: viewModel,
88+
isLoading: $isLoading,
89+
alertMessage: $alertMessage,
90+
presentAlert: $presentAlert)
8391
}
84-
.onAppear(perform: handleState)
8592
}
86-
func coinsList() -> some View {
93+
94+
var coinsList: some View {
8795
ScrollView {
8896
LazyVStack {
8997
ForEach(viewModel.marketData, id: \.id) { item in
@@ -111,7 +119,8 @@ struct MainView: Coordinatable {
111119
.padding()
112120
}
113121
}
114-
func whishList() -> some View {
122+
123+
var whishList: some View {
115124
ScrollView {
116125
VStack {
117126
ForEach(viewModel.wishListData, id: \.symbol) { item in
@@ -135,25 +144,6 @@ extension MainView {
135144
}
136145
}
137146

138-
extension MainView {
139-
private func handleState() {
140-
self.viewModel.loadingState
141-
.receive(on: WorkScheduler.mainThread)
142-
.sink { state in
143-
switch state {
144-
case .loadStart:
145-
self.isLoading = true
146-
case .dismissAlert:
147-
self.isLoading = false
148-
case .emptyStateHandler(let message, _):
149-
self.isLoading = false
150-
self.presentAlert = false
151-
self.alertMesagee = message
152-
}
153-
}.store(in: subscriber)
154-
}
155-
}
156-
157147
extension MainView {
158148
func showAlert(_ title: String, _ message: String) -> some View {
159149
CustomAlertView(title: title, message: message, primaryButtonLabel: "Retry", primaryButtonAction: {

EasyCrypto/Presentation/Main/ViewModel/MainViewModel.swift

+9-2
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ protocol DefaultMainViewModel: MainViewModelProtocol { }
1919
final class MainViewModel: DefaultViewModel, DefaultMainViewModel {
2020

2121
let title: String = Constants.Title.mainTitle
22+
let errorTitle: String = Constants.Title.errorTitle
2223

2324
private let marketPriceUsecase: MarketPriceUsecaseProtocol
2425
private let searchMarketUsecase: SearchMarketUsecaseProtocol
@@ -100,7 +101,13 @@ extension MainViewModel: DataFlowProtocol {
100101
func getMarketData(vs_currency: String = "usd",
101102
order: String = "market_cap_desc",
102103
sparkline: Bool = false) {
103-
self.callWithProgress(argument: self.marketPriceUsecase.execute(vs_currency: vs_currency,
104+
105+
// Check if the number of market data entries is already 30 to limit service calls
106+
if marketData.count == 30 {
107+
return
108+
}
109+
110+
self.call(argument: self.marketPriceUsecase.execute(vs_currency: vs_currency,
104111
order: order,
105112
per_page: self.perPage,
106113
page: self.page,
@@ -112,7 +119,7 @@ extension MainViewModel: DataFlowProtocol {
112119
}
113120

114121
func searchMarketData(text: String) {
115-
self.callWithProgress(argument: self.searchMarketUsecase.execute(text: text)) { [weak self] data in
122+
self.call(argument: self.searchMarketUsecase.execute(text: text)) { [weak self] data in
116123
let coin = data?.coins ?? []
117124
self?.searchData = []
118125
self?.searchData = coin

0 commit comments

Comments
 (0)