Skip to content

Commit 146eeb4

Browse files
authored
πŸ”€ SearchViewModel κ΅¬ν˜„ 및 μ—°κ΄€λœ κΈ°λŠ₯ κ΅¬ν˜„ (#78)
* ✨ SearchViewModel κ΅¬ν˜„ * πŸ”₯ λΆˆν•„μš”ν•œ μ½”λ“œ μ‚­μ œ * ✨ SearchViewComponent κ΅¬ν˜„ * ✨ HomeView μ΄λ‹ˆμ…œλΌμ΄μ € μˆ˜μ • 및 viewModel 속성 μΆ”κ°€
1 parent 3b91473 commit 146eeb4

File tree

6 files changed

+166
-13
lines changed

6 files changed

+166
-13
lines changed

β€ŽPyeonHaeng-iOS.xcodeproj/project.pbxproj

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,8 @@
4949
BAF2BEB32B61236100931AF0 /* Localizable.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = BAF2BEB22B61236100931AF0 /* Localizable.xcstrings */; };
5050
E50176262B6A204F0098D1BE /* ProductInfoLineGraphView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E50176252B6A204F0098D1BE /* ProductInfoLineGraphView.swift */; };
5151
E50584532B763C8C002FDACF /* ProductInfoViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = E50584522B763C8C002FDACF /* ProductInfoViewModel.swift */; };
52+
E52F371B2B947DC8000EBAD5 /* SearchViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = E52F371A2B947DC8000EBAD5 /* SearchViewModel.swift */; };
53+
E52F371D2B94D239000EBAD5 /* SearchViewComponent.swift in Sources */ = {isa = PBXBuildFile; fileRef = E52F371C2B94D239000EBAD5 /* SearchViewComponent.swift */; };
5254
E5462C662B65677B00E9FDF2 /* PromotionTag.swift in Sources */ = {isa = PBXBuildFile; fileRef = E5462C652B65677B00E9FDF2 /* PromotionTag.swift */; };
5355
E55DD5122B91DE9500AA63C0 /* SearchListCardView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E55DD5112B91DE9500AA63C0 /* SearchListCardView.swift */; };
5456
E55DD5182B9370D100AA63C0 /* SearchAPI in Frameworks */ = {isa = PBXBuildFile; productRef = E55DD5172B9370D100AA63C0 /* SearchAPI */; };
@@ -114,6 +116,8 @@
114116
BAF2BEB22B61236100931AF0 /* Localizable.xcstrings */ = {isa = PBXFileReference; lastKnownFileType = text.json.xcstrings; path = Localizable.xcstrings; sourceTree = "<group>"; };
115117
E50176252B6A204F0098D1BE /* ProductInfoLineGraphView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProductInfoLineGraphView.swift; sourceTree = "<group>"; };
116118
E50584522B763C8C002FDACF /* ProductInfoViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProductInfoViewModel.swift; sourceTree = "<group>"; };
119+
E52F371A2B947DC8000EBAD5 /* SearchViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchViewModel.swift; sourceTree = "<group>"; };
120+
E52F371C2B94D239000EBAD5 /* SearchViewComponent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchViewComponent.swift; sourceTree = "<group>"; };
117121
E5462C652B65677B00E9FDF2 /* PromotionTag.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PromotionTag.swift; sourceTree = "<group>"; };
118122
E55DD5112B91DE9500AA63C0 /* SearchListCardView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchListCardView.swift; sourceTree = "<group>"; };
119123
E55DD5152B936DD200AA63C0 /* SearchAPI */ = {isa = PBXFileReference; lastKnownFileType = folder; name = SearchAPI; path = APIService/Sources/SearchAPI; sourceTree = "<group>"; };
@@ -264,7 +268,9 @@
264268
BA28F18F2B61565F0052855E /* ProductSearchScene */ = {
265269
isa = PBXGroup;
266270
children = (
271+
E52F371C2B94D239000EBAD5 /* SearchViewComponent.swift */,
267272
BA28F1902B61566E0052855E /* SearchView.swift */,
273+
E52F371A2B947DC8000EBAD5 /* SearchViewModel.swift */,
268274
E55DD5112B91DE9500AA63C0 /* SearchListCardView.swift */,
269275
);
270276
path = ProductSearchScene;
@@ -572,13 +578,15 @@
572578
E57F2AA82B774CA700E12B3D /* ProductInfoDependency.swift in Sources */,
573579
E55DD5122B91DE9500AA63C0 /* SearchListCardView.swift in Sources */,
574580
9CE4B4712B6F0B57002DC446 /* OnboardingPage.swift in Sources */,
581+
E52F371B2B947DC8000EBAD5 /* SearchViewModel.swift in Sources */,
575582
BAE159DE2B663A9A002DCF94 /* HomeProductSorterView.swift in Sources */,
576583
BAE159D82B65FA6F002DCF94 /* HomeProductDetailSelectionView.swift in Sources */,
577584
E50176262B6A204F0098D1BE /* ProductInfoLineGraphView.swift in Sources */,
578585
BAB5CF252B6B7C5A008B24BF /* AppRootComponent.swift in Sources */,
579586
BAA4D9AF2B5A1795005999F8 /* SplashView.swift in Sources */,
580587
BA8E83242B8EF83B00FE968C /* ProductConfiguration.swift in Sources */,
581588
BAA4D9AD2B5A1795005999F8 /* PyeonHaengApp.swift in Sources */,
589+
E52F371D2B94D239000EBAD5 /* SearchViewComponent.swift in Sources */,
582590
);
583591
runOnlyForDeploymentPostprocessing = 0;
584592
};

β€ŽPyeonHaeng-iOS/Sources/Scenes/HomeScene/View/HomeView.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,10 +48,10 @@ struct HomeView<ViewModel>: View where ViewModel: HomeViewModelRepresentable {
4848
.foregroundStyle(.gray900)
4949
}
5050
}
51-
5251
ToolbarItemGroup(placement: .topBarTrailing) {
5352
NavigationLink {
54-
SearchView()
53+
let component = SearchViewComponent()
54+
SearchView(viewModel: SearchViewModel(service: component.searchService))
5555
.toolbarRole(.editor)
5656
} label: {
5757
Image.magnifyingglass

β€ŽPyeonHaeng-iOS/Sources/Scenes/ProductSearchScene/SearchView.swift

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,13 @@ import SwiftUI
1010

1111
// MARK: - SearchView
1212

13-
struct SearchView: View {
13+
struct SearchView<ViewModel>: View where ViewModel: SearchViewModelRepresentable {
14+
@StateObject private var viewModel: ViewModel
15+
16+
init(viewModel: @autoclosure @escaping () -> ViewModel) {
17+
_viewModel = .init(wrappedValue: viewModel())
18+
}
19+
1420
var body: some View {
1521
ScrollView {
1622
LazyVStack {
@@ -88,7 +94,3 @@ private enum Metrics {
8894

8995
static let removeButtonSize = 32.0
9096
}
91-
92-
#Preview {
93-
SearchView()
94-
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
//
2+
// SearchViewComponent.swift
3+
// PyeonHaeng-iOS
4+
//
5+
// Created by 김응철 on 3/4/24.
6+
//
7+
8+
import Foundation
9+
import Network
10+
import SearchAPI
11+
import SearchAPISupport
12+
13+
// MARK: - SearchDependency
14+
15+
protocol SearchDependency {
16+
var searchService: SearchServiceRepresentable { get }
17+
}
18+
19+
// MARK: - SearchViewComponent
20+
21+
struct SearchViewComponent: SearchDependency {
22+
let searchService: SearchServiceRepresentable
23+
24+
init() {
25+
let searchNetworking: Networking = {
26+
let configuration: URLSessionConfiguration
27+
#if DEBUG
28+
configuration = .ephemeral
29+
configuration.protocolClasses = [SearchURLProtocol.self]
30+
#else
31+
configuration = .default
32+
#endif
33+
let provider = NetworkProvider(session: URLSession(configuration: configuration))
34+
return provider
35+
}()
36+
37+
searchService = SearchService(network: searchNetworking)
38+
}
39+
}
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
//
2+
// SearchViewModel.swift
3+
// PyeonHaeng-iOS
4+
//
5+
// Created by 김응철 on 3/3/24.
6+
//
7+
8+
import Entity
9+
import Foundation
10+
import Log
11+
import SearchAPI
12+
13+
// MARK: - SearchAction
14+
15+
enum SearchAction {
16+
case textChanged(String)
17+
case loadMoreProducts
18+
}
19+
20+
// MARK: - SearchState
21+
22+
struct SearchState {
23+
var currentText = ""
24+
var products = [SearchProduct]()
25+
var offset = 0
26+
var hasMore = false
27+
28+
/// ν™”λ©΄ 쀑앙에 ν‘œμ‹œλ˜λŠ” λ‘œλ”© 인디케이터
29+
var isLoading = false
30+
/// 리슀트 맨 ν•˜λ‹¨μ— ν‘œμ‹œλ˜λŠ” λ‘œλ”© 인디케이터
31+
var isMoreLoading = false
32+
}
33+
34+
// MARK: - SearchViewModelRepresentable
35+
36+
protocol SearchViewModelRepresentable: ObservableObject {
37+
var state: SearchState { get }
38+
func trigger(_ action: SearchAction)
39+
}
40+
41+
// MARK: - SearchViewModel
42+
43+
final class SearchViewModel: SearchViewModelRepresentable {
44+
/// 비동기 ν•¨μˆ˜λ₯Ό μ‹€ν–‰ν•©λ‹ˆλ‹€. λ§Œμ•½ 였λ₯˜κ°€ λ°œμƒν•˜λ©΄ Log둜 값을 확인할 수 μžˆμŠ΅λ‹ˆλ‹€.
45+
func performAsyncAction(_ action: () async throws -> Void) async {
46+
do {
47+
try await action()
48+
} catch {
49+
Log.make(with: .viewModel)?.error("\(String(describing: error))")
50+
}
51+
}
52+
53+
private let service: SearchServiceRepresentable
54+
55+
@Published private(set) var state: SearchState = .init()
56+
57+
init(service: SearchServiceRepresentable) {
58+
self.service = service
59+
}
60+
61+
func trigger(_ action: SearchAction) {
62+
Task {
63+
await handle(action)
64+
}
65+
}
66+
67+
private func handle(_ action: SearchAction) async {
68+
// 비동기 ν•¨μˆ˜λ₯Ό μ‹€ν–‰ν•©λ‹ˆλ‹€. λ§Œμ•½ 였λ₯˜κ°€ λ°œμƒν•˜λ©΄ Log둜 값을 확인할 수 μžˆμŠ΅λ‹ˆλ‹€.
69+
func performAsyncAction(_ action: () async throws -> Void) async {
70+
do {
71+
try await action()
72+
} catch {
73+
Log.make(with: .viewModel)?.error("\(String(describing: error))")
74+
}
75+
}
76+
77+
switch action {
78+
case let .textChanged(text):
79+
state.currentText = text
80+
state.offset = 0
81+
await performAsyncAction {
82+
try await fetchSearchList(isReplace: true)
83+
}
84+
85+
case .loadMoreProducts:
86+
await performAsyncAction {
87+
try await fetchSearchList(isReplace: false)
88+
}
89+
}
90+
}
91+
92+
private func fetchSearchList(isReplace: Bool) async throws {
93+
let request = SearchProductRequest(
94+
name: state.currentText,
95+
order: .normal,
96+
pageSize: 20,
97+
offset: state.offset
98+
)
99+
100+
let paginatedModel = try await service.fetchSearchList(request: request)
101+
102+
state.hasMore = paginatedModel.hasMore
103+
state.offset += 1
104+
if isReplace {
105+
state.products = paginatedModel.results
106+
} else {
107+
state.products.append(contentsOf: paginatedModel.results)
108+
}
109+
}
110+
}

β€ŽShared/Sources/DesignSystem/Sources/Extensions/Image+Resource.swift

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -85,9 +85,3 @@ public extension Image {
8585
static let xCircleFill: Image = .init(.xCircleFill)
8686
static let xCircle: Image = .init(.xCircle)
8787
}
88-
89-
public extension UIImage {
90-
static var backButtonImage: UIImage? {
91-
UIImage(resource: .chevronLeftLarge)
92-
}
93-
}

0 commit comments

Comments
Β (0)