From b8b862963cedaf3347e43f4a20d76b0c5cebed84 Mon Sep 17 00:00:00 2001 From: Seoyeon Park <110394722+syss220211@users.noreply.github.com> Date: Mon, 10 Jun 2024 10:52:37 +0900 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8[Feat]=20=ED=99=88/=EC=B9=B4=ED=85=8C?= =?UTF-8?q?=EA=B3=A0=EB=A6=AC/=EB=A6=AC=EB=B7=B0=20=ED=99=94=EB=A9=B4=20UI?= =?UTF-8?q?=20=EA=B5=AC=ED=98=84=20(#16)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * ✨[Feat] 출시 예정 신상품 뷰 구성 1차 홈 화면 출시 예정 신상품 Carousel content UI * ✨[Feat] 홈 화면 UI 구성 2차 - 중복 코드 처리 필요 - Carousel 오류 해결중 * ✨[Feat] 홈, 카테코리 UI 구현 3차 커밋 * ✨[Feat] 제품 상세 및 리뷰 UI 1차 커밋 * ✨[Feat] 리뷰 페이지 UI 구현 --- Projects/App/Sources/Extension/Common.swift | 15 ++ .../Categoery/CategoeryMain.swift | 32 ++++ .../Component/CateogeoryComponent.swift | 71 ++++++++ .../Presentation/Detail/NoneZeroView.swift | 52 ++++++ .../Detail/ProductDetailView.swift | 67 ++++++++ .../Detail/Review/CreateReviewView.swift | 83 +++++++++ .../Detail/Review/DetailReviewView.swift | 74 +++++++++ .../Detail/Review/ReviewListView.swift | 100 +++++++++++ .../Detail/SimiliarProductView.swift | 27 +++ .../Home/Carousel/CustomCarouselView.swift | 104 ++++++++++++ .../Carousel/ProductPreviewComponent.swift | 53 ++++++ .../Home/Component/CustomPageView.swift | 75 +++++++++ .../Home/Component/HomeSubTitleView.swift | 100 +++++++++++ .../Sources/Presentation/Home/HomeMain.swift | 71 ++++++++ .../Home/HomeSampleData/SampleData.swift | 25 +++ Projects/App/Support/Info.plist | 6 + .../Category/CategoryDetailView.swift | 38 +++++ .../Component/Category/CategoryGridView.swift | 71 ++++++++ .../Component/Common/CommonButton.swift | 58 +++++++ .../Component/Common/CustomTextEditor.swift | 157 ++++++++++++++++++ .../Component/Common/DivideRectangle.swift | 30 ++++ .../Component/Home/ProductInfoComponent.swift | 55 ++++++ .../Component/Home/ReleasedCarouselView.swift | 44 +++++ Projects/DesignSystem/Sources/Extension.swift | 9 + Projects/DesignSystem/Sources/Font/Font.swift | 18 +- Projects/SPM/Project.swift | 3 +- Tuist/Dependencies.swift | 4 +- .../SPM+TargetDependency.swift | 1 + 28 files changed, 1439 insertions(+), 4 deletions(-) create mode 100644 Projects/App/Sources/Extension/Common.swift create mode 100644 Projects/App/Sources/Presentation/Categoery/CategoeryMain.swift create mode 100644 Projects/App/Sources/Presentation/Categoery/Component/CateogeoryComponent.swift create mode 100644 Projects/App/Sources/Presentation/Detail/NoneZeroView.swift create mode 100644 Projects/App/Sources/Presentation/Detail/ProductDetailView.swift create mode 100644 Projects/App/Sources/Presentation/Detail/Review/CreateReviewView.swift create mode 100644 Projects/App/Sources/Presentation/Detail/Review/DetailReviewView.swift create mode 100644 Projects/App/Sources/Presentation/Detail/Review/ReviewListView.swift create mode 100644 Projects/App/Sources/Presentation/Detail/SimiliarProductView.swift create mode 100644 Projects/App/Sources/Presentation/Home/Carousel/CustomCarouselView.swift create mode 100644 Projects/App/Sources/Presentation/Home/Carousel/ProductPreviewComponent.swift create mode 100644 Projects/App/Sources/Presentation/Home/Component/CustomPageView.swift create mode 100644 Projects/App/Sources/Presentation/Home/Component/HomeSubTitleView.swift create mode 100644 Projects/App/Sources/Presentation/Home/HomeMain.swift create mode 100644 Projects/App/Sources/Presentation/Home/HomeSampleData/SampleData.swift create mode 100644 Projects/DesignSystem/Sources/Component/Category/CategoryDetailView.swift create mode 100644 Projects/DesignSystem/Sources/Component/Category/CategoryGridView.swift create mode 100644 Projects/DesignSystem/Sources/Component/Common/CommonButton.swift create mode 100644 Projects/DesignSystem/Sources/Component/Common/CustomTextEditor.swift create mode 100644 Projects/DesignSystem/Sources/Component/Common/DivideRectangle.swift create mode 100644 Projects/DesignSystem/Sources/Component/Home/ProductInfoComponent.swift create mode 100644 Projects/DesignSystem/Sources/Component/Home/ReleasedCarouselView.swift create mode 100644 Projects/DesignSystem/Sources/Extension.swift diff --git a/Projects/App/Sources/Extension/Common.swift b/Projects/App/Sources/Extension/Common.swift new file mode 100644 index 0000000..1b0862b --- /dev/null +++ b/Projects/App/Sources/Extension/Common.swift @@ -0,0 +1,15 @@ +// +// Common.swift +// App +// +// Created by 박서연 on 2024/06/08. +// Copyright © 2024 iOS. All rights reserved. +// + +import Foundation +import SwiftUI + +struct Size { + static let width = UIScreen.main.bounds.width + static let height = UIScreen.main.bounds.height +} diff --git a/Projects/App/Sources/Presentation/Categoery/CategoeryMain.swift b/Projects/App/Sources/Presentation/Categoery/CategoeryMain.swift new file mode 100644 index 0000000..0487ba1 --- /dev/null +++ b/Projects/App/Sources/Presentation/Categoery/CategoeryMain.swift @@ -0,0 +1,32 @@ +// +// CategoryMain.swift +// App +// +// Created by 박서연 on 2024/06/06. +// Copyright © 2024 iOS. All rights reserved. +// + +import DesignSystem +import SwiftUI + +struct CategoryMain: View { + + var body: some View { + ScrollView { + Text("카테고리") + CategoryGridView(data: ZeroDrinkSampleData.dirnkType, type: "카페 음료", + last: false, pageSpacing: 22, gridSpacing: 17) + CategoryGridView(data: ZeroDrinkSampleData.cafeType, type: "과자/아이스크림", + last: false, pageSpacing: 22, gridSpacing: 17) + CategoryGridView(data: ZeroDrinkSampleData.snackType, type: "과자/아이스크림", + last: false, pageSpacing: 22, gridSpacing: 17) + + + CategoryDetailView(data: ZeroDrinkSampleData.categoryDetail) + } + } +} + +#Preview { + CategoryMain() +} diff --git a/Projects/App/Sources/Presentation/Categoery/Component/CateogeoryComponent.swift b/Projects/App/Sources/Presentation/Categoery/Component/CateogeoryComponent.swift new file mode 100644 index 0000000..5316e8b --- /dev/null +++ b/Projects/App/Sources/Presentation/Categoery/Component/CateogeoryComponent.swift @@ -0,0 +1,71 @@ +// +// CateogeoryView.swift +// App +// +// Created by 박서연 on 2024/06/06. +// Copyright © 2024 iOS. All rights reserved. +// + +import SwiftUI + +// MARK: - 디자인시스엠으로 이동 완료 +//struct CateogeoryComponent: View { +// +// let columns: [GridItem] = Array(repeating: GridItem(.flexible()), count: 4) +// let data: [String] +// let type: String +// let last: Bool +// let pageSpacing: CGFloat +// let gridSpacing: CGFloat +// +// init(data: [String], +// type: String, +// last: Bool, +// pageSpacing: CGFloat, +// gridSpacing: CGFloat +// ) { +// self.data = data +// self.type = type +// self.last = last +// self.pageSpacing = pageSpacing +// self.gridSpacing = gridSpacing +// } +// +// var body: some View { +// VStack(alignment: .leading, spacing: 12) { +// let size = (UIScreen.main.bounds.width - (pageSpacing * 2) - (gridSpacing * 3)) / 4 +// +// Text(type) +// .applyFont(font: .heading2) +// +// LazyVGrid(columns: columns, spacing: 20) { +// ForEach(data, id: \.self) { type in +// VStack(spacing: 6) { +// Rectangle() +// .fill(Color.neutral50) +// .frame(width: size, height: size) +// .clipShape(RoundedRectangle(cornerRadius: 8)) +// Text(type) +// .applyFont(font: .body2) +// .foregroundStyle(Color.neutral900) +// } +// } +// } +// } +// .padding(.horizontal, 22) +// +// if last { +// EmptyView() +// } else { +// Rectangle() +// .frame(maxWidth: .infinity) +// .frame(height: 12) +// .foregroundStyle(Color.neutral50) +// .padding(.vertical, 30) +// } +// } +//} + +//#Preview { +// CateogeoryComponent(data: ZeroDrinkSampleData.cafeType, type: "카페", last: false, pageSpacing: 22, gridSpacing: 17) +//} diff --git a/Projects/App/Sources/Presentation/Detail/NoneZeroView.swift b/Projects/App/Sources/Presentation/Detail/NoneZeroView.swift new file mode 100644 index 0000000..1d8c401 --- /dev/null +++ b/Projects/App/Sources/Presentation/Detail/NoneZeroView.swift @@ -0,0 +1,52 @@ +// +// NoneZeroView.swift +// App +// +// Created by 박서연 on 2024/06/08. +// Copyright © 2024 iOS. All rights reserved. +// + +import SwiftUI + +struct NoneZeroView: View { + private let sample = ["영양성분1", "영양성분2", "영양성분3"] + + var body: some View { + VStack { + Text("제로가 아닌 상품을 먹었다면?") + .applyFont(font: .heading2) + .frame(maxWidth: .infinity, alignment: .leading) + Rectangle() + .fill(Color.neutral200) + .frame(width: 116, height: 116) + .padding(.vertical, 32) + .padding(.bottom, 54) + + LazyVStack(spacing: 10) { + ForEach(sample, id: \.self) { index in + HStack { + Text(index) + Spacer() + Text(index) + } + .applyFont(font: .body2) + .foregroundStyle(Color.neutral600) + .padding(.vertical, 14) + + Rectangle() + .fill(Color.neutral50) + .frame(maxWidth: .infinity) + .frame(height: 1) + .opacity(index == sample.last! ? 0 : 1) + } + .padding(.bottom, 6) + } + } + .padding(.top, 30) + .background(Color.neutral50) + } +} + +#Preview { + NoneZeroView() +} diff --git a/Projects/App/Sources/Presentation/Detail/ProductDetailView.swift b/Projects/App/Sources/Presentation/Detail/ProductDetailView.swift new file mode 100644 index 0000000..23de138 --- /dev/null +++ b/Projects/App/Sources/Presentation/Detail/ProductDetailView.swift @@ -0,0 +1,67 @@ +// +// ProductDetailView.swift +// App +// +// Created by 박서연 on 2024/06/08. +// Copyright © 2024 iOS. All rights reserved. +// + +import SwiftUI + +struct ProductDetailView: View { + private let storeSample = ["네이버 쇼핑", "쿠팡", "판매처명"] + var body: some View { + ScrollView { + Rectangle() + .fill(Color.neutral500) + .scaledToFit() + + Text("브랜드명브랜드명브랜드명") + Text("[상품명상품명상품명상품명상품명]") + + ForEach(0..<5) { index in + HStack{ + Text("영양성분명1") + Spacer() + Text("영양성분") + } + .padding(.vertical, 14) + + Rectangle() + .fill(Color.neutral100) + .frame(maxWidth: .infinity, maxHeight: 1) + .opacity(index == 4 ? 0 : 1) + } + + Text("영양 성분 모두 보기") + .padding(.init(top: 8, leading: 24, bottom: 8, trailing: 24)) + .applyFont(font: .body2) + .foregroundStyle(Color.neutral400) + .overlay { + RoundedRectangle(cornerRadius: 50) + .stroke(Color.neutral400, lineWidth: 1) + } + + Text("오프라인 판매처") + + Text("온라인 판매처") + LazyVStack(spacing: 10) { + ForEach(storeSample, id: \.self){ store in + Text(store) + .padding(.init(top: 10, leading: 16, bottom: 10, trailing: 16)) + .frame(maxWidth: .infinity, alignment: .leading) + .background(Color.neutral50) + .clipShape(RoundedRectangle(cornerRadius: 8)) + } + } + + Spacer().frame(height: 30) + + + } + } +} + +#Preview { + ProductDetailView() +} diff --git a/Projects/App/Sources/Presentation/Detail/Review/CreateReviewView.swift b/Projects/App/Sources/Presentation/Detail/Review/CreateReviewView.swift new file mode 100644 index 0000000..8ab29b3 --- /dev/null +++ b/Projects/App/Sources/Presentation/Detail/Review/CreateReviewView.swift @@ -0,0 +1,83 @@ +// +// CreateReviewView.swift +// App +// +// Created by 박서연 on 2024/06/09. +// Copyright © 2024 iOS. All rights reserved. +// + +import SwiftUI +import DesignSystem +import AutoHeightEditor + +struct SampleProduct { + let name: String + let brand: String + let content: String + + static let sampleProduct = SampleProduct(name: "파워에이드", brand: "노브랜드", content: "상품입니다상품입니다상품입니다상품입니다") +} + +struct CreateReviewView: View { + let data = SampleProduct.sampleProduct + @State var finish = false + @State var test = false + @State var text: String = "" + @State var dynamicHeight: CGFloat = 100 + + var body: some View { + ScrollView { + VStack(spacing: 30) { + Image(systemName: "heart") + .resizable() + .scaledToFit() + + VStack(spacing: 6) { + Text("[\(data.brand)]") + .applyFont(font: .body2) + .foregroundStyle(Color.neutral700) + Text(data.name) + .applyFont(font: .subtitle2) + .foregroundStyle(Color.neutral900) + .lineLimit(1) + } + .padding(.horizontal, 22) + + DivideRectangle(height: 1, color: Color.neutral100) + + VStack(spacing: 10){ + Text("상품은 어떠셨나요?") + .applyFont(font: .subtitle1) + .frame(maxWidth: .infinity, alignment: .center) + HStack(spacing: 2){ + ForEach(0..<5) { _ in + Image(systemName: "star") + .font(.system(size: 36)) + .foregroundStyle(Color.neutral200) + } + } + } + + DynamicHeightTextEditor(text: $text, dynamicHeight: $dynamicHeight, + initialHeight: 100, radius: 10, + font: .body2, backgroundColor: Color.neutral50, + fontColor: Color.neutral700, + placeholder: "리뷰를 남겨주세요", + placeholderColor: Color.neutral500) .padding(.horizontal, 22) + + + CommonButton(title: "작성 완료", font: .subtitle1) + .enable( + // TODO: - Button 조건 수정 + !text.isEmpty + ) + .padding(.horizontal, 22) + .padding(.top, -2) + } + } + } +} + +#Preview { + CreateReviewView() +} diff --git a/Projects/App/Sources/Presentation/Detail/Review/DetailReviewView.swift b/Projects/App/Sources/Presentation/Detail/Review/DetailReviewView.swift new file mode 100644 index 0000000..2e158bf --- /dev/null +++ b/Projects/App/Sources/Presentation/Detail/Review/DetailReviewView.swift @@ -0,0 +1,74 @@ +// +// DetailReviewView.swift +// App +// +// Created by 박서연 on 2024/06/08. +// Copyright © 2024 iOS. All rights reserved. +// + +import SwiftUI +import DesignSystem + +struct DetailReviewView: View { + let data = [2 : ["content1", "content2", "content3"]] + + var body: some View { + VStack { + Text("리뷰 2") + .applyFont(font: .heading2) + .frame(maxWidth: .infinity, alignment: .leading) + .padding(.bottom, 12) + + ReviewScoreComponent(background: Color.neutral50, + heightPadding: 18, radius: 0) + .padding(.bottom, 20) + + // TODO: - Carousel, component수정 + VStack(spacing: 16) { + HStack{ + Image(systemName: "star") + .padding(.trailing, 4) + Text("4.7") + .foregroundStyle(Color.neutral700) + Spacer() + Text("2024.06.05") + .foregroundStyle(Color.neutral400) + } + + Text("리뷰입니다리뷰는두줄까지노출합니다리뷰는두줄까지노출합니다리뷰는두줄까지노출합니다리뷰...") + .lineLimit(2) + .foregroundStyle(Color.neutral700) + .applyFont(font: .body2) + } + .padding(14) + .overlay { + RoundedRectangle(cornerRadius: 10) + .stroke(Color.neutral100, lineWidth: 1) + } + + CommonButton(title: "리뷰 작성", font: .subtitle1) + } + } +} + +struct ReviewScoreComponent: View { + let background: Color + let heightPadding: CGFloat + let radius: CGFloat + + var body: some View { + VStack(spacing:2) { + Text("4.3") + .applyFont(font: .heading2) + Image(systemName: "star") + } + .padding(.vertical, heightPadding) + .frame(maxWidth: .infinity) + .background(background) + .clipShape(RoundedRectangle(cornerRadius: 8)) + } +} + +#Preview { + DetailReviewView() +} diff --git a/Projects/App/Sources/Presentation/Detail/Review/ReviewListView.swift b/Projects/App/Sources/Presentation/Detail/Review/ReviewListView.swift new file mode 100644 index 0000000..b914cf1 --- /dev/null +++ b/Projects/App/Sources/Presentation/Detail/Review/ReviewListView.swift @@ -0,0 +1,100 @@ +// +// ReviewListView.swift +// App +// +// Created by 박서연 on 2024/06/08. +// Copyright © 2024 iOS. All rights reserved. +// + +import SwiftUI +import DesignSystem + +struct ReviewListView: View { + @State var plus: Bool = false + let data = ReviewSampleData.reivewSample + + var body: some View { + ScrollView { + ReviewScoreComponent(background: Color.neutral50, + heightPadding: 38, radius: 8) + .padding(.init(top: 10, leading: 0, bottom: 30, trailing: 0)) + + LazyVStack { + ForEach(data, id: \.id) { index in + VStack(alignment: .leading, spacing: 4) { + HStack { + Text(index.user) + .applyFont(font: .subtitle2) + .foregroundStyle(Color.neutral700) + Spacer() + Text(index.date) + .foregroundStyle(Color.neutral400) + .applyFont(font: .label1) + } + + HStack(spacing: 4) { + Image(systemName: index.star) + Text(index.score) + .applyFont(font: .label1) + } + + Text(index.content) + .foregroundStyle(Color.neutral700) + .applyFont(font: .body2) + .lineLimit(plus ? nil : 3) + .padding(.vertical, 12) + + Text("더보기") + .foregroundStyle(Color.neutral600) + .applyFont(font: .body3) + .onTapGesture { + print("더보기") + plus.toggle() + } + .frame(maxWidth: .infinity, alignment: .trailing) + + Text("신고") + .applyFont(font: .body3) + .foregroundStyle(Color.neutral300) + .onTapGesture { + print("더보기") + } + } + DivideRectangle(height: 1, color: Color.neutral50) + .opacity(index.id == data.last?.id ? 0 : 1) + } + } + .padding(.horizontal, 22) + } + } +} + +#Preview { + ReviewListView() +} + +struct ReviewSampleData { + let id = UUID().uuidString + let user: String + let star: String = "star.fill" + let score: String = "4.7" + let date: String = "2023.07.25" + let content: String = "리뷰입니다리뷰는세줄까지노출합니다리뷰는세줄까지노출합니다리뷰는세줄까지노출합니다리뷰는세줄까지노출합니다리뷰는세줄까지노출합니다리뷰는세줄까지노출합니다리뷰는리뷰입니다리뷰는세줄까지노출합니다리뷰는세줄까지노출합니다리뷰는세줄까지노출합니다리뷰는세줄까지노출합니다리뷰는세줄까지노출합니다리뷰는세줄까지노출합니다리뷰는" + + static let reivewSample = [ + ReviewSampleData(user: "user1"), + ReviewSampleData(user: "user2"), + ReviewSampleData(user: "user3"), + ReviewSampleData(user: "user4"), + ReviewSampleData(user: "user5"), + ReviewSampleData(user: "user6"), + ReviewSampleData(user: "user7"), + ReviewSampleData(user: "user8"), + ReviewSampleData(user: "user9"), + ReviewSampleData(user: "user10"), + ReviewSampleData(user: "user11"), + ReviewSampleData(user: "user12"), + ReviewSampleData(user: "user13"), + ReviewSampleData(user: "user14") + ] +} diff --git a/Projects/App/Sources/Presentation/Detail/SimiliarProductView.swift b/Projects/App/Sources/Presentation/Detail/SimiliarProductView.swift new file mode 100644 index 0000000..a4609e4 --- /dev/null +++ b/Projects/App/Sources/Presentation/Detail/SimiliarProductView.swift @@ -0,0 +1,27 @@ +// +// SimiliarProductView.swift +// App +// +// Created by 박서연 on 2024/06/08. +// Copyright © 2024 iOS. All rights reserved. +// + +import SwiftUI +import DesignSystem + +struct SimiliarProductView: View { + var body: some View { + VStack { + Text("이 상품과 비슷한 상품이에요") + .applyFont(font: .heading2) + .frame(maxWidth: .infinity, alignment: .leading) + + // TODO: - component 넣기 + CommonButton(title: "작성완료", font: .subtitle2) + } + } +} + +#Preview { + SimiliarProductView() +} diff --git a/Projects/App/Sources/Presentation/Home/Carousel/CustomCarouselView.swift b/Projects/App/Sources/Presentation/Home/Carousel/CustomCarouselView.swift new file mode 100644 index 0000000..d9fee97 --- /dev/null +++ b/Projects/App/Sources/Presentation/Home/Carousel/CustomCarouselView.swift @@ -0,0 +1,104 @@ +// +// CustomCarouselView.swift +// App +// +// Created by 박서연 on 2024/06/05. +// Copyright © 2024 iOS. All rights reserved. +// + +import SwiftUI + +struct CustomCarouselView: View { + + let pageCount: Int + let pageSpacing: CGFloat + let edgeSpacing: CGFloat + let cardSpacing: CGFloat + let content: (Int) -> Content + + @GestureState var offset: CGFloat = 0 + @State var currentIndex: Int = 0 + + init(pageCount: Int, + pageSpacing: CGFloat, + edgeSpacing: CGFloat, + cardSpacing: CGFloat, + @ViewBuilder content: @escaping (Int) -> Content) { + self.pageCount = pageCount + self.pageSpacing = pageSpacing + self.edgeSpacing = edgeSpacing + self.cardSpacing = cardSpacing + self.content = content + } + + var body: some View { + VStack { + GeometryReader { geometry in + let width = geometry.size.width + let contentWidth = width - (pageSpacing * 2) + + HStack(spacing: cardSpacing) { + ForEach(0..: View { + let pageCount: Int + let content: Content + + @State private var currentPage = 0 + + init(pageCount: Int, @ViewBuilder content: () -> Content) { + self.pageCount = pageCount + self.content = content() + } + + var body: some View { + VStack { + TabView(selection: $currentPage) { + content + .frame(maxWidth: .infinity, maxHeight: .infinity) + .tag(0...pageCount-1) + } + .tabViewStyle(PageTabViewStyle(indexDisplayMode: .never)) + .overlay(alignment: .bottom) { + PageControl(numberOfPages: pageCount, currentPage: $currentPage) + .padding(.bottom, 20) + } + } + } +} + +struct PageControl: View { + let numberOfPages: Int + @Binding var currentPage: Int + + var body: some View { + HStack(spacing: 6) { + ForEach(0.. + UIAppFonts + + Pretendard-Bold.otf + Pretendard-SemiBold.otf + Pretendard-Medium.otf + LSApplicationCategoryType CFBundleDevelopmentRegion diff --git a/Projects/DesignSystem/Sources/Component/Category/CategoryDetailView.swift b/Projects/DesignSystem/Sources/Component/Category/CategoryDetailView.swift new file mode 100644 index 0000000..a1bf787 --- /dev/null +++ b/Projects/DesignSystem/Sources/Component/Category/CategoryDetailView.swift @@ -0,0 +1,38 @@ +// +// CategoryDetailView.swift +// DesignSystem +// +// Created by 박서연 on 2024/06/07. +// Copyright © 2024 iOS. All rights reserved. +// + +import SwiftUI + +public struct CategoryDetailView: View { + public let rows = Array(repeating: GridItem(.flexible()), count: 1) + public let data: [String] + + public init(data: [String]) { + self.data = data + } + + public var body: some View { + ScrollView(.horizontal) { + LazyHGrid(rows: rows, spacing: 6) { + ForEach(data, id: \.self) { type in + HStack(spacing: 2) { + Text(type) + Image(systemName: "chevron.down") + } + .padding(.init(top: 6, leading: 12, bottom: 6, trailing: 12)) + .background(Color.neutral50) + .clipShape(RoundedRectangle(cornerRadius: 6)) + } + } + } + } +} + +#Preview { + CategoryDetailView(data: ["태그1", "태그2", "태그3"]) +} diff --git a/Projects/DesignSystem/Sources/Component/Category/CategoryGridView.swift b/Projects/DesignSystem/Sources/Component/Category/CategoryGridView.swift new file mode 100644 index 0000000..92b7b50 --- /dev/null +++ b/Projects/DesignSystem/Sources/Component/Category/CategoryGridView.swift @@ -0,0 +1,71 @@ +// +// CategoryGridView.swift +// DesignSystem +// +// Created by 박서연 on 2024/06/07. +// Copyright © 2024 iOS. All rights reserved. +// + +import SwiftUI + +public struct CategoryGridView: View { + + public let columns: [GridItem] = Array(repeating: GridItem(.flexible()), count: 4) + public let data: [String] + public let type: String + public let last: Bool + public let pageSpacing: CGFloat + public let gridSpacing: CGFloat + + public init(data: [String], + type: String, + last: Bool, + pageSpacing: CGFloat, + gridSpacing: CGFloat + ) { + self.data = data + self.type = type + self.last = last + self.pageSpacing = pageSpacing + self.gridSpacing = gridSpacing + } + + public var body: some View { + VStack(alignment: .leading, spacing: 12) { + let size = (UIScreen.main.bounds.width - (pageSpacing * 2) - (gridSpacing * 3)) / 4 + + Text(type) + .applyFont(font: .heading2) + + LazyVGrid(columns: columns, spacing: 20) { + ForEach(data, id: \.self) { type in + VStack(spacing: 6) { + Rectangle() + .fill(Color.neutral50) + .frame(width: size, height: size) + .clipShape(RoundedRectangle(cornerRadius: 8)) + Text(type) + .foregroundStyle(Color.neutral900) + .applyFont(font: .body2) + + } + } + } + } + .padding(.horizontal, 22) + + if last { + EmptyView() + } else { + Rectangle() + .foregroundStyle(Color.neutral50) + .frame(maxWidth: .infinity) + .frame(height: 12) + .padding(.vertical, 30) + } + } +} + +#Preview { + CategoryGridView(data: ["11", "22"], type: "카페", last: false, pageSpacing: 22, gridSpacing: 17) +} diff --git a/Projects/DesignSystem/Sources/Component/Common/CommonButton.swift b/Projects/DesignSystem/Sources/Component/Common/CommonButton.swift new file mode 100644 index 0000000..88fdc5b --- /dev/null +++ b/Projects/DesignSystem/Sources/Component/Common/CommonButton.swift @@ -0,0 +1,58 @@ +// +// CommonButton.swift +// DesignSystem +// +// Created by 박서연 on 2024/06/08. +// Copyright © 2024 iOS. All rights reserved. +// + +import SwiftUI + +public struct CommonButton: View { + public var title: String + public var isEnable: Bool = true + public var height: CGFloat = 52 + public var action: (() -> Void)? + public var font: ZSFont + + public init(title: String, font: ZSFont) { + self.title = title + self.font = font + } + + public var body: some View { + Text(title) + .applyFont(font: font) + .foregroundStyle(isEnable ? Color.white : Color.neutral300) + .frame(height: height) + .frame(maxWidth: .infinity) + .background( + RoundedRectangle(cornerRadius: 10) + .foregroundStyle(isEnable ? Color.primaryFF6972 : Color.neutral100) + ) + .onTapGesture { + action?() + } + } +} + +public extension CommonButton { + func enable(_ isEnable: Bool) -> Self { + var copy = self + copy.isEnable = isEnable + return copy + } + + func height(_ height: CGFloat) -> Self { + var copy = self + copy.height = height + return copy + } + + func tap(action: @escaping (() -> Void)) -> Self { + var copy = self + copy.action = action + return copy + } +} + diff --git a/Projects/DesignSystem/Sources/Component/Common/CustomTextEditor.swift b/Projects/DesignSystem/Sources/Component/Common/CustomTextEditor.swift new file mode 100644 index 0000000..0288c01 --- /dev/null +++ b/Projects/DesignSystem/Sources/Component/Common/CustomTextEditor.swift @@ -0,0 +1,157 @@ +// +// CustomTextEditor.swift +// DesignSystem +// +// Created by 박서연 on 2024/06/09. +// Copyright © 2024 iOS. All rights reserved. +// + +import SwiftUI +import UIKit + +public struct DynamicHeightTextEditor: View { + @Binding public var text: String + @Binding public var dynamicHeight: CGFloat + public let initialHeight: CGFloat + public let radius: CGFloat + public let font: ZSFont + public let backgroundColor: Color + public let fontColor: Color + public let placeholder: String + public let placeholderColor: Color + + public init(text: Binding, + dynamicHeight: Binding, + initialHeight: CGFloat, + radius: CGFloat, + font: ZSFont, + backgroundColor: Color, + fontColor: Color, + placeholder: String, + placeholderColor: Color) { + self._text = text + self._dynamicHeight = dynamicHeight + self.initialHeight = initialHeight + self.radius = radius + self.font = font + self.backgroundColor = backgroundColor + self.fontColor = fontColor + self.placeholder = placeholder + self.placeholderColor = placeholderColor + } + + public var body: some View { + VStack(spacing: 6) { + UITextViewWrapper(text: $text, + dynamicHeight: $dynamicHeight, + font: .body2, + backgroundColor: UIColor(Color.neutral50), + fontColor: UIColor(Color.neutral700), + placeholder: "제품에 대한 의견을 자유롭게 남겨주세요.", + placeholderColor: UIColor(Color.neutral500)) + .frame(minHeight: initialHeight, maxHeight: dynamicHeight) + .cornerRadius(8) + .clipShape(RoundedRectangle(cornerRadius: 8)) + + Text("\(text.count)/1000") + .frame(maxWidth: .infinity, alignment: .trailing) + .applyFont(font: .label1) + .foregroundStyle(Color.neutral400) + .onChange(of: text) { newValue in + if newValue.count > 1000 { + text = String(newValue.prefix(100)) + } + } + } + } +} + +fileprivate struct UITextViewWrapper: UIViewRepresentable { + @Binding fileprivate var text: String + @Binding fileprivate var dynamicHeight: CGFloat + fileprivate let font: ZSFont + fileprivate let backgroundColor: UIColor + fileprivate let fontColor: UIColor + fileprivate let placeholder: String + fileprivate let placeholderColor: UIColor + + fileprivate init(text: Binding, + dynamicHeight: Binding, + font: ZSFont, + backgroundColor: UIColor, + fontColor: UIColor, + placeholder: String, + placeholderColor: UIColor) { + self._text = text + self._dynamicHeight = dynamicHeight + self.font = font + self.backgroundColor = backgroundColor + self.fontColor = fontColor + self.placeholder = placeholder + self.placeholderColor = placeholderColor + } + + fileprivate func makeUIView(context: Context) -> UITextView { + let textView = UITextView() + textView.isScrollEnabled = true + textView.font = font.toUIFont + textView.delegate = context.coordinator + textView.backgroundColor = backgroundColor + textView.textColor = fontColor + textView.textContainerInset = UIEdgeInsets(top: 10, left: 12, bottom: 10, right: 12) + + let placeholderLabel = UILabel() + placeholderLabel.text = placeholder + placeholderLabel.font = font.toUIFont + placeholderLabel.textColor = placeholderColor + placeholderLabel.translatesAutoresizingMaskIntoConstraints = false + textView.addSubview(placeholderLabel) + textView.setValue(placeholderLabel, forKey: "_placeholderLabel") + + NSLayoutConstraint.activate([ + placeholderLabel.topAnchor.constraint(equalTo: textView.topAnchor, constant: 10), + placeholderLabel.leadingAnchor.constraint(equalTo: textView.leadingAnchor, constant: 12), + placeholderLabel.trailingAnchor.constraint(equalTo: textView.trailingAnchor, constant: -12) + ]) + updatePlaceholderVisibility(textView: textView) + + return textView + } + + fileprivate func updateUIView(_ uiView: UITextView, context: Context) { + uiView.text = text + recalculateHeight(view: uiView) + } + + fileprivate func recalculateHeight(view: UITextView) { + let size = view.sizeThatFits(CGSize(width: view.frame.width, height: CGFloat.greatestFiniteMagnitude)) + if dynamicHeight != size.height { + DispatchQueue.main.async { + self.dynamicHeight = size.height + } + } + } + + fileprivate func makeCoordinator() -> Coordinator { + Coordinator(self) + } + + fileprivate class Coordinator: NSObject, UITextViewDelegate { + var parent: UITextViewWrapper + + init(_ parent: UITextViewWrapper) { + self.parent = parent + } + + fileprivate func textViewDidChange(_ textView: UITextView) { + parent.text = textView.text + parent.recalculateHeight(view: textView) + } + } + + fileprivate func updatePlaceholderVisibility(textView: UITextView) { + if let placeholderLabel = textView.value(forKey: "_placeholderLabel") as? UILabel { + placeholderLabel.isHidden = !text.isEmpty + } + } +} diff --git a/Projects/DesignSystem/Sources/Component/Common/DivideRectangle.swift b/Projects/DesignSystem/Sources/Component/Common/DivideRectangle.swift new file mode 100644 index 0000000..d4bdd78 --- /dev/null +++ b/Projects/DesignSystem/Sources/Component/Common/DivideRectangle.swift @@ -0,0 +1,30 @@ +// +// DivideRectangle.swift +// DesignSystem +// +// Created by 박서연 on 2024/06/09. +// Copyright © 2024 iOS. All rights reserved. +// + +import SwiftUI + +public struct DivideRectangle: View { + public let height: CGFloat + public let color: Color + + public init(height: CGFloat, color: Color) { + self.height = height + self.color = color + } + + public var body: some View { + Rectangle() + .fill(color) + .frame(maxWidth: .infinity) + .frame(height: height) + } +} + +#Preview { + DivideRectangle(height: 1, color: Color.neutral50) +} diff --git a/Projects/DesignSystem/Sources/Component/Home/ProductInfoComponent.swift b/Projects/DesignSystem/Sources/Component/Home/ProductInfoComponent.swift new file mode 100644 index 0000000..6c7c459 --- /dev/null +++ b/Projects/DesignSystem/Sources/Component/Home/ProductInfoComponent.swift @@ -0,0 +1,55 @@ +// +// ProductInfoComponent.swift +// DesignSystem +// +// Created by 박서연 on 2024/06/07. +// Copyright © 2024 iOS. All rights reserved. +// + +import SwiftUI + +// 출시 예정 신상품 Carousel 내부뷰를 위한 컴포넌트 +public struct ProductInfoComponent: View { + public enum ScrollData { + case category + case store + } + + public let data: [String] + public let type: ScrollData + + public init(data: [String], type: ScrollData) { + self.data = data + self.type = type + } + + public var body: some View { + switch type { + case .category: + HStack(spacing: 8) { + ForEach(data, id: \.self) { index in + Text(index) + .foregroundStyle(Color.neutral500) + } + } + .frame(maxWidth: .infinity, alignment: .center) + case .store: + HStack(spacing: 6) { + ForEach(data, id: \.self) { index in + Text(index) + .padding(.init(top: 4, leading: 16, bottom: 4, trailing: 16)) + .background(Color.neutral50) + .clipShape(RoundedRectangle(cornerRadius: 4)) + .applyFont(font: .label1) + .foregroundStyle(Color.neutral700) + + } + } + } + + } +} + +#Preview { + ProductInfoComponent(data: ["11", "22"], type: .category) +} diff --git a/Projects/DesignSystem/Sources/Component/Home/ReleasedCarouselView.swift b/Projects/DesignSystem/Sources/Component/Home/ReleasedCarouselView.swift new file mode 100644 index 0000000..289a209 --- /dev/null +++ b/Projects/DesignSystem/Sources/Component/Home/ReleasedCarouselView.swift @@ -0,0 +1,44 @@ +// +// ReleasedCarouselView.swift +// DesignSystem +// +// Created by 박서연 on 2024/06/07. +// Copyright © 2024 iOS. All rights reserved. +// + +import SwiftUI + +public struct ReleasedCarouselView: View { + public let category = ["#생수/음료", "#탄산음료"] + public let store = ["쿠팡", "이마트", "판매처"] + + public init() {} + + public var body: some View { + VStack(spacing: 14) { + Rectangle() + .fill(Color.neutral200) + .frame(height: 216) + .frame(maxWidth: .infinity) + + VStack { + ProductInfoComponent(data: category, type: .category) + .padding(.bottom, 6) + + Text("상품명상품명상품명") + .applyFont(font: .subtitle1) + .lineLimit(1) + .padding(.bottom, 15) + + ProductInfoComponent(data: store, type: .store) + .padding(.bottom, 16) + } + .frame(alignment: .center) + } + .clipShape(RoundedRectangle(cornerRadius: 12)) + } +} + +#Preview { + ReleasedCarouselView() +} diff --git a/Projects/DesignSystem/Sources/Extension.swift b/Projects/DesignSystem/Sources/Extension.swift new file mode 100644 index 0000000..33078b1 --- /dev/null +++ b/Projects/DesignSystem/Sources/Extension.swift @@ -0,0 +1,9 @@ +// +// Extension.swift +// DesignSystem +// +// Created by 박서연 on 2024/06/06. +// Copyright © 2024 iOS. All rights reserved. +// + +import SwiftUI diff --git a/Projects/DesignSystem/Sources/Font/Font.swift b/Projects/DesignSystem/Sources/Font/Font.swift index c486037..cc2a102 100644 --- a/Projects/DesignSystem/Sources/Font/Font.swift +++ b/Projects/DesignSystem/Sources/Font/Font.swift @@ -5,7 +5,7 @@ // Created by 박서연 on 2024/05/12. // Copyright © 2024 iOS. All rights reserved. // - +import UIKit import SwiftUI public enum ZSFont { @@ -15,6 +15,7 @@ public enum ZSFont { case subtitle2 case body1 case body2 + case body3 case label1 case label2 case caption @@ -36,6 +37,8 @@ extension ZSFont { return DesignSystemFontFamily.Pretendard.medium.name case .body2: return DesignSystemFontFamily.Pretendard.medium.name + case .body3: + return DesignSystemFontFamily.Pretendard.medium.name case .label1: return DesignSystemFontFamily.Pretendard.medium.name case .label2: @@ -59,8 +62,10 @@ extension ZSFont { return 15 case .body2: return 14 - case .label1: + case .body3: return 13 + case .label1: + return 12 case .label2: return 11 case .caption: @@ -82,6 +87,8 @@ extension ZSFont { return CGFloat(ZSFont.body1.size * 0.140) case .body2: return CGFloat(ZSFont.body2.size * 0.140) + case .body3: + return CGFloat(ZSFont.body2.size * 0.140) case .label1: return CGFloat(ZSFont.label1.size * 0.140) case .label2: @@ -105,6 +112,8 @@ extension ZSFont { return 0 case .body2: return 0 + case .body3: + return 0 case .label1: return 0 case .label2: @@ -113,6 +122,11 @@ extension ZSFont { return 0 } } + + public var toUIFont: UIFont { + return UIFont(name: self.name, size: self.size) ?? UIFont.systemFont(ofSize: self.size) + } + } public struct FontModifier: ViewModifier { diff --git a/Projects/SPM/Project.swift b/Projects/SPM/Project.swift index fa1f99b..17758f3 100644 --- a/Projects/SPM/Project.swift +++ b/Projects/SPM/Project.swift @@ -20,7 +20,8 @@ let spmTarget = Target.makeTarget( dependencies: [.SPM.Alamofire, .SPM.Kakao, .SPM.KingFisher, - .SPM.Lottie], + .SPM.Lottie, + .SPM.AutoHeight], infoPlistPath: "Support/Info.plist", // scripts: [.swiftLintPath], // -> lint 적용o scripts: [], // -> lint 적용x diff --git a/Tuist/Dependencies.swift b/Tuist/Dependencies.swift index 2467c47..7a98b09 100644 --- a/Tuist/Dependencies.swift +++ b/Tuist/Dependencies.swift @@ -20,7 +20,9 @@ let dependencies = Dependencies( .remote(url: "https://github.com/kakao/kakao-ios-sdk", requirement: .upToNextMajor(from: "2.0.0")), .remote(url: "https://github.com/airbnb/lottie-ios", - requirement: .upToNextMajor(from: "4.4.3")) + requirement: .upToNextMajor(from: "4.4.3")), + .remote(url: "https://github.com/wontaeyoung/AutoHeightEditor", + requirement: .upToNextMajor(from: "1.0.0")) ] ), diff --git a/Tuist/ProjectDescriptionHelpers/SPM+TargetDependency.swift b/Tuist/ProjectDescriptionHelpers/SPM+TargetDependency.swift index 3935ff3..8679e07 100644 --- a/Tuist/ProjectDescriptionHelpers/SPM+TargetDependency.swift +++ b/Tuist/ProjectDescriptionHelpers/SPM+TargetDependency.swift @@ -16,4 +16,5 @@ public extension TargetDependency.SPM { static let Alamofire = TargetDependency.external(name: "Alamofire") static let Kakao = TargetDependency.external(name: "KakaoSDK") static let Lottie = TargetDependency.external(name: "Lottie") + static let AutoHeight = TargetDependency.external(name: "AutoHeightEditor") }