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

SearchView UI 구현 및 데이터 바인딩 #82

Merged
merged 9 commits into from
Mar 5, 2024
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
"name": "롯데)펩시콜라캔355ML",
"img": "https://image.woodongs.com/imgsvr/item/GD_8801056150013_007.jpg",
"price": 1900,
"store": "GS25",
"store": "7-ELEVEn",
"tag": "1+1",
"proinfo": 0,
"date": "2024-02-01T00:00:00Z",
Expand Down
10 changes: 4 additions & 6 deletions PyeonHaeng-iOS.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@
BA28F1882B6155910052855E /* HomeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA28F1872B6155910052855E /* HomeView.swift */; };
BA28F18B2B6155BD0052855E /* ProductInfoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA28F18A2B6155BD0052855E /* ProductInfoView.swift */; };
BA28F18E2B6156420052855E /* SettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA28F18D2B6156420052855E /* SettingsView.swift */; };
BA28F1912B61566E0052855E /* SearchView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA28F1902B61566E0052855E /* SearchView.swift */; };
BA28F19D2B61572A0052855E /* Pretendard-Bold.otf in Resources */ = {isa = PBXBuildFile; fileRef = BA28F1932B61572A0052855E /* Pretendard-Bold.otf */; };
BA28F19E2B61572A0052855E /* Pretendard-SemiBold.otf in Resources */ = {isa = PBXBuildFile; fileRef = BA28F1942B61572A0052855E /* Pretendard-SemiBold.otf */; };
BA28F1A02B61572A0052855E /* Pretendard-Regular.otf in Resources */ = {isa = PBXBuildFile; fileRef = BA28F1962B61572A0052855E /* Pretendard-Regular.otf */; };
Expand Down Expand Up @@ -48,6 +47,8 @@
BAE159DE2B663A9A002DCF94 /* HomeProductSorterView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BAE159DD2B663A9A002DCF94 /* HomeProductSorterView.swift */; };
BAF2BEB32B61236100931AF0 /* Localizable.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = BAF2BEB22B61236100931AF0 /* Localizable.xcstrings */; };
E50176262B6A204F0098D1BE /* ProductInfoLineGraphView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E50176252B6A204F0098D1BE /* ProductInfoLineGraphView.swift */; };
E5028D5C2B96BA9400B36C16 /* SearchView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA28F1902B61566E0052855E /* SearchView.swift */; };
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 */; };
Expand All @@ -59,7 +60,6 @@
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 */; };
E5F2EC402B637D4A00EE0838 /* ProductInfoDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E5F2EC3F2B637D4A00EE0838 /* ProductInfoDetailView.swift */; };
E5F2EC452B64926100EE0838 /* PromotionTagView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E5F2EC442B64926100EE0838 /* PromotionTagView.swift */; };
/* End PBXBuildFile section */

Expand Down Expand Up @@ -558,9 +558,9 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
E5028D5D2B96BA9F00B36C16 /* ProductInfoDetailView.swift in Sources */,
E5028D5C2B96BA9400B36C16 /* SearchView.swift in Sources */,
BAE159DA2B65FC35002DCF94 /* HomeProductListView.swift in Sources */,
E5F2EC402B637D4A00EE0838 /* ProductInfoDetailView.swift in Sources */,
E5F2EC402B637D4A00EE0838 /* ProductInfoDetailView.swift in Sources */,
BA402F7E2B85E31800E86AAD /* ConvenienceSelectBottomSheetView.swift in Sources */,
BA28F1852B6155810052855E /* OnboardingView.swift in Sources */,
BAB5CF272B6B7CF3008B24BF /* HomeViewModel.swift in Sources */,
Expand All @@ -570,8 +570,6 @@
E5462C662B65677B00E9FDF2 /* PromotionTag.swift in Sources */,
E50584532B763C8C002FDACF /* ProductInfoViewModel.swift in Sources */,
BAB720342B9325F200C2CA1A /* PromotionSelectBottomSheetView.swift in Sources */,
BA28F1912B61566E0052855E /* ProductSearchView.swift in Sources */,
BA28F1912B61566E0052855E /* SearchView.swift in Sources */,
9CE4B4732B6F0BA3002DC446 /* OnboardingViewModel.swift in Sources */,
9CE4B4752B6F78E8002DC446 /* OnboardingPageControl.swift in Sources */,
BA28F18E2B6156420052855E /* SettingsView.swift in Sources */,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,24 +5,31 @@
// Created by 김응철 on 3/1/24.
//

import Entity
import SwiftUI

// MARK: - SearchListCardView

struct SearchListCardView: View {
let product: SearchProduct

var body: some View {
HStack {
SearchImageView()
SearchDetailView()
SearchImageView(product: product)
SearchDetailView(product: product)
}
.padding(.vertical, Metrics.verticalPadding)
.padding(.horizontal, Metrics.horizontalPadding)
}
}

// MARK: - SearchImageView

private struct SearchImageView: View {
let product: SearchProduct

var body: some View {
AsyncImage(url: nil) { phase in
AsyncImage(url: product.imageURL) { phase in
if let image = phase.image {
image
.resizable()
Expand All @@ -38,49 +45,47 @@ private struct SearchImageView: View {
}
.frame(width: Metrics.totalImageSize, height: Metrics.totalImageSize)
}

enum Metrics {
static let imageSize = 70.0
static let totalImageSize = 96.0
}
}

// MARK: - SearchDetailView

private struct SearchDetailView: View {
let product: SearchProduct

var body: some View {
VStack(alignment: .leading) {
PromotionTagView(promotion: .buyOneGetOneFree)
.padding(.bottom, Metrics.promotionTagViewBottomPadding)
Text(verbatim: "펩시 제로 라임 250ml")
VStack(alignment: .leading, spacing: 8.0) {
PromotionTagView(promotion: product.promotion)
Text(product.name)
.font(.title1)
.foregroundStyle(.gray900)
.frame(maxWidth: .infinity, minHeight: 19, maxHeight: 19, alignment: .leading)
.padding(.bottom, Metrics.productTitleBottomPadding)
.frame(maxWidth: .infinity, alignment: .leading)
.padding(.bottom, 8.0)
HStack {
Text(verbatim: "1,800원")
Text("\(product.price.formatted())원")
.font(.x2)
.strikethrough()
.foregroundColor(.gray100)
.padding(.trailing, Metrics.previousPriceTrailingPadding)
Text(verbatim: "개당")
Text("개당")
.font(.c3)
.foregroundColor(.gray900)
Text(verbatim: "1,250원")
Text("\(Int(product.price / 2).formatted())원")
.font(.h4)
.foregroundColor(.gray900)
}
.frame(maxWidth: .infinity, alignment: .trailing)
}
}

enum Metrics {
static let promotionTagViewBottomPadding = 8.0
static let productTitleBottomPadding = 16.0
static let previousPriceTrailingPadding = 10.0
}
}

#Preview {
SearchListCardView()
// MARK: - Metrics

private enum Metrics {
static let productTitleBottomPadding = 16.0
static let previousPriceTrailingPadding = 10.0

static let verticalPadding = 20.0
static let horizontalPadding = 16.0

static let imageSize = 70.0
static let totalImageSize = 96.0
}
63 changes: 53 additions & 10 deletions PyeonHaeng-iOS/Sources/Scenes/ProductSearchScene/SearchView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
//

import DesignSystem
import Entity
import SwiftUI

// MARK: - SearchView
Expand All @@ -19,26 +20,42 @@ struct SearchView<ViewModel>: View where ViewModel: SearchViewModelRepresentable

var body: some View {
ScrollView {
LazyVStack {
Section {
SearchListCardView()
} header: {
SearchHeaderView()
LazyVStack(spacing: .zero) {
ForEach(Array(viewModel.state.products), id: \.key) { key, items in
Section {
ForEach(items) { item in
SearchListCardView(product: item)
}
} header: {
SearchHeaderView(
store: key,
productsCount: items.count
)
.padding(.horizontal, Metrics.horizontalPadding)
.padding(.top, Metrics.headerTopPadding)
} footer: {
Rectangle()
.foregroundStyle(.gray050)
.frame(maxWidth: .infinity, maxHeight: 10)
}
}
}
}
.scrollIndicators(.hidden)
.toolbar {
ToolbarItem(placement: .principal) {
SearchTextField()
SearchTextField<ViewModel>()
}
}
.environmentObject(viewModel)
}
}

// MARK: - SearchTextField

private struct SearchTextField: View {
private struct SearchTextField<ViewModel>: View where ViewModel: SearchViewModelRepresentable {
@State private var textInput: String = ""
@EnvironmentObject private var viewModel: ViewModel

var body: some View {
ZStack {
Expand All @@ -55,6 +72,7 @@ private struct SearchTextField: View {
lineWidth: Metrics.textFieldBorderWidth
)
}
.onSubmit { viewModel.trigger(.textChanged(textInput)) }
Button(action: {
textInput = ""
}) {
Expand All @@ -70,13 +88,35 @@ private struct SearchTextField: View {
// MARK: - SearchHeaderView

private struct SearchHeaderView: View {
let store: ConvenienceStore
let productsCount: Int

var body: some View {
HStack(spacing: 8.0) {
Image._7Eleven
Text(verbatim: "3")
convenienceImageView()
.resizable()
.scaledToFit()
.frame(height: 32.0)
Text(verbatim: "\(productsCount)")
.font(.title2)
.foregroundStyle(.green500)
}
.frame(maxWidth: .infinity, alignment: .bottomLeading)
}

private func convenienceImageView() -> Image {
switch store {
case .cu:
.cu
case .gs25:
.gs25
case ._7Eleven:
._7Eleven
case .emart24:
.emart24
case .ministop:
.ministop
}
.frame(maxWidth: .infinity, alignment: .leading)
}
}

Expand All @@ -92,5 +132,8 @@ private enum Metrics {
static let textFieldBorderWidth = 1.0
static let cornerRadius = 8.0

static let horizontalPadding = 20.0
static let headerTopPadding = 24.0

static let removeButtonSize = 32.0
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ enum SearchAction {

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

Expand Down Expand Up @@ -101,10 +101,11 @@ final class SearchViewModel: SearchViewModelRepresentable {

state.hasMore = paginatedModel.hasMore
state.offset += 1

let results = Dictionary(grouping: paginatedModel.results, by: { $0.convenienceStore })

if isReplace {
state.products = paginatedModel.results
} else {
state.products.append(contentsOf: paginatedModel.results)
state.products = results
}
}
}