Skip to content

Commit

Permalink
πŸ”€ SearchViewModel κ΅¬ν˜„ 및 μ—°κ΄€λœ κΈ°λŠ₯ κ΅¬ν˜„ (#78)
Browse files Browse the repository at this point in the history
* ✨ SearchViewModel κ΅¬ν˜„

* πŸ”₯ λΆˆν•„μš”ν•œ μ½”λ“œ μ‚­μ œ

* ✨ SearchViewComponent κ΅¬ν˜„

* ✨ HomeView μ΄λ‹ˆμ…œλΌμ΄μ € μˆ˜μ • 및 viewModel 속성 μΆ”κ°€
  • Loading branch information
eung7 authored Mar 4, 2024
1 parent 3b91473 commit 146eeb4
Show file tree
Hide file tree
Showing 6 changed files with 166 additions and 13 deletions.
8 changes: 8 additions & 0 deletions PyeonHaeng-iOS.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@
BAF2BEB32B61236100931AF0 /* Localizable.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = BAF2BEB22B61236100931AF0 /* Localizable.xcstrings */; };
E50176262B6A204F0098D1BE /* ProductInfoLineGraphView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E50176252B6A204F0098D1BE /* ProductInfoLineGraphView.swift */; };
E50584532B763C8C002FDACF /* ProductInfoViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = E50584522B763C8C002FDACF /* ProductInfoViewModel.swift */; };
E52F371B2B947DC8000EBAD5 /* SearchViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = E52F371A2B947DC8000EBAD5 /* SearchViewModel.swift */; };
E52F371D2B94D239000EBAD5 /* SearchViewComponent.swift in Sources */ = {isa = PBXBuildFile; fileRef = E52F371C2B94D239000EBAD5 /* SearchViewComponent.swift */; };
E5462C662B65677B00E9FDF2 /* PromotionTag.swift in Sources */ = {isa = PBXBuildFile; fileRef = E5462C652B65677B00E9FDF2 /* PromotionTag.swift */; };
E55DD5122B91DE9500AA63C0 /* SearchListCardView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E55DD5112B91DE9500AA63C0 /* SearchListCardView.swift */; };
E55DD5182B9370D100AA63C0 /* SearchAPI in Frameworks */ = {isa = PBXBuildFile; productRef = E55DD5172B9370D100AA63C0 /* SearchAPI */; };
Expand Down Expand Up @@ -114,6 +116,8 @@
BAF2BEB22B61236100931AF0 /* Localizable.xcstrings */ = {isa = PBXFileReference; lastKnownFileType = text.json.xcstrings; path = Localizable.xcstrings; sourceTree = "<group>"; };
E50176252B6A204F0098D1BE /* ProductInfoLineGraphView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProductInfoLineGraphView.swift; sourceTree = "<group>"; };
E50584522B763C8C002FDACF /* ProductInfoViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProductInfoViewModel.swift; sourceTree = "<group>"; };
E52F371A2B947DC8000EBAD5 /* SearchViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchViewModel.swift; sourceTree = "<group>"; };
E52F371C2B94D239000EBAD5 /* SearchViewComponent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchViewComponent.swift; sourceTree = "<group>"; };
E5462C652B65677B00E9FDF2 /* PromotionTag.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PromotionTag.swift; sourceTree = "<group>"; };
E55DD5112B91DE9500AA63C0 /* SearchListCardView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchListCardView.swift; sourceTree = "<group>"; };
E55DD5152B936DD200AA63C0 /* SearchAPI */ = {isa = PBXFileReference; lastKnownFileType = folder; name = SearchAPI; path = APIService/Sources/SearchAPI; sourceTree = "<group>"; };
Expand Down Expand Up @@ -264,7 +268,9 @@
BA28F18F2B61565F0052855E /* ProductSearchScene */ = {
isa = PBXGroup;
children = (
E52F371C2B94D239000EBAD5 /* SearchViewComponent.swift */,
BA28F1902B61566E0052855E /* SearchView.swift */,
E52F371A2B947DC8000EBAD5 /* SearchViewModel.swift */,
E55DD5112B91DE9500AA63C0 /* SearchListCardView.swift */,
);
path = ProductSearchScene;
Expand Down Expand Up @@ -572,13 +578,15 @@
E57F2AA82B774CA700E12B3D /* ProductInfoDependency.swift in Sources */,
E55DD5122B91DE9500AA63C0 /* SearchListCardView.swift in Sources */,
9CE4B4712B6F0B57002DC446 /* OnboardingPage.swift in Sources */,
E52F371B2B947DC8000EBAD5 /* SearchViewModel.swift in Sources */,
BAE159DE2B663A9A002DCF94 /* HomeProductSorterView.swift in Sources */,
BAE159D82B65FA6F002DCF94 /* HomeProductDetailSelectionView.swift in Sources */,
E50176262B6A204F0098D1BE /* ProductInfoLineGraphView.swift in Sources */,
BAB5CF252B6B7C5A008B24BF /* AppRootComponent.swift in Sources */,
BAA4D9AF2B5A1795005999F8 /* SplashView.swift in Sources */,
BA8E83242B8EF83B00FE968C /* ProductConfiguration.swift in Sources */,
BAA4D9AD2B5A1795005999F8 /* PyeonHaengApp.swift in Sources */,
E52F371D2B94D239000EBAD5 /* SearchViewComponent.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
Expand Down
4 changes: 2 additions & 2 deletions PyeonHaeng-iOS/Sources/Scenes/HomeScene/View/HomeView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -48,10 +48,10 @@ struct HomeView<ViewModel>: View where ViewModel: HomeViewModelRepresentable {
.foregroundStyle(.gray900)
}
}

ToolbarItemGroup(placement: .topBarTrailing) {
NavigationLink {
SearchView()
let component = SearchViewComponent()
SearchView(viewModel: SearchViewModel(service: component.searchService))
.toolbarRole(.editor)
} label: {
Image.magnifyingglass
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,13 @@ import SwiftUI

// MARK: - SearchView

struct SearchView: View {
struct SearchView<ViewModel>: View where ViewModel: SearchViewModelRepresentable {
@StateObject private var viewModel: ViewModel

init(viewModel: @autoclosure @escaping () -> ViewModel) {
_viewModel = .init(wrappedValue: viewModel())
}

var body: some View {
ScrollView {
LazyVStack {
Expand Down Expand Up @@ -88,7 +94,3 @@ private enum Metrics {

static let removeButtonSize = 32.0
}

#Preview {
SearchView()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
//
// SearchViewComponent.swift
// PyeonHaeng-iOS
//
// Created by 김응철 on 3/4/24.
//

import Foundation
import Network
import SearchAPI
import SearchAPISupport

// MARK: - SearchDependency

protocol SearchDependency {
var searchService: SearchServiceRepresentable { get }
}

// MARK: - SearchViewComponent

struct SearchViewComponent: SearchDependency {
let searchService: SearchServiceRepresentable

init() {
let searchNetworking: Networking = {
let configuration: URLSessionConfiguration
#if DEBUG
configuration = .ephemeral
configuration.protocolClasses = [SearchURLProtocol.self]
#else
configuration = .default
#endif
let provider = NetworkProvider(session: URLSession(configuration: configuration))
return provider
}()

searchService = SearchService(network: searchNetworking)
}
}
110 changes: 110 additions & 0 deletions PyeonHaeng-iOS/Sources/Scenes/ProductSearchScene/SearchViewModel.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
//
// SearchViewModel.swift
// PyeonHaeng-iOS
//
// Created by 김응철 on 3/3/24.
//

import Entity
import Foundation
import Log
import SearchAPI

// MARK: - SearchAction

enum SearchAction {
case textChanged(String)
case loadMoreProducts
}

// MARK: - SearchState

struct SearchState {
var currentText = ""
var products = [SearchProduct]()
var offset = 0
var hasMore = false

/// ν™”λ©΄ 쀑앙에 ν‘œμ‹œλ˜λŠ” λ‘œλ”© 인디케이터
var isLoading = false
/// 리슀트 맨 ν•˜λ‹¨μ— ν‘œμ‹œλ˜λŠ” λ‘œλ”© 인디케이터
var isMoreLoading = false
}

// MARK: - SearchViewModelRepresentable

protocol SearchViewModelRepresentable: ObservableObject {
var state: SearchState { get }
func trigger(_ action: SearchAction)
}

// MARK: - SearchViewModel

final class SearchViewModel: SearchViewModelRepresentable {
/// 비동기 ν•¨μˆ˜λ₯Ό μ‹€ν–‰ν•©λ‹ˆλ‹€. λ§Œμ•½ 였λ₯˜κ°€ λ°œμƒν•˜λ©΄ Log둜 값을 확인할 수 μžˆμŠ΅λ‹ˆλ‹€.
func performAsyncAction(_ action: () async throws -> Void) async {
do {
try await action()
} catch {
Log.make(with: .viewModel)?.error("\(String(describing: error))")
}
}

private let service: SearchServiceRepresentable

@Published private(set) var state: SearchState = .init()

init(service: SearchServiceRepresentable) {
self.service = service
}

func trigger(_ action: SearchAction) {
Task {
await handle(action)
}
}

private func handle(_ action: SearchAction) async {
// 비동기 ν•¨μˆ˜λ₯Ό μ‹€ν–‰ν•©λ‹ˆλ‹€. λ§Œμ•½ 였λ₯˜κ°€ λ°œμƒν•˜λ©΄ Log둜 값을 확인할 수 μžˆμŠ΅λ‹ˆλ‹€.
func performAsyncAction(_ action: () async throws -> Void) async {
do {
try await action()
} catch {
Log.make(with: .viewModel)?.error("\(String(describing: error))")
}
}

switch action {
case let .textChanged(text):
state.currentText = text
state.offset = 0
await performAsyncAction {
try await fetchSearchList(isReplace: true)
}

case .loadMoreProducts:
await performAsyncAction {
try await fetchSearchList(isReplace: false)
}
}
}

private func fetchSearchList(isReplace: Bool) async throws {
let request = SearchProductRequest(
name: state.currentText,
order: .normal,
pageSize: 20,
offset: state.offset
)

let paginatedModel = try await service.fetchSearchList(request: request)

state.hasMore = paginatedModel.hasMore
state.offset += 1
if isReplace {
state.products = paginatedModel.results
} else {
state.products.append(contentsOf: paginatedModel.results)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -85,9 +85,3 @@ public extension Image {
static let xCircleFill: Image = .init(.xCircleFill)
static let xCircle: Image = .init(.xCircle)
}

public extension UIImage {
static var backButtonImage: UIImage? {
UIImage(resource: .chevronLeftLarge)
}
}

0 comments on commit 146eeb4

Please sign in to comment.