diff --git a/APIService/Sources/ProductInfoAPI/ProductInfoService.swift b/APIService/Sources/ProductInfoAPI/ProductInfoService.swift index ea28d56..15135fb 100644 --- a/APIService/Sources/ProductInfoAPI/ProductInfoService.swift +++ b/APIService/Sources/ProductInfoAPI/ProductInfoService.swift @@ -12,18 +12,15 @@ import Network // MARK: - ProductInfoServiceRepresentable public protocol ProductInfoServiceRepresentable { - func fetchProduct() async throws -> DetailProduct - func fetchProductPrice() async throws -> [DetailProduct] + func fetchProduct(productID: Int) async throws -> DetailProduct + func fetchProductPrice(productID: Int) async throws -> [DetailProduct] } // MARK: - ProductInfoService public struct ProductInfoService { private let network: Networking - private let productID: Int - - public init(productID: Int, network: Networking) { - self.productID = productID + public init(network: Networking) { self.network = network } } @@ -31,7 +28,7 @@ public struct ProductInfoService { // MARK: ProductInfoServiceRepresentable extension ProductInfoService: ProductInfoServiceRepresentable { - public func fetchProduct() async throws -> DetailProduct { + public func fetchProduct(productID: Int) async throws -> DetailProduct { let response: ProductDetailResponse = try await network.request( with: ProductInfoEndPoint.fetchProduct(productID) ) @@ -42,7 +39,7 @@ extension ProductInfoService: ProductInfoServiceRepresentable { } } - public func fetchProductPrice() async throws -> [DetailProduct] { + public func fetchProductPrice(productID: Int) async throws -> [DetailProduct] { let response: ProductDetailPricesResponse = try await network.request( with: ProductInfoEndPoint.fetchPrices(productID) ) diff --git a/PyeonHaeng-iOS.xcodeproj/project.pbxproj b/PyeonHaeng-iOS.xcodeproj/project.pbxproj index d722e16..914e404 100644 --- a/PyeonHaeng-iOS.xcodeproj/project.pbxproj +++ b/PyeonHaeng-iOS.xcodeproj/project.pbxproj @@ -62,7 +62,6 @@ E5028D5D2B96BA9F00B36C16 /* ProductInfoDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E5F2EC3F2B637D4A00EE0838 /* ProductInfoDetailView.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 */; }; @@ -70,7 +69,6 @@ E57654D92B90D78900E92F3A /* Shared in Resources */ = {isa = PBXBuildFile; fileRef = BABFEA6F2B6399C30084C0EC /* Shared */; }; E57F2AA42B7717EA00E12B3D /* ProductInfoAPI in Frameworks */ = {isa = PBXBuildFile; productRef = E57F2AA32B7717EA00E12B3D /* ProductInfoAPI */; settings = {ATTRIBUTES = (Required, ); }; }; E57F2AA62B7717EA00E12B3D /* ProductInfoAPISupport in Frameworks */ = {isa = PBXBuildFile; productRef = E57F2AA52B7717EA00E12B3D /* ProductInfoAPISupport */; }; - E57F2AA82B774CA700E12B3D /* ProductInfoDependency.swift in Sources */ = {isa = PBXBuildFile; fileRef = E57F2AA72B774CA700E12B3D /* ProductInfoDependency.swift */; }; E5DB08EB2BA7EC7000E83910 /* OnboardingPage.swift in Sources */ = {isa = PBXBuildFile; fileRef = E5DB08EA2BA7EC7000E83910 /* OnboardingPage.swift */; }; E5F2EC452B64926100EE0838 /* PromotionTagView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E5F2EC442B64926100EE0838 /* PromotionTagView.swift */; }; /* End PBXBuildFile section */ @@ -137,10 +135,8 @@ E50176252B6A204F0098D1BE /* ProductInfoLineGraphView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProductInfoLineGraphView.swift; sourceTree = ""; }; E50584522B763C8C002FDACF /* ProductInfoViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProductInfoViewModel.swift; sourceTree = ""; }; E52F371A2B947DC8000EBAD5 /* SearchViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchViewModel.swift; sourceTree = ""; }; - E52F371C2B94D239000EBAD5 /* SearchViewComponent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchViewComponent.swift; sourceTree = ""; }; E5462C652B65677B00E9FDF2 /* PromotionTag.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PromotionTag.swift; sourceTree = ""; }; E55DD5112B91DE9500AA63C0 /* SearchListCardView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchListCardView.swift; sourceTree = ""; }; - E57F2AA72B774CA700E12B3D /* ProductInfoDependency.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProductInfoDependency.swift; sourceTree = ""; }; E5DB08EA2BA7EC7000E83910 /* OnboardingPage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingPage.swift; sourceTree = ""; }; E5F2EC3F2B637D4A00EE0838 /* ProductInfoDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProductInfoDetailView.swift; sourceTree = ""; }; E5F2EC442B64926100EE0838 /* PromotionTagView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PromotionTagView.swift; sourceTree = ""; }; @@ -300,7 +296,6 @@ BA28F1892B6155940052855E /* ProductInfoScene */ = { isa = PBXGroup; children = ( - E57F2AA72B774CA700E12B3D /* ProductInfoDependency.swift */, BA28F18A2B6155BD0052855E /* ProductInfoView.swift */, E50584522B763C8C002FDACF /* ProductInfoViewModel.swift */, E5F2EC3F2B637D4A00EE0838 /* ProductInfoDetailView.swift */, @@ -322,7 +317,6 @@ BA28F18F2B61565F0052855E /* ProductSearchScene */ = { isa = PBXGroup; children = ( - E52F371C2B94D239000EBAD5 /* SearchViewComponent.swift */, BA28F1902B61566E0052855E /* SearchView.swift */, E52F371A2B947DC8000EBAD5 /* SearchViewModel.swift */, E55DD5112B91DE9500AA63C0 /* SearchListCardView.swift */, @@ -645,7 +639,6 @@ E5DB08EB2BA7EC7000E83910 /* OnboardingPage.swift in Sources */, 9CE4B4752B6F78E8002DC446 /* OnboardingPageControl.swift in Sources */, BA28F18E2B6156420052855E /* SettingsView.swift in Sources */, - E57F2AA82B774CA700E12B3D /* ProductInfoDependency.swift in Sources */, E55DD5122B91DE9500AA63C0 /* SearchListCardView.swift in Sources */, BA097F0E2B9CA82A002D3E1E /* MailSheetView.swift in Sources */, E52F371B2B947DC8000EBAD5 /* SearchViewModel.swift in Sources */, @@ -658,7 +651,6 @@ BAA4D9AF2B5A1795005999F8 /* SplashView.swift in Sources */, BA8E83242B8EF83B00FE968C /* ProductConfiguration.swift in Sources */, BAA4D9AD2B5A1795005999F8 /* PyeonHaengApp.swift in Sources */, - E52F371D2B94D239000EBAD5 /* SearchViewComponent.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/PyeonHaeng-iOS/Sources/Scenes/HomeScene/View/HomeView.swift b/PyeonHaeng-iOS/Sources/Scenes/HomeScene/View/HomeView.swift index 544afd7..d0d39d1 100644 --- a/PyeonHaeng-iOS/Sources/Scenes/HomeScene/View/HomeView.swift +++ b/PyeonHaeng-iOS/Sources/Scenes/HomeScene/View/HomeView.swift @@ -14,6 +14,7 @@ struct HomeView: View where ViewModel: HomeViewModelRepresentable { @StateObject private var viewModel: ViewModel @State private var isOnboardingSheetOpen = false @AppStorage("isFirstLaunch") private var isFirstLaunch: Bool = false + @Environment(\.injected) private var container init(viewModel: @autoclosure @escaping () -> ViewModel) { _viewModel = .init(wrappedValue: viewModel()) @@ -51,8 +52,7 @@ struct HomeView: View where ViewModel: HomeViewModelRepresentable { } ToolbarItemGroup(placement: .topBarTrailing) { NavigationLink { - let component = SearchViewComponent() - SearchView(viewModel: SearchViewModel(service: component.searchService)) + SearchView(viewModel: SearchViewModel(service: container.services.searchService)) .toolbarRole(.editor) } label: { Image.magnifyingglass diff --git a/PyeonHaeng-iOS/Sources/Scenes/ProductInfoScene/ProductInfoDependency.swift b/PyeonHaeng-iOS/Sources/Scenes/ProductInfoScene/ProductInfoDependency.swift deleted file mode 100644 index e03d79e..0000000 --- a/PyeonHaeng-iOS/Sources/Scenes/ProductInfoScene/ProductInfoDependency.swift +++ /dev/null @@ -1,40 +0,0 @@ -// -// ProductInfoDependency.swift -// PyeonHaeng-iOS -// -// Created by 김응철 on 2/10/24. -// - -import Foundation -import Network -import ProductInfoAPI -import ProductInfoAPISupport - -// MARK: - ProductInfoDependency - -protocol ProductInfoDependency { - var productInfoService: ProductInfoServiceRepresentable { get } -} - -// MARK: - ProductInfoComponent - -struct ProductInfoComponent: ProductInfoDependency { - let productInfoService: ProductInfoServiceRepresentable - - init(productID: Int) { - let productInfoNetworking: Networking = { - var configuration = URLSessionConfiguration.ephemeral - #if DEBUG - configuration = .ephemeral - configuration.protocolClasses = [ProductInfoURLProtocol.self] - #else - configuration = .default - #endif - let provider = NetworkProvider(session: URLSession(configuration: configuration)) - return provider - }() - - productInfoService = ProductInfoService(productID: productID, - network: productInfoNetworking) - } -} diff --git a/PyeonHaeng-iOS/Sources/Scenes/ProductInfoScene/ProductInfoDetailView.swift b/PyeonHaeng-iOS/Sources/Scenes/ProductInfoScene/ProductInfoDetailView.swift index 2e6bd80..c65cdbb 100644 --- a/PyeonHaeng-iOS/Sources/Scenes/ProductInfoScene/ProductInfoDetailView.swift +++ b/PyeonHaeng-iOS/Sources/Scenes/ProductInfoScene/ProductInfoDetailView.swift @@ -70,6 +70,7 @@ private struct DetailView: View { .font(.h2) .frame(maxHeight: 38.0) } + .frame(maxWidth: .infinity, alignment: .trailing) } .foregroundStyle(Color.gray900) } diff --git a/PyeonHaeng-iOS/Sources/Scenes/ProductInfoScene/ProductInfoViewModel.swift b/PyeonHaeng-iOS/Sources/Scenes/ProductInfoScene/ProductInfoViewModel.swift index 98feeb7..4eaccc0 100644 --- a/PyeonHaeng-iOS/Sources/Scenes/ProductInfoScene/ProductInfoViewModel.swift +++ b/PyeonHaeng-iOS/Sources/Scenes/ProductInfoScene/ProductInfoViewModel.swift @@ -48,11 +48,13 @@ final class ProductInfoViewModel: ProductInfoViewModelRepresentable { private let action: PassthroughSubject = .init() private let service: ProductInfoServiceRepresentable private var subscriptions: Set = .init() + private let productID: Int @Published private(set) var state: ProductInfoState = .init() - init(service: ProductInfoServiceRepresentable) { + init(service: ProductInfoServiceRepresentable, productID: Int) { self.service = service + self.productID = productID action.sink { [weak self] action in self?.render(as: action) } @@ -75,11 +77,11 @@ final class ProductInfoViewModel: ProductInfoViewModelRepresentable { private func fetchProductDetail() async throws { state.isLoading = true - try await state.product = service.fetchProduct() + try await state.product = service.fetchProduct(productID: productID) } private func fetchProductPrices() async throws { - try await state.previousProducts = service.fetchProductPrice().reversed() + try await state.previousProducts = service.fetchProductPrice(productID: productID).reversed() state.isLoading = false } } diff --git a/PyeonHaeng-iOS/Sources/Scenes/ProductSearchScene/SearchView.swift b/PyeonHaeng-iOS/Sources/Scenes/ProductSearchScene/SearchView.swift index e8b8c0e..7aa5635 100644 --- a/PyeonHaeng-iOS/Sources/Scenes/ProductSearchScene/SearchView.swift +++ b/PyeonHaeng-iOS/Sources/Scenes/ProductSearchScene/SearchView.swift @@ -14,12 +14,27 @@ import SwiftUI struct SearchView: View where ViewModel: SearchViewModelRepresentable { @StateObject private var viewModel: ViewModel @State private var text: String = "" + @Environment(\.dismiss) private var dismiss + @Environment(\.injected) private var container init(viewModel: @autoclosure @escaping () -> ViewModel) { _viewModel = .init(wrappedValue: viewModel()) } var body: some View { + HStack(spacing: 8) { + Button { + dismiss() + } label: { + Image.chevronLeftLarge + .resizable() + .scaledToFit() + .frame(width: 24) + } + SearchTextField(text: $text) + .environmentObject(viewModel) + } + .padding(.horizontal, 20) ScrollView { if viewModel.state.isLoading { ProgressView() @@ -53,11 +68,7 @@ struct SearchView: View where ViewModel: SearchViewModelRepresentable Section { ForEach(items) { item in NavigationLink { - ProductInfoView( - viewModel: ProductInfoViewModel( - service: ProductInfoComponent(productID: item.id).productInfoService - ) - ) + ProductInfoView(viewModel: ProductInfoViewModel(service: container.services.productInfoService, productID: item.id)) } label: { SearchListCardView(product: item) } @@ -80,12 +91,8 @@ struct SearchView: View where ViewModel: SearchViewModelRepresentable } } } + .toolbar(.hidden, for: .automatic) .scrollIndicators(.hidden) - .toolbar { - ToolbarItem(placement: .principal) { - SearchTextField(text: $text) - } - } .scrollDismissesKeyboard(.immediately) .environmentObject(viewModel) } @@ -113,14 +120,17 @@ private struct SearchTextField: View where ViewModel: SearchViewModel ) } .onSubmit { viewModel.trigger(.textChanged(text)) } - Button(action: { - text = "" - }) { - Image.xCircleFill - .renderingMode(.template) - .foregroundStyle(.gray200) + if !text.isEmpty { + Button { + text = "" + } label: { + Image.xCircleFill + .renderingMode(.template) + .foregroundStyle(.gray200) + } + .frame(maxWidth: .infinity, alignment: .trailing) + .padding(.trailing, 8) } - .frame(maxWidth: .infinity, alignment: .trailing) } } } @@ -166,7 +176,7 @@ private enum Metrics { static let textFieldVerticalPadding = 8.0 static let textFieldLeadingPadding = 12.0 static let textFieldTrailingPadding = 40.0 - static let textFieldHeight = 28.0 + static let textFieldHeight = 32.0 static let textFieldBorderWidth = 1.0 static let cornerRadius = 8.0 diff --git a/PyeonHaeng-iOS/Sources/Scenes/ProductSearchScene/SearchViewComponent.swift b/PyeonHaeng-iOS/Sources/Scenes/ProductSearchScene/SearchViewComponent.swift deleted file mode 100644 index 90dc63d..0000000 --- a/PyeonHaeng-iOS/Sources/Scenes/ProductSearchScene/SearchViewComponent.swift +++ /dev/null @@ -1,39 +0,0 @@ -// -// 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) - } -} diff --git a/PyeonHaeng-iOS/Sources/Services.swift b/PyeonHaeng-iOS/Sources/Services.swift index c9119cd..0a1be46 100644 --- a/PyeonHaeng-iOS/Sources/Services.swift +++ b/PyeonHaeng-iOS/Sources/Services.swift @@ -11,13 +11,18 @@ import HomeAPISupport import Network import NoticeAPI import NoticeAPISupport +import ProductInfoAPI +import ProductInfoAPISupport +import SearchAPI +import SearchAPISupport // MARK: - Services struct Services { let homeService: HomeServiceRepresentable let noticeService: NoticeServiceRepresentable - + let productInfoService: ProductInfoServiceRepresentable + let searchService: SearchServiceRepresentable init() { let homeNetworking: Networking = { let configuration: URLSessionConfiguration @@ -30,7 +35,6 @@ struct Services { let provider = NetworkProvider(session: URLSession(configuration: configuration)) return provider }() - let noticeNetworking: Networking = { let configuration: URLSessionConfiguration #if DEBUG @@ -42,8 +46,31 @@ struct Services { let provider = NetworkProvider(session: URLSession(configuration: configuration)) return provider }() - + let productInfoNetworking: Networking = { + let configuration: URLSessionConfiguration + #if DEBUG + configuration = .ephemeral + configuration.protocolClasses = [ProductInfoURLProtocol.self] + #else + configuration = .default + #endif + let provider = NetworkProvider(session: URLSession(configuration: configuration)) + return provider + }() + 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 + }() homeService = HomeService(network: homeNetworking) noticeService = NoticeService(network: noticeNetworking) + productInfoService = ProductInfoService(network: productInfoNetworking) + searchService = SearchService(network: searchNetworking) } }