Skip to content

Commit

Permalink
feat: Apply new MealView design
Browse files Browse the repository at this point in the history
  • Loading branch information
hhhello0507 committed Aug 12, 2024
1 parent d769671 commit efa4d1e
Show file tree
Hide file tree
Showing 7 changed files with 319 additions and 78 deletions.
4 changes: 4 additions & 0 deletions Projects/App/iOS/Source/AppMain.swift
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,10 @@ struct AppMain: App {
FlowPresenter(flow: flow)
}
.ignoresSafeArea()
.onAppear {
let color = Dodam.color(DodamColor.Primary.normal)
UIRefreshControl.appearance().tintColor = UIColor(color)
}
}
}
}
51 changes: 51 additions & 0 deletions Projects/Feature/Source/Component/CalendarDateCell.swift
Original file line number Diff line number Diff line change
@@ -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)
}
}
4 changes: 2 additions & 2 deletions Projects/Feature/Source/Meal/Component/MealCell.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
269 changes: 195 additions & 74 deletions Projects/Feature/Source/Meal/MealView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
8 changes: 8 additions & 0 deletions Projects/Feature/Source/Meal/MealViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading

0 comments on commit efa4d1e

Please sign in to comment.