From d7696710c5a202a449705f1d78f69fd70d02121a Mon Sep 17 00:00:00 2001 From: hhhello Date: Mon, 12 Aug 2024 08:16:38 +0900 Subject: [PATCH 1/3] feat: Create MealCell --- Projects/Domain/Source/Entity/MealType.swift | 26 +++++++ .../Source/Meal/Component/MealCell.swift | 40 +++++++++++ Projects/Feature/Source/Meal/MealView.swift | 69 ++----------------- 3 files changed, 72 insertions(+), 63 deletions(-) create mode 100644 Projects/Domain/Source/Entity/MealType.swift create mode 100644 Projects/Feature/Source/Meal/Component/MealCell.swift diff --git a/Projects/Domain/Source/Entity/MealType.swift b/Projects/Domain/Source/Entity/MealType.swift new file mode 100644 index 0000000..cfcee49 --- /dev/null +++ b/Projects/Domain/Source/Entity/MealType.swift @@ -0,0 +1,26 @@ +// +// MealType.swift +// Domain +// +// Created by hhhello0507 on 8/12/24. +// + +import Foundation +import SwiftBok + +public enum MealType: Int, RawRepresentable { + case breakfast + case lunch + case dinner + + public var label: String { + switch self { + case .breakfast: + "아침" + case .lunch: + "점심" + case .dinner: + "저녁" + } + } +} diff --git a/Projects/Feature/Source/Meal/Component/MealCell.swift b/Projects/Feature/Source/Meal/Component/MealCell.swift new file mode 100644 index 0000000..13419fe --- /dev/null +++ b/Projects/Feature/Source/Meal/Component/MealCell.swift @@ -0,0 +1,40 @@ +// +// MealCell.swift +// Feature +// +// Created by hhhello0507 on 8/12/24. +// + +import SwiftUI +import DDS +import Domain + +struct MealCell: View { + + private let type: MealType + private let meal: Meal + + init(type: MealType, meal: Meal) { + self.type = type + self.meal = meal + } + + var body: some View { + VStack(spacing: 16) { + HStack(spacing: 12) { + DodamTag(type.label, type: .primary) + Spacer() + Text("\(Int(meal.calorie))Kcal") + .label(.medium) + .foreground(DodamColor.Label.alternative) + } + Text(meal.details.map { $0.name }.joined(separator: "\n")) + .body1(.medium) + .foreground(DodamColor.Label.normal) + .frame(maxWidth: .infinity, alignment: .leading) + } + .padding(16) + .background(DodamColor.Background.normal) + .clipShape(.large) + } +} diff --git a/Projects/Feature/Source/Meal/MealView.swift b/Projects/Feature/Source/Meal/MealView.swift index 33286d0..b1a0eba 100644 --- a/Projects/Feature/Source/Meal/MealView.swift +++ b/Projects/Feature/Source/Meal/MealView.swift @@ -31,70 +31,13 @@ struct MealView: View { let today = Calendar.current.startOfDay(for: Date()) return $0.exists && $0.date >= today } - }(), id: \.self) { datas in - Text(datas.date.parseString(format: "M월 d일 EEEE")) -// .font(.body(.medium)) // TODO: Add color - // .dodamColor( - // isToday(datas.date) - // ? .onPrimary - // : .onSecondaryContainer - // ) - .padding(.vertical, 4) - .frame(width: 218, height: 31) -// .background( -// isToday(datas.date) -// ? Dodam.color(.primary).opacity(0.65) -// : Dodam.color(.secondaryContainer) -// ) - .clipShape(RoundedRectangle(cornerRadius: 32)) - + }(), id: \.self) { meals in + let meals = Array([meals.breakfast, meals.lunch, meals.dinner].enumerated()) LazyVStack(spacing: 12) { - ForEach(0..<3, id: \.self) { idx in - let data: Meal? = { - switch idx { - case 0: datas.breakfast - case 1: datas.lunch - case 2: datas.dinner - default: nil - } - }() - if let data = data { - VStack(spacing: 16) { - HStack(spacing: 12) { - ZStack { - Text({ () -> String in - return switch idx { - case 0: "아침" - case 1: "점심" - case 2:"저녁" - default: "" - } - }()) -// .font(.body(.medium)) -// .dodamColor(.onPrimary) - } - .frame(width: 52, height: 27) -// .background( -// isMealTime(datas.date, mealType: idx) -// ? Dodam.color(.primary) -// : Dodam.color(.onSurfaceVariant) -// ) - .clipShape(RoundedRectangle(cornerRadius: 32)) - Spacer() - Text("\(Int(data.calorie))Kcal") -// .font(.label(.large)) -// .dodamColor(.onSurfaceVariant) - .padding(.top, 5) - } - .padding([.top, .horizontal], 16) - Text(data.details.map { $0.name }.joined(separator: "\n")) -// .font(.body(.medium)) -// .dodamColor(.onSurface) - .frame(maxWidth: .infinity, alignment: .leading) - .padding([.bottom, .horizontal], 16) - } -// .background(Dodam.color(.surfaceContainer)) - .clipShape(RoundedRectangle(cornerRadius: 18)) + ForEach(meals, id: \.element) { idx, meal in + if let mealType = MealType(rawValue: idx), + let meal { + MealCell(type: mealType, meal: meal) } } } From efa4d1ecb602eb634740ba73f2bcb2891a6f42c6 Mon Sep 17 00:00:00 2001 From: hhhello Date: Mon, 12 Aug 2024 13:10:39 +0900 Subject: [PATCH 2/3] feat: Apply new MealView design --- Projects/App/iOS/Source/AppMain.swift | 4 + .../Source/Component/CalendarDateCell.swift | 51 ++++ .../Source/Meal/Component/MealCell.swift | 4 +- Projects/Feature/Source/Meal/MealView.swift | 269 +++++++++++++----- .../Feature/Source/Meal/MealViewModel.swift | 8 + .../Source/Extension/Foundation/DateExt.swift | 59 +++- .../Extension/Foundation/StringExt.swift | 2 +- 7 files changed, 319 insertions(+), 78 deletions(-) create mode 100644 Projects/Feature/Source/Component/CalendarDateCell.swift diff --git a/Projects/App/iOS/Source/AppMain.swift b/Projects/App/iOS/Source/AppMain.swift index 19dbc4a..06ca19c 100644 --- a/Projects/App/iOS/Source/AppMain.swift +++ b/Projects/App/iOS/Source/AppMain.swift @@ -36,6 +36,10 @@ struct AppMain: App { FlowPresenter(flow: flow) } .ignoresSafeArea() + .onAppear { + let color = Dodam.color(DodamColor.Primary.normal) + UIRefreshControl.appearance().tintColor = UIColor(color) + } } } } diff --git a/Projects/Feature/Source/Component/CalendarDateCell.swift b/Projects/Feature/Source/Component/CalendarDateCell.swift new file mode 100644 index 0000000..f2cb664 --- /dev/null +++ b/Projects/Feature/Source/Component/CalendarDateCell.swift @@ -0,0 +1,51 @@ +// +// CalendarCell.swift +// Feature +// +// Created by hhhello0507 on 8/12/24. +// + +import SwiftUI +import DDS + +struct CalendarDateCell: View { + + private let date: Date? + private let selected: Bool + + init(date: Date?, selected: Bool) { + self.date = date + self.selected = selected + } + + private var label: String { + guard let date else { + return "" + } + guard let day = date[.day] else { + return "" + } + return "\(day)" + } + + var body: some View { + Text(label) + .headline(.medium) + .foreground( + selected + ? DodamColor.Static.white + : DodamColor.Label.alternative + ) + .padding(.vertical, 8) + .if(selected) { view in + view.background { + Rectangle() + .dodamFill(DodamColor.Primary.normal) + .clipShape(.medium) + .frame(width: 36, height: 36) + } + } + .frame(maxWidth: .infinity) + .opacity(date == nil ? 0 : 1) + } +} diff --git a/Projects/Feature/Source/Meal/Component/MealCell.swift b/Projects/Feature/Source/Meal/Component/MealCell.swift index 13419fe..3568d0e 100644 --- a/Projects/Feature/Source/Meal/Component/MealCell.swift +++ b/Projects/Feature/Source/Meal/Component/MealCell.swift @@ -20,8 +20,8 @@ struct MealCell: View { } var body: some View { - VStack(spacing: 16) { - HStack(spacing: 12) { + VStack(spacing: 12) { + HStack { DodamTag(type.label, type: .primary) Spacer() Text("\(Int(meal.calorie))Kcal") diff --git a/Projects/Feature/Source/Meal/MealView.swift b/Projects/Feature/Source/Meal/MealView.swift index b1a0eba..e0c4612 100644 --- a/Projects/Feature/Source/Meal/MealView.swift +++ b/Projects/Feature/Source/Meal/MealView.swift @@ -11,99 +11,220 @@ import Domain import FlowKit import Shared +struct ScrollOffsetPreferenceKey: PreferenceKey { + static var defaultValue: CGFloat = 0 + static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) { + value += nextValue() + } +} + struct MealView: View { - @StateObject var viewModel = MealViewModel() - @Flow var flow - - func isToday(_ date: Date) -> Bool { - let today = Date() - return Calendar.current.isDate(date, inSameDayAs: today) - } + @StateObject private var viewModel = MealViewModel() + @Flow private var flow + @Namespace private var animation + @GestureState private var dragOffset = CGSize.zero + @State private var openCalendar = false + @State private var scrollOffset: CGFloat = .zero + + private let weekdays = ["일", "월", "화", "수", "목", "금", "토"] + private let calendar = Calendar.current + private let animationDuration = 0.4 var body: some View { - DodamScrollView.default(title: "급식") { - LazyVStack(spacing: 20) { - if let datas = viewModel.mealData { - if !datas.isEmpty { - ForEach({ () -> [MealResponse] in - datas.filter { - let today = Calendar.current.startOfDay(for: Date()) - return $0.exists && $0.date >= today + let title = openCalendar ? "급식" : viewModel.selectedCalendar.parseString(format: "yyyy년 M월 급식") + DodamScrollView.default(title: title) { + LazyVStack(spacing: 16) { + VStack(spacing: 16) { + if openCalendar { + HStack(spacing: 8) { + Text(viewModel.selectedCalendar.parseString(format: "yyyy년 M월")) + .headline(.medium) + .foreground(DodamColor.Label.strong) + Spacer() + Button { + if let date = calendar.date(byAdding: .month, value: -1, to: viewModel.selectedCalendar) { + viewModel.selectedCalendar = date + } + } label: { + Image(icon: .chevronLeft) + .resizable() + .foreground(DodamColor.Primary.normal) + .frame(width: 20, height: 20) + .padding(8) + } + .scaledButtonStyle() + Button { + if let date = calendar.date(byAdding: .month, value: 1, to: viewModel.selectedCalendar) { + viewModel.selectedCalendar = date + } + } label: { + Image(icon: .chevronRight) + .resizable() + .foreground(DodamColor.Primary.normal) + .frame(width: 20, height: 20) + .padding(8) } - }(), id: \.self) { meals in - let meals = Array([meals.breakfast, meals.lunch, meals.dinner].enumerated()) - LazyVStack(spacing: 12) { - ForEach(meals, id: \.element) { idx, meal in - if let mealType = MealType(rawValue: idx), - let meal { - MealCell(type: mealType, meal: meal) + .scaledButtonStyle() + } + } + VStack(spacing: 0) { + HStack(spacing: 0) { + ForEach(weekdays, id: \.self) { date in + Text(date) + .label(.regular) + .foreground(DodamColor.Label.alternative) + .frame(maxWidth: .infinity) + } + } + if openCalendar { + ForEach(viewModel.selectedCalendar.weeks, id: \.hashValue) { week in + HStack(spacing: 0) { + ForEach(Array(week.enumerated()), id: \.offset) { _, date in + let selected: Bool = date == nil ? false : viewModel.selectedDate.equals(date!, components: [.year, .month, .day]) + Button { + withAnimation(.spring(duration: animationDuration)) { + openCalendar = false + } + if !selected, let date { + viewModel.selectedDate = date + viewModel.selectedCalendar = date + } + } label: { + CalendarDateCell(date: date, selected: selected) + } } } } + } else { + HStack(spacing: 0) { + ForEach(viewModel.selectedDate.weeklyDates, id: \.self) { date in + let selected = viewModel.selectedDate.equals(date, components: [.year, .month, .day]) + Button { + withAnimation(.spring(duration: animationDuration)) { + openCalendar = selected + } + if !selected { + viewModel.selectedDate = date + viewModel.selectedCalendar = date + } + } label: { + CalendarDateCell(date: date, selected: selected) + } + .scaledButtonStyle() + } + } + .simultaneousGesture( + DragGesture() + .onEnded { value in + if abs(value.translation.width) > 85 || abs(value.translation.height) > 85 { + withAnimation(.spring(duration: animationDuration)) { + openCalendar = true + } + } + } + ) + } + } + } + .padding(.horizontal, 16) + .padding(.top, 12) + DodamDivider() + if let meals = viewModel.selectedMeal { + let meals = Array([meals.breakfast, meals.lunch, meals.dinner].compactMap { $0 }.enumerated()) + VStack(spacing: 12) { + ForEach(meals, id: \.offset) { idx, meal in + if let mealType = MealType(rawValue: idx) { + MealCell(type: mealType, meal: meal) + } } - } else { - Text("이번 달 급식이 없어요") - .font(.system(size: 16, weight: .medium)) -// .dodamColor(.tertiary) -// .padding(.top, 20) -// .frame(maxWidth: .infinity) } - } else { - DodamLoadingView() + .padding(.horizontal, 16) + .matchedGeometryEffect(id: 0, in: animation) + } + } + .background { + GeometryReader { geometry in + Color.clear + .preference(key: ScrollOffsetPreferenceKey.self, value: geometry.frame(in: .named("scrollView")).minY) } } - .padding(.horizontal, 16) } .background(DodamColor.Background.neutral) - .task { - await viewModel.onAppear() + .coordinateSpace(name: "scrollView") + .onPreferenceChange(ScrollOffsetPreferenceKey.self) { value in + self.scrollOffset = value } - .refreshable { - await viewModel.onRefresh() + .onChange(of: scrollOffset) { + if $0 > 100 { + withAnimation(.spring(duration: animationDuration)) { + openCalendar = true + } + } else if $0 < 48 { // top app bar height + withAnimation(.spring(duration: animationDuration)) { + openCalendar = false + viewModel.selectedCalendar = viewModel.selectedDate + } + } } - } - - func isMealTime(_ date: Date, mealType: Int) -> Bool { - - let cc = Calendar.current - // 현재 시간의 년, 월, 일 - let currentDate = Date() - let currentYear = cc.component(.year, from: currentDate) - let currentMonth = cc.component(.month, from: currentDate) - let currentDay = cc.component(.day, from: currentDate) - let currentHour = cc.component(.hour, from: currentDate) - let currentMinute = cc.component(.minute, from: currentDate) - - // 입력된 시간의 년, 월, 일 - let year = cc.component(.year, from: date) - let month = cc.component(.month, from: date) - let day = cc.component(.day, from: date) - - // 날짜가 오늘인지 확인 - guard currentYear == year && currentMonth == month && currentDay == day else { - return false + .overlay { + if viewModel.selectedMeal == nil && !openCalendar { + VStack(spacing: 12) { + Image(icon: .cookedRice) + .resizable() + .frame(width: 36, height: 36) + Text("급식이 없어요") + .label(.medium) + .foreground(DodamColor.Label.alternative) + } + .padding(.vertical, 40) + } } - - switch mealType { - case 0: - // 아침: ~ 8:20 - return (currentHour >= 0 && currentHour < 8) || - (currentHour == 8 && currentMinute <= 20) - case 1: - // 점심: 8:21 ~ 13:30 - return (currentHour == 8 && currentMinute > 20) || - (currentHour > 8 && currentHour < 13) || - (currentHour == 13 && currentMinute <= 30) - case 2: - // 저녁: 13:31 ~ 19:10 - return (currentHour == 13 && currentMinute > 30) || - (currentHour > 13 && currentHour < 19) || - (currentHour == 19 && currentMinute <= 10) - default: - return false + .task { + await viewModel.onAppear() } } + // + // func isMealTime(_ date: Date, mealType: Int) -> Bool { + // + // let cc = Calendar.current + // // 현재 시간의 년, 월, 일 + // let currentDate = Date() + // let currentYear = cc.component(.year, from: currentDate) + // let currentMonth = cc.component(.month, from: currentDate) + // let currentDay = cc.component(.day, from: currentDate) + // let currentHour = cc.component(.hour, from: currentDate) + // let currentMinute = cc.component(.minute, from: currentDate) + // + // // 입력된 시간의 년, 월, 일 + // let year = cc.component(.year, from: date) + // let month = cc.component(.month, from: date) + // let day = cc.component(.day, from: date) + // + // // 날짜가 오늘인지 확인 + // guard currentYear == year && currentMonth == month && currentDay == day else { + // return false + // } + // + // switch mealType { + // case 0: + // // 아침: ~ 8:20 + // return (currentHour >= 0 && currentHour < 8) || + // (currentHour == 8 && currentMinute <= 20) + // case 1: + // // 점심: 8:21 ~ 13:30 + // return (currentHour == 8 && currentMinute > 20) || + // (currentHour > 8 && currentHour < 13) || + // (currentHour == 13 && currentMinute <= 30) + // case 2: + // // 저녁: 13:31 ~ 19:10 + // return (currentHour == 13 && currentMinute > 30) || + // (currentHour > 13 && currentHour < 19) || + // (currentHour == 19 && currentMinute <= 10) + // default: + // return false + // } + // } } #Preview { diff --git a/Projects/Feature/Source/Meal/MealViewModel.swift b/Projects/Feature/Source/Meal/MealViewModel.swift index 795e48e..a3f1725 100644 --- a/Projects/Feature/Source/Meal/MealViewModel.swift +++ b/Projects/Feature/Source/Meal/MealViewModel.swift @@ -9,11 +9,19 @@ import Combine import Domain import DIContainer import Shared +import Foundation class MealViewModel: ObservableObject { // MARK: - State @Published var mealData: [MealResponse]? + @Published var selectedDate: Date = .now + var selectedMeal: MealResponse? { + mealData?.first { + $0.date.equals(selectedDate, components: [.year, .month, .day]) + } + } + @Published var selectedCalendar: Date = .now // MARK: - Repository @Inject var mealRepository: any MealRepository diff --git a/Projects/Shared/Source/Extension/Foundation/DateExt.swift b/Projects/Shared/Source/Extension/Foundation/DateExt.swift index 12000f8..ba2cb6c 100644 --- a/Projects/Shared/Source/Extension/Foundation/DateExt.swift +++ b/Projects/Shared/Source/Extension/Foundation/DateExt.swift @@ -10,7 +10,6 @@ import Foundation public extension Date { func parseString(format: String) -> String { - let dateFormatter = DateFormatter() dateFormatter.dateFormat = format dateFormatter.locale = Locale(identifier: "ko_KR") @@ -29,4 +28,62 @@ public extension Date { "-" } } + + subscript(components: Calendar.Component) -> Int? { + var calendar = Calendar.current + calendar.locale = .init(identifier: "ko_KR") + return calendar.dateComponents([components], from: self).value(for: components) + } + + func equals(_ other: Date, components: Set) -> Bool { + var calendar = Calendar.current + calendar.locale = .init(identifier: "ko_KR") + let selfComponents = calendar.dateComponents(components, from: self) + let otherComponents = calendar.dateComponents(components, from: other) + return selfComponents == otherComponents + } + + var weeklyDates: [Date] { + var calendar = Calendar.current + calendar.locale = .init(identifier: "ko_KR") + let startOfWeek = calendar.date(from: calendar.dateComponents([.yearForWeekOfYear, .weekOfYear], from: self))! + return (0..<7).compactMap { number in + if let date = calendar.date(byAdding: .day, value: number, to: startOfWeek) { + date + } else { + nil + } + } + } + + var range: Int? { + let calendar = Calendar.current + return calendar.range(of: .day, in: .month, for: self)?.count + } + + // self의 month를 기준으로 calendar 생성 + // nil: 이전 month 혹은 다음 month + var weeks: [[Date?]] { + let calendar = Calendar.current + // 해당 월의 첫째 날 + var components = calendar.dateComponents([.year, .month], from: self) + components.day = 1 + let firstDayOfMonth = calendar.date(from: components)! + + // 첫째 날의 요일 (일요일 = 1, 월요일 = 2, ..., 토요일 = 7) + let firstWeekday = calendar.component(.weekday, from: firstDayOfMonth) + + // 날짜 배열 생성 + var days: [Date?] = Array(repeating: nil, count: firstWeekday - 1) + days += Array(1...(range ?? 0)).compactMap { + components.day = $0 + return calendar.date(from: components) + } + days += Array(repeating: nil, count: (7 - days.count % 7) % 7) + + // 주 단위로 배열을 나눔 + return stride(from: 0, to: days.count, by: 7).map { + Array(days[$0..<$0 + 7]) + } + } } diff --git a/Projects/Shared/Source/Extension/Foundation/StringExt.swift b/Projects/Shared/Source/Extension/Foundation/StringExt.swift index dcf98b9..adc5235 100644 --- a/Projects/Shared/Source/Extension/Foundation/StringExt.swift +++ b/Projects/Shared/Source/Extension/Foundation/StringExt.swift @@ -11,7 +11,7 @@ public extension StringProtocol { subscript(offset: Int) -> Character { self[index(startIndex, offsetBy: offset)] } - subscript (bounds: CountableRange) -> String { + subscript(bounds: CountableRange) -> String { let start = index(startIndex, offsetBy: bounds.lowerBound) let end = index(startIndex, offsetBy: bounds.upperBound) return String(self[start.. Date: Mon, 12 Aug 2024 15:09:53 +0900 Subject: [PATCH 3/3] fix: MealContainer --- .../Source/Response/Meal/MealResponse.swift | 4 ++ .../Source/Home/Component/MealContainer.swift | 63 ++++++------------- Projects/Feature/Source/Meal/MealView.swift | 3 +- .../Extension/Foundation/ArrayUtil.swift | 15 +++++ .../Source/Extension/SwiftUI/ViewExt.swift | 21 +++++++ 5 files changed, 60 insertions(+), 46 deletions(-) create mode 100644 Projects/Shared/Source/Extension/Foundation/ArrayUtil.swift diff --git a/Projects/Domain/Source/Response/Meal/MealResponse.swift b/Projects/Domain/Source/Response/Meal/MealResponse.swift index d2128a6..6d83ab3 100644 --- a/Projects/Domain/Source/Response/Meal/MealResponse.swift +++ b/Projects/Domain/Source/Response/Meal/MealResponse.swift @@ -17,4 +17,8 @@ public struct MealResponse: ResponseProtocol { public let breakfast: Meal? public let lunch: Meal? public let dinner: Meal? + + public var meals: [Meal] { + [self.breakfast, self.lunch, self.dinner].compactMap { $0 } + } } diff --git a/Projects/Feature/Source/Home/Component/MealContainer.swift b/Projects/Feature/Source/Home/Component/MealContainer.swift index 704ab55..435a24a 100644 --- a/Projects/Feature/Source/Home/Component/MealContainer.swift +++ b/Projects/Feature/Source/Home/Component/MealContainer.swift @@ -8,72 +8,47 @@ import SwiftUI import DDS import Domain +import Shared struct MealContainer: View { + @State private var pageSize: CGSize? private let mealData: MealResponse? @Binding var mealIdx: Int - + public init( data mealData: MealResponse?, mealIdx: Binding ) { self.mealData = mealData self._mealIdx = mealIdx - self.animatedIdx = mealIdx.wrappedValue } - - @State private var animatedIdx: Int - @State private var heights: [Int: CGFloat] = [:] - + var body: some View { if let data = mealData { if data.exists { - let meals = [ - data.breakfast, - data.lunch, - data.dinner - ] DodamPageView(selection: $mealIdx) { - ForEach(meals.indices, id: \.self) { idx in - GeometryReader { geometryProxy in - let text = meals[idx]?.details.map { - $0.name - }.joined(separator: ", ") ?? "급식이 없어요." - Text(text) - .body1(.medium) - .foreground(DodamColor.Label.normal) - .multilineTextAlignment(.leading) - .fixedSize(horizontal: false, vertical: true) - .onAppear { - withAnimation(.spring) { - heights[idx] = text.boundingRect( - with: CGSize( - width: geometryProxy.size.width, - height: .greatestFiniteMagnitude - ), - options: .usesLineFragmentOrigin, - attributes: [ - .font: UIFont( - name: Pretendard.Weight.semibold.rawValue, - size: 18 - )! - ], - context: nil - ).height + ForEach(data.meals, id: \.self) { meal in + let splitedArray = splitArray(array: meal.details) + HStack(alignment: .top) { + ForEach(splitedArray, id: \.self) { meals in + VStack(alignment: .leading, spacing: 0) { + ForEach(meals, id: \.self) { meal in + Text(meal.name) + .body1(.medium) + .foreground(DodamColor.Label.normal) } } + .frame(maxWidth: .infinity, alignment: .leading) + } + } + .onReadSize { size in + self.pageSize = size } - .padding(.horizontal, 6) .page() } } - .frame(height: heights[animatedIdx] ?? 44.928) - .onChange(of: mealIdx) { newValue in - withAnimation(.spring(duration: 0.2)) { - animatedIdx = newValue - } - } + .frame(height: pageSize?.height ?? 999) .onAppear { let currentTime = Date() let calendar = Calendar.current diff --git a/Projects/Feature/Source/Meal/MealView.swift b/Projects/Feature/Source/Meal/MealView.swift index e0c4612..ac94637 100644 --- a/Projects/Feature/Source/Meal/MealView.swift +++ b/Projects/Feature/Source/Meal/MealView.swift @@ -131,9 +131,8 @@ struct MealView: View { .padding(.top, 12) DodamDivider() if let meals = viewModel.selectedMeal { - let meals = Array([meals.breakfast, meals.lunch, meals.dinner].compactMap { $0 }.enumerated()) VStack(spacing: 12) { - ForEach(meals, id: \.offset) { idx, meal in + ForEach(Array(meals.meals.enumerated()), id: \.offset) { idx, meal in if let mealType = MealType(rawValue: idx) { MealCell(type: mealType, meal: meal) } diff --git a/Projects/Shared/Source/Extension/Foundation/ArrayUtil.swift b/Projects/Shared/Source/Extension/Foundation/ArrayUtil.swift new file mode 100644 index 0000000..5ec6143 --- /dev/null +++ b/Projects/Shared/Source/Extension/Foundation/ArrayUtil.swift @@ -0,0 +1,15 @@ +// +// ArrayExt.swift +// Shared +// +// Created by hhhello0507 on 8/12/24. +// + +import Foundation + +public func splitArray(array: [T]) -> [[T]] { + let middleIndex: Int = Int(ceil(Double(array.count) / 2.0)) + let firstHalf = Array(array[0.. Void) -> some View { + self.customBackground { + GeometryReader { geometryProxy in + Color.clear + .preference(key: SizePreferenceKey.self, value: geometryProxy.size) + } + } + .onPreferenceChange(SizePreferenceKey.self, perform: perform) + } + + @ViewBuilder + func customBackground(alignment: Alignment = .center, @ViewBuilder content: () -> V) -> some View { + self.background(alignment: alignment, content: content) + } +} + +struct SizePreferenceKey: PreferenceKey { + static var defaultValue: CGSize = .zero + static func reduce(value: inout CGSize, nextValue: () -> CGSize) { } }