Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

검색 화면 텍스트 필드 UI 수정 및 Services 프로퍼티 추가 #112

Merged
merged 13 commits into from
Mar 21, 2024
13 changes: 5 additions & 8 deletions APIService/Sources/ProductInfoAPI/ProductInfoService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,26 +12,23 @@ 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
}
}

// 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)
)
Expand All @@ -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)
)
Expand Down
8 changes: 0 additions & 8 deletions PyeonHaeng-iOS.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -62,15 +62,13 @@
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 */; };
E55DD51A2B9370D100AA63C0 /* SearchAPISupport in Frameworks */ = {isa = PBXBuildFile; productRef = E55DD5192B9370D100AA63C0 /* SearchAPISupport */; };
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 */
Expand Down Expand Up @@ -137,10 +135,8 @@
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>"; };
E57F2AA72B774CA700E12B3D /* ProductInfoDependency.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProductInfoDependency.swift; sourceTree = "<group>"; };
E5DB08EA2BA7EC7000E83910 /* OnboardingPage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingPage.swift; sourceTree = "<group>"; };
E5F2EC3F2B637D4A00EE0838 /* ProductInfoDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProductInfoDetailView.swift; sourceTree = "<group>"; };
E5F2EC442B64926100EE0838 /* PromotionTagView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PromotionTagView.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -300,7 +296,6 @@
BA28F1892B6155940052855E /* ProductInfoScene */ = {
isa = PBXGroup;
children = (
E57F2AA72B774CA700E12B3D /* ProductInfoDependency.swift */,
BA28F18A2B6155BD0052855E /* ProductInfoView.swift */,
E50584522B763C8C002FDACF /* ProductInfoViewModel.swift */,
E5F2EC3F2B637D4A00EE0838 /* ProductInfoDetailView.swift */,
Expand All @@ -322,7 +317,6 @@
BA28F18F2B61565F0052855E /* ProductSearchScene */ = {
isa = PBXGroup;
children = (
E52F371C2B94D239000EBAD5 /* SearchViewComponent.swift */,
BA28F1902B61566E0052855E /* SearchView.swift */,
E52F371A2B947DC8000EBAD5 /* SearchViewModel.swift */,
E55DD5112B91DE9500AA63C0 /* SearchListCardView.swift */,
Expand Down Expand Up @@ -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 */,
Expand All @@ -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;
};
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 @@ -14,6 +14,7 @@ struct HomeView<ViewModel>: 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())
Expand Down Expand Up @@ -51,8 +52,7 @@ struct HomeView<ViewModel>: 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
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ private struct DetailView: View {
.font(.h2)
.frame(maxHeight: 38.0)
}
.frame(maxWidth: .infinity, alignment: .trailing)
}
.foregroundStyle(Color.gray900)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,11 +48,13 @@ final class ProductInfoViewModel: ProductInfoViewModelRepresentable {
private let action: PassthroughSubject<ProductInfoAction, Never> = .init()
private let service: ProductInfoServiceRepresentable
private var subscriptions: Set<AnyCancellable> = .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)
}
Expand All @@ -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
}
}
46 changes: 28 additions & 18 deletions PyeonHaeng-iOS/Sources/Scenes/ProductSearchScene/SearchView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,27 @@ import SwiftUI
struct SearchView<ViewModel>: 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<ViewModel>(text: $text)
.environmentObject(viewModel)
}
.padding(.horizontal, 20)
ScrollView {
if viewModel.state.isLoading {
ProgressView()
Expand Down Expand Up @@ -53,11 +68,7 @@ struct SearchView<ViewModel>: 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)
}
Expand All @@ -80,12 +91,8 @@ struct SearchView<ViewModel>: View where ViewModel: SearchViewModelRepresentable
}
}
}
.toolbar(.hidden, for: .automatic)
.scrollIndicators(.hidden)
.toolbar {
ToolbarItem(placement: .principal) {
SearchTextField<ViewModel>(text: $text)
}
}
.scrollDismissesKeyboard(.immediately)
.environmentObject(viewModel)
}
Expand Down Expand Up @@ -113,14 +120,17 @@ private struct SearchTextField<ViewModel>: 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)
}
}
}
Expand Down Expand Up @@ -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

Expand Down

This file was deleted.

33 changes: 30 additions & 3 deletions PyeonHaeng-iOS/Sources/Services.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -30,7 +35,6 @@ struct Services {
let provider = NetworkProvider(session: URLSession(configuration: configuration))
return provider
}()

let noticeNetworking: Networking = {
let configuration: URLSessionConfiguration
#if DEBUG
Expand All @@ -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)
}
}