diff --git a/Projects/Core/DesignSystem/Sources/BasicModal.swift b/Projects/Core/DesignSystem/Sources/BasicModal.swift new file mode 100644 index 0000000..f4c652e --- /dev/null +++ b/Projects/Core/DesignSystem/Sources/BasicModal.swift @@ -0,0 +1,75 @@ +// +// BasicModal.swift +// DesignSystem +// +// Created by 리나 on 2024/06/30. +// + +import SwiftUI +import ResourceKit + +// Thanks to 균 +public struct BasicModal: View { + @Binding var isPresented: Bool + private let content: () -> Content + private var opacity: Double + + public init( + isPresented: Binding, + opacity: Double, + content: @escaping () -> Content + ) { + self._isPresented = isPresented + self.opacity = opacity + self.content = content + } + + public var body: some View { + GeometryReader { geometry in + ZStack { + if isPresented { + Color.black.opacity(opacity) + .onTapGesture { + self.isPresented.toggle() + } + .transition(.opacity) + + content() + .padding(.horizontal, 40) + } + } + .edgesIgnoringSafeArea(.all) + } + } +} + +extension View { + public func basicModal( + isPresented: Binding, + opacity: Double, + @ViewBuilder content: @escaping () -> Content + ) -> some View { + modifier( + BasicModalModifier( + content: { + BasicModal( + isPresented: isPresented, + opacity: opacity, + content: content + ) + } + ) + ) + } +} + +fileprivate struct BasicModalModifier: ViewModifier where SheetContent: View { + var content: () -> BasicModal + + func body(content: Content) -> some View { + ZStack { + content + self.content() + } + } +} diff --git a/Projects/Core/DesignSystem/Sources/CategoryTagView.swift b/Projects/Core/DesignSystem/Sources/CategoryTagView.swift new file mode 100644 index 0000000..3f75de6 --- /dev/null +++ b/Projects/Core/DesignSystem/Sources/CategoryTagView.swift @@ -0,0 +1,87 @@ +// +// CategoryTagView.swift +// DesignSystem +// +// Created by 리나 on 2024/06/29. +// + +import SwiftUI +import ResourceKit + +// thanks to NamS +public struct CategoryTagView: View { + @State public var categories: [String] + + public init(categories: [String]) { + self.categories = categories + } + + public var body: some View { + ScrollView { + CategoryTagLayout(verticalSpacing: 8, horizontalSpacing: 8) { + ForEach(categories, id: \.self) { category in + Text(category) + .font(Font.Body.Medium.medium) + .foregroundColor(Color.Text.primary) + .padding(.horizontal, 16) + .padding(.vertical, 9.5) + .background( + Capsule().foregroundStyle(Color.Background.assistive) + ) + } + } + } + .onAppear { + DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { + let cacheValue = categories + categories = [] + categories = cacheValue + } + } + } +} + +struct CategoryTagLayout: Layout { + var verticalSpacing: CGFloat = 0 + var horizontalSpacing: CGFloat = 0 + + func sizeThatFits(proposal: ProposedViewSize, subviews: Subviews, cache: inout Cache) -> CGSize { + CGSize(width: proposal.width ?? 0, height: cache.height) + } + + func placeSubviews(in bounds: CGRect, proposal: ProposedViewSize, subviews: Subviews, cache: inout Cache) { + var sumX: CGFloat = bounds.minX + var sumY: CGFloat = bounds.minY + + for index in subviews.indices { + let view = subviews[index] + let viewSize = view.sizeThatFits(.unspecified) + guard let proposalWidth = proposal.width else { continue } + + if (sumX + viewSize.width > proposalWidth) { + sumX = bounds.minX + sumY += viewSize.height + sumY += verticalSpacing + } + + let point = CGPoint(x: sumX, y: sumY) + view.place(at: point, anchor: .topLeading, proposal: proposal) + sumX += viewSize.width + sumX += horizontalSpacing + } + + if let firstViewSize = subviews.first?.sizeThatFits(.unspecified) { + cache.height = sumY + firstViewSize.height + } + } + + struct Cache { + var height: CGFloat + } + + func makeCache(subviews: Subviews) -> Cache { + return Cache(height: 0) + } + + func updateCache(_ cache: inout Cache, subviews: Subviews) { } +} diff --git a/Projects/Core/DesignSystem/Sources/HorizontalMimScrollView.swift b/Projects/Core/DesignSystem/Sources/HorizontalMimScrollView.swift new file mode 100644 index 0000000..925d1f9 --- /dev/null +++ b/Projects/Core/DesignSystem/Sources/HorizontalMimScrollView.swift @@ -0,0 +1,38 @@ +// +// HorizontalMimScrollView.swift +// DesignSystem +// +// Created by 리나 on 2024/06/29. +// + +import SwiftUI +import ResourceKit + +public protocol HorizontalMimItemProtocol: Hashable { } + +public protocol HorizontalMimItemViewProtocol: View { + associatedtype Item: HorizontalMimItemProtocol + init(item: Item) +} + +public struct HorizontalMimScrollView: View where ItemView.Item == Item { + @Binding public var items: [Item] + + public init(items: Binding<[Item]>) { + self._items = items + } + + public var body: some View { + ScrollView(.horizontal) { + LazyHStack(spacing: 10) { + ForEach(items, id: \.self) { item in + ItemView(item: item) + } + .listStyle(.plain) + } + } + .contentMargins(20) + .scrollIndicators(.hidden) + .frame(height: 90) + } +} diff --git a/Projects/Core/DesignSystem/Sources/MimCategoryView.swift b/Projects/Core/DesignSystem/Sources/MimCategoryView.swift new file mode 100644 index 0000000..4dcad2b --- /dev/null +++ b/Projects/Core/DesignSystem/Sources/MimCategoryView.swift @@ -0,0 +1,38 @@ +// +// MimCategoryView.swift +// DesignSystem +// +// Created by 리나 on 2024/06/30. +// + +import SwiftUI +import ResourceKit + +public struct MimCategoryView: View { + public let title: String + public let categories: [String] + + public init(title: String, categories: [String]) { + self.title = title + self.categories = categories + } + + public var body: some View { + VStack(spacing: 0) { + HStack { + Text(title) + .font(Font.Body.Small.semiBold) + .foregroundColor(Color.Text.tertiary) + + Spacer() + } + .padding(.top, 4) + .padding(.bottom, 16) + .padding(.horizontal, 20) + + CategoryTagView(categories: categories) + .padding(.horizontal, 20) + .padding(.bottom, 20) + } + } +} diff --git a/Projects/Core/PPACModels/Sources/Search/HotKeyword.swift b/Projects/Core/PPACModels/Sources/Search/HotKeyword.swift new file mode 100644 index 0000000..65f54bc --- /dev/null +++ b/Projects/Core/PPACModels/Sources/Search/HotKeyword.swift @@ -0,0 +1,48 @@ +// +// HotKeyword.swift +// PPACModels +// +// Created by 리나 on 2024/06/29. +// + +import Foundation + +public struct HotKeyword: Hashable { + + // MARK: - Properties + + public let title: String + public let imageUrlString: String + + // MARK: - Initializers + + public init( + title: String, + imageUrlString: String + ) { + self.title = title + self.imageUrlString = imageUrlString + } +} + +public extension HotKeyword { + static let mock1 = HotKeyword( + title: "띠용오오우어어우어엉?", + imageUrlString: "https://www.blockmedia.co.kr/wp-content/uploads/2024/05/%EB%8F%84%EC%A7%80%EC%BD%94%EC%9D%B8Doge-%EC%B9%B4%EB%B6%80%EC%88%98.png" + ) + + static let mock2 = HotKeyword( + title: "짜란다짜란다", + imageUrlString: "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcR_xO_sgszdvyVBp8xz4B82kHmyyO0SNZO-4A&s" + ) + + static let mock3 = HotKeyword( + title: "후회", + imageUrlString: "https://s3.ap-northeast-2.amazonaws.com/univ-careet/FileData/Picture/202310/3e7e0445-3812-4c06-bc75-a1bed40a3332_770x426.png" + ) + + static let mock4 = HotKeyword( + title: "무한도전", + imageUrlString: "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQBnKb96Uc-894Fhgt_5xLsCvY0pSkCl0B4TA&s" + ) +} diff --git a/Projects/Core/PPACModels/Sources/Search/MimCategory.swift b/Projects/Core/PPACModels/Sources/Search/MimCategory.swift new file mode 100644 index 0000000..91ca7a6 --- /dev/null +++ b/Projects/Core/PPACModels/Sources/Search/MimCategory.swift @@ -0,0 +1,33 @@ +// +// MimCategory.swift +// PPACModels +// +// Created by 리나 on 2024/06/29. +// + +import Foundation + +public struct MimCategory: Hashable { + public let title: String + public let categories: [String] + + public init(title: String, categories: [String]) { + self.title = title + self.categories = categories + } +} + +public extension MimCategory { + static let mock1 = MimCategory( + title: "감정", + categories: ["행복", "슬픈", "무념무상", "분노", "웃긴", "피곤", "절망", "현타", "당황"] + ) + static let mock2 = MimCategory( + title: "상황", + categories: ["회사", "대학", "공부", "연애", "긁", "다이어트", "주식", "고민", "축하", "칭찬"] + ) + static let mock3 = MimCategory( + title: "컨텐츠", + categories: ["동물", "무한도전", "캐릭터", "짱구", "문상훈", "검정고무신", "그 외"] + ) +} diff --git a/Projects/Features/Search/Project.swift b/Projects/Features/Search/Project.swift index 4007555..17f30fb 100644 --- a/Projects/Features/Search/Project.swift +++ b/Projects/Features/Search/Project.swift @@ -18,7 +18,11 @@ let project = Project( sources: "Sources/**", resources: "Resources/**", dependencies: [ - + .ResourceKit, + .Core.DesignSystem, + .Core.PPACModels, + .ThirdParty.Dependency, + .ThirdParty.Kingfisher, ] ) ] diff --git a/Projects/Features/Search/Sources/View/FakeSearchBar.swift b/Projects/Features/Search/Sources/View/FakeSearchBar.swift new file mode 100644 index 0000000..cb88305 --- /dev/null +++ b/Projects/Features/Search/Sources/View/FakeSearchBar.swift @@ -0,0 +1,40 @@ +// +// FakeSearchBar.swift +// DesignSystem +// +// Created by 리나 on 2024/06/29. +// + +import SwiftUI +import ResourceKit + +struct FakeSearchBar: View { + let placeHolder: String + + init(placeHolder: String) { + self.placeHolder = placeHolder + } + + var body: some View { + fakeTextField + .frame(maxWidth: .infinity) + .frame(height: 44) + .background(Color.Background.assistive) + .clipShape(RoundedRectangle(cornerRadius: 10)) + .padding(.horizontal, 20) + .padding(.vertical, 16) + } + + private var fakeTextField: some View { + HStack(spacing: 12) { + ResourceKitAsset.Icon.search.swiftUIImage + + Text(placeHolder) + .font(Font.Body.Large.medium) + .foregroundColor(Color.Text.tertiary) + + Spacer() + } + .padding(.horizontal, 16) + } +} diff --git a/Projects/Features/Search/Sources/View/HotKeywordImageView.swift b/Projects/Features/Search/Sources/View/HotKeywordImageView.swift new file mode 100644 index 0000000..e2442e4 --- /dev/null +++ b/Projects/Features/Search/Sources/View/HotKeywordImageView.swift @@ -0,0 +1,57 @@ +// +// HotKeywordImageView.swift +// Search +// +// Created by 리나 on 2024/06/30. +// + +import SwiftUI +import Kingfisher +import PPACModels +import DesignSystem +import ResourceKit + +struct HotKeywordImageView: View, HorizontalMimItemViewProtocol { + typealias Item = HotKeyword + + let hotKeyword: HotKeyword + + init(item hotKeyword: HotKeyword) { + self.hotKeyword = hotKeyword + } + + var body: some View { + ZStack { + imageView + + Color.black.opacity(0.4) + + keywordView + } + .overlay( + RoundedRectangle(cornerRadius: 10) + .inset(by: 1) + .stroke(Color.Border.primary, lineWidth: 2) + ) + .clipShape(RoundedRectangle(cornerRadius: 10)) + } + + private var imageView: some View { + KFImage(URL(string: hotKeyword.imageUrlString)) + .resizable() + .loadDiskFileSynchronously() + .cacheMemoryOnly() + .frame(width: 90, height: 90) + .aspectRatio(contentMode: .fit) + } + + private var keywordView: some View { + Text(hotKeyword.title) + .font(Font.Body.Large.semiBold) + .foregroundColor(Color.Text.inverse) + .lineLimit(2) + .multilineTextAlignment(.center) + .padding(.horizontal, 16) + .frame(width: 90, height: 90) + } +} diff --git a/Projects/Features/Search/Sources/View/SearchPreparingAlert.swift b/Projects/Features/Search/Sources/View/SearchPreparingAlert.swift new file mode 100644 index 0000000..016e0d0 --- /dev/null +++ b/Projects/Features/Search/Sources/View/SearchPreparingAlert.swift @@ -0,0 +1,67 @@ +// +// SearchPreparingAlert.swift +// Search +// +// Created by 리나 on 2024/06/30. +// + +import SwiftUI +import ResourceKit + +struct SearchPreparingAlert: View { + private var dismiss: (() -> Void) + + init(dismiss: @escaping (() -> Void)) { + self.dismiss = dismiss + } + + var body: some View { + VStack(spacing: 0) { + title + + description + .padding(.top, 8) + + confirmButton + .padding(.top, 14) + } + .padding(.horizontal, 30) + .padding(.vertical, 20) + .background(Color.Background.white) + .cornerRadius(20) + } + + + private var title: some View { + HStack { + Text("조금만 기다려주세요!") + .font(Font.Heading.Medium.semiBold) + .foregroundColor(Color.Text.primary) + .foregroundColor(.black) + Spacer() + } + } + + private var description: some View { + HStack { + Text("검색은 준비 중이에요.") + .font(Font.Body.Large.medium) + .foregroundColor(Color.Text.secondary) + .multilineTextAlignment(.leading) + Spacer() + } + } + + private var confirmButton: some View { + HStack{ + Spacer() + Button { + dismiss() + } label: { + Text("확인") + .font(Font.Body.Large.medium) + .foregroundColor(Color.Text.brand) + } + } + } +} diff --git a/Projects/Features/Search/Sources/View/SearchView.swift b/Projects/Features/Search/Sources/View/SearchView.swift index 86c3eee..44f8c09 100644 --- a/Projects/Features/Search/Sources/View/SearchView.swift +++ b/Projects/Features/Search/Sources/View/SearchView.swift @@ -6,17 +6,106 @@ // import SwiftUI +import PPACModels +import ResourceKit +import DesignSystem public struct SearchView: View { - public init() { } + @State public var hotKeywords: [HotKeyword] + @State public var mimCategories: [MimCategory] + @State private var isPresenting: Bool = false + + public init(hotKeywords: [HotKeyword] = [], mimCategories: [MimCategory] = []) { + self.hotKeywords = hotKeywords + self.mimCategories = mimCategories + } public var body: some View { - Text("검색 화면") - .frame(maxWidth: .infinity, maxHeight: .infinity) - .background(.pink) + VStack(spacing: 0) { + fakeSearchBar + + ScrollView { + VStack(spacing: 0) { + currentHotKeywords + mimCategoriesViews + } + } + .scrollIndicators(.hidden) + } + .padding(.bottom, 40) + .basicModal( + isPresented: $isPresenting, + opacity: 0.5, + content: { + SearchPreparingAlert(dismiss: { isPresenting = false }) + } + ) + } + + private var fakeSearchBar: some View { + Button { + isPresenting = true + } label: { + FakeSearchBar(placeHolder: "🚧 검색은 오픈 준비 중!") + } + .buttonStyle(PlainButtonStyle()) + } + + private var currentHotKeywords: some View { + VStack(alignment: .leading, spacing: 0) { + HStack(spacing: 8) { + ResourceKitAsset.Icon.special.swiftUIImage + .resizable() + .aspectRatio(contentMode: .fit) + .frame(width: 20, height: 20) + + Text("두둥! 요즘 핫한 #키워드") + .font(Font.Heading.Small.semiBold) + .foregroundColor(Color.Text.primary) + } + .padding(.horizontal, 20) + .padding(.vertical, 18) + + HorizontalMimScrollView(items: $hotKeywords) + } + .padding(.bottom, 40) + } + + private var mimCategoriesViews: some View { + VStack(alignment: .leading, spacing: 0) { + HStack(spacing: 8) { + ResourceKitAsset.Icon.category.swiftUIImage + .resizable() + .aspectRatio(contentMode: .fit) + .frame(width: 20, height: 20) + + Text("무슨 밈 찾아?") + .font(Font.Heading.Small.semiBold) + .foregroundColor(Color.Text.primary) + } + .padding(.horizontal, 20) + .padding(.vertical, 18) + + ForEach(mimCategories, id: \.self) { mimCategory in + MimCategoryView(title: mimCategory.title, categories: mimCategory.categories) + } + } } } +extension HotKeyword: HorizontalMimItemProtocol {} + #Preview { - SearchView() + SearchView( + hotKeywords: [ + .mock1, + .mock2, + .mock3, + .mock4 + ], mimCategories: [ + .mock1, + .mock2, + .mock3 + ] + ) } diff --git a/Projects/ResourceKit/Resources/Icon.xcassets/Search.imageset/Contents.json b/Projects/ResourceKit/Resources/Icon.xcassets/Search.imageset/Contents.json index 4e324d2..668818a 100644 --- a/Projects/ResourceKit/Resources/Icon.xcassets/Search.imageset/Contents.json +++ b/Projects/ResourceKit/Resources/Icon.xcassets/Search.imageset/Contents.json @@ -1,19 +1,8 @@ { "images" : [ { - "filename" : "Search.png", - "idiom" : "universal", - "scale" : "1x" - }, - { - "filename" : "Search@2x.png", - "idiom" : "universal", - "scale" : "2x" - }, - { - "filename" : "Search@3x.png", - "idiom" : "universal", - "scale" : "3x" + "filename" : "Search.svg", + "idiom" : "universal" } ], "info" : { diff --git a/Projects/ResourceKit/Resources/Icon.xcassets/Search.imageset/Search.png b/Projects/ResourceKit/Resources/Icon.xcassets/Search.imageset/Search.png deleted file mode 100644 index 2effee2..0000000 Binary files a/Projects/ResourceKit/Resources/Icon.xcassets/Search.imageset/Search.png and /dev/null differ diff --git a/Projects/ResourceKit/Resources/Icon.xcassets/Search.imageset/Search.svg b/Projects/ResourceKit/Resources/Icon.xcassets/Search.imageset/Search.svg new file mode 100644 index 0000000..28fb681 --- /dev/null +++ b/Projects/ResourceKit/Resources/Icon.xcassets/Search.imageset/Search.svg @@ -0,0 +1,4 @@ + + + + diff --git a/Projects/ResourceKit/Resources/Icon.xcassets/Search.imageset/Search@2x.png b/Projects/ResourceKit/Resources/Icon.xcassets/Search.imageset/Search@2x.png deleted file mode 100644 index e42a2d4..0000000 Binary files a/Projects/ResourceKit/Resources/Icon.xcassets/Search.imageset/Search@2x.png and /dev/null differ diff --git a/Projects/ResourceKit/Resources/Icon.xcassets/Search.imageset/Search@3x.png b/Projects/ResourceKit/Resources/Icon.xcassets/Search.imageset/Search@3x.png deleted file mode 100644 index 4d1a0ab..0000000 Binary files a/Projects/ResourceKit/Resources/Icon.xcassets/Search.imageset/Search@3x.png and /dev/null differ