From 91d64cd35ee44e58d665b9b5a143bfc1d5a95fb2 Mon Sep 17 00:00:00 2001 From: HISEHOONAN Date: Sun, 1 Jun 2025 18:48:27 +0900 Subject: [PATCH 1/5] [FEAT] #40 Add Loading Indicator in dietaryView --- .../Article/View/ArticleView.swift | 1 + .../Article/ViewModel/ArticleViewModel.swift | 1 + .../Common/LoadingIndicator.swift | 39 +++++++++++++++++++ .../Dietary/View/DietaryView.swift | 16 -------- .../Dietary/View/RecommendView.swift | 1 + .../Dietary/ViewModel/DietaryViewModel.swift | 5 +++ 6 files changed, 47 insertions(+), 16 deletions(-) create mode 100644 Dietto/Dietto/Presentation/Common/LoadingIndicator.swift diff --git a/Dietto/Dietto/Presentation/Article/View/ArticleView.swift b/Dietto/Dietto/Presentation/Article/View/ArticleView.swift index e8e43bd..7229872 100644 --- a/Dietto/Dietto/Presentation/Article/View/ArticleView.swift +++ b/Dietto/Dietto/Presentation/Article/View/ArticleView.swift @@ -37,6 +37,7 @@ struct ArticleView: View { .background(Color.appMain) .clipShape(Capsule()) } + } .padding([.leading, .trailing], 16) Text("내 관심사") diff --git a/Dietto/Dietto/Presentation/Article/ViewModel/ArticleViewModel.swift b/Dietto/Dietto/Presentation/Article/ViewModel/ArticleViewModel.swift index 721aa46..ca7b8c6 100644 --- a/Dietto/Dietto/Presentation/Article/ViewModel/ArticleViewModel.swift +++ b/Dietto/Dietto/Presentation/Article/ViewModel/ArticleViewModel.swift @@ -10,6 +10,7 @@ import SwiftUI final class ArticleViewModel: ObservableObject { @Published var selectedInterests: [InterestEntity] = [] @Published var articles: [ArticleEntity] = [] + @Published var isLoading : Bool = false private let alanUsecase: AlanUsecase private let storageUsecase: InterestsUsecase diff --git a/Dietto/Dietto/Presentation/Common/LoadingIndicator.swift b/Dietto/Dietto/Presentation/Common/LoadingIndicator.swift new file mode 100644 index 0000000..7d698c6 --- /dev/null +++ b/Dietto/Dietto/Presentation/Common/LoadingIndicator.swift @@ -0,0 +1,39 @@ +// +// LoadingIndicator.swift +// Dietto +// +// Created by 안세훈 on 6/1/25. +// + +import SwiftUI + +struct LoadingViewModifier: ViewModifier { + let isLoading: Bool + var tint: Color = .white + var scale: CGFloat = 1.5 + var overlayColor: Color = Color.white.opacity(0.3) + + func body(content: Content) -> some View { + ZStack { + content + if isLoading { + overlayColor.ignoresSafeArea() + ProgressView().progressViewStyle(CircularProgressViewStyle(tint: .appMain)) + .scaleEffect(1.5) + .padding() + } + } + } +} + + +extension View { + func loadingOverlay(isLoading: Bool, tint: Color = .appMain, scale: CGFloat = 1.5, overlayColor: Color = Color.black.opacity(0.15)) -> some View { + self.modifier(LoadingViewModifier(isLoading: isLoading, tint: tint, scale: scale, overlayColor: overlayColor)) + } +} + +#Preview { + ZStack { + }.loadingOverlay(isLoading: true) +} diff --git a/Dietto/Dietto/Presentation/Dietary/View/DietaryView.swift b/Dietto/Dietto/Presentation/Dietary/View/DietaryView.swift index fce246b..9dce1c1 100644 --- a/Dietto/Dietto/Presentation/Dietary/View/DietaryView.swift +++ b/Dietto/Dietto/Presentation/Dietary/View/DietaryView.swift @@ -20,7 +20,6 @@ struct DietaryView: View { @State private var PushToRecommandView : Bool = false // 화면이동 - @State private var isLoading : Bool = false #warning("상태값 관리.") @@ -162,9 +161,7 @@ struct DietaryView: View { Button("식단 추천받기") { print("식단 추천 받기 버튼이 클릭댐") if !dietartViewModel.presentIngredients.isEmpty { - isLoading = true dietartViewModel.fetchRecommendations(ingredients: dietartViewModel.presentIngredients) - isLoading = false PushToRecommandView = true }else{ print("비어있음 현재 식재료가 ") @@ -184,19 +181,6 @@ struct DietaryView: View { } } - .overlay(content: { - ///loading indicator - if isLoading { - Color.black.opacity(0.4) - .ignoresSafeArea() - ProgressView() - .progressViewStyle(CircularProgressViewStyle(tint: .white)) - .foregroundStyle(.white) - .padding() - .background(Color.gray.opacity(0.8)) - - } - }) .navigationDestination(isPresented: $PushToRecommandView) { RecommendView().environmentObject(dietartViewModel) } diff --git a/Dietto/Dietto/Presentation/Dietary/View/RecommendView.swift b/Dietto/Dietto/Presentation/Dietary/View/RecommendView.swift index 4a181c9..eb3ea67 100644 --- a/Dietto/Dietto/Presentation/Dietary/View/RecommendView.swift +++ b/Dietto/Dietto/Presentation/Dietary/View/RecommendView.swift @@ -80,6 +80,7 @@ struct RecommendView: View { .navigationTitle("추천 식사") .navigationBarTitleDisplayMode(.inline) .font(.pretendardBold16) + .loadingOverlay(isLoading: viewModel.isLoading) } } diff --git a/Dietto/Dietto/Presentation/Dietary/ViewModel/DietaryViewModel.swift b/Dietto/Dietto/Presentation/Dietary/ViewModel/DietaryViewModel.swift index 5040f5f..b16298d 100644 --- a/Dietto/Dietto/Presentation/Dietary/ViewModel/DietaryViewModel.swift +++ b/Dietto/Dietto/Presentation/Dietary/ViewModel/DietaryViewModel.swift @@ -8,6 +8,8 @@ import SwiftUI class DietaryViewModel: ObservableObject { + //로딩 여부 + @Published var isLoading : Bool = false //현재 @Published var presentIngredients: [IngredientEntity] = [] @@ -76,14 +78,17 @@ class DietaryViewModel: ObservableObject { //MARK: - 현재 재료를 통해 식단 추천 받기. func fetchRecommendations(ingredients: [IngredientEntity]) { + isLoading = true Task { do { let result = try await usecase.fetchRecommend(ingredients: ingredients) await MainActor.run { self.recommendList = result + isLoading = false } } catch { print(#file,#function,#line, error.localizedDescription) + isLoading = false } } } From bfe5265304203733f9b9474af5e95b37796b8b56 Mon Sep 17 00:00:00 2001 From: HISEHOONAN Date: Mon, 2 Jun 2025 10:55:54 +0900 Subject: [PATCH 2/5] [FEAT] #40 Edit animation in LoaingView --- .../Presentation/Common/LogoProgress.swift | 108 ++++++++++++++++++ .../Dietto/Presentation/Common/PillText.swift | 6 +- .../Dietary/View/DietaryView.swift | 4 +- .../Dietary/View/RecommendView.swift | 2 +- .../Dietary/ViewModel/DietaryViewModel.swift | 8 +- .../Onboarding/View/TutorialView.swift | 1 - 6 files changed, 117 insertions(+), 12 deletions(-) create mode 100644 Dietto/Dietto/Presentation/Common/LogoProgress.swift diff --git a/Dietto/Dietto/Presentation/Common/LogoProgress.swift b/Dietto/Dietto/Presentation/Common/LogoProgress.swift new file mode 100644 index 0000000..418f80f --- /dev/null +++ b/Dietto/Dietto/Presentation/Common/LogoProgress.swift @@ -0,0 +1,108 @@ +// +// ProgressViews.swift +// Dietto +// +// Created by 안세훈 on 6/2/25. +// + +import SwiftUI + +struct LogoProgress: View { + @Binding var isAnimated: Bool + var width: CGFloat = 100 + + var body: some View { + VStack { + Spacer() + ZStack { + //가려진 뷰 + VStack(spacing: 3) { + Text("Dietto") + .font(.NerkoOne40) + .frame(width: width) + .scaledToFit() + .foregroundStyle(.black.opacity(0.3)) + } + + //진행중인 뷰 + VStack(spacing: 3) { + Text("Dietto") + .font(.NerkoOne40) + .frame(width: width) + .scaledToFit() + .foregroundStyle(.appMain) + .mask { + Rectangle() + .frame(width: isAnimated ? width : 0) + .offset(x: isAnimated ? 0 : -width) + } + } + } + Spacer() + } + .frame(maxWidth: .infinity) + .background(Color.black.opacity(0.01)) + } +} +//MARK: - ViewModifer + +struct LogoProgressModifier: ViewModifier { + @Binding var isPresented: Bool + @State private var isAnimated = false + + var speed: Double = 0.3 + var delayTime: Double = 0.3 + + func body(content: Content) -> some View { + ZStack { + content + if isPresented { + Rectangle() + .fill(Color.black.opacity(0.3)) + .ignoresSafeArea() + + LogoProgress(isAnimated: $isAnimated) + .onAppear { + isAnimated = true + } + .animation( + .easeInOut + .speed(speed) + .delay(delayTime) + .repeatForever(autoreverses: false), + value: isAnimated + ) + } + } + } +} + +extension View { + func LogoProgressOverlay(isPresented: Binding) -> some View { + self.modifier(LogoProgressModifier(isPresented: isPresented)) + } +} + +//MARK: - 프리뷰 하려고 임시로 만들어놓은 것. + +#Preview { + AnimatedLoadingPreview() +} + +struct AnimatedLoadingPreview: View { + @State private var isAnimated = false + + var body: some View { + LogoProgress(isAnimated: $isAnimated) + .onAppear { + withAnimation( + .easeInOut + .speed(0.3) + .delay(0.3) + .repeatForever(autoreverses: false) + ) { + isAnimated = true + } + } + } +} diff --git a/Dietto/Dietto/Presentation/Common/PillText.swift b/Dietto/Dietto/Presentation/Common/PillText.swift index 0156672..d2d2788 100644 --- a/Dietto/Dietto/Presentation/Common/PillText.swift +++ b/Dietto/Dietto/Presentation/Common/PillText.swift @@ -13,7 +13,7 @@ struct PillText: View { var onDelete: (() -> Void)? = nil // X 버튼 클릭 액션 var body: some View { - HStack() { + HStack(spacing : 2) { Button(action: { onAdd?() }) { @@ -29,11 +29,11 @@ struct PillText: View { Image(systemName: "xmark") .font(.pretendardBold12) .foregroundStyle(.white) - .padding(4) + .padding(2) } .buttonStyle(PlainButtonStyle()) } - .padding(.horizontal, 10) + .padding(.horizontal, 8) .padding(.vertical, 4) .background(Capsule().fill(Color.appMain)) .fixedSize() diff --git a/Dietto/Dietto/Presentation/Dietary/View/DietaryView.swift b/Dietto/Dietto/Presentation/Dietary/View/DietaryView.swift index 9dce1c1..81574d2 100644 --- a/Dietto/Dietto/Presentation/Dietary/View/DietaryView.swift +++ b/Dietto/Dietto/Presentation/Dietary/View/DietaryView.swift @@ -176,9 +176,7 @@ struct DietaryView: View { } .padding(.horizontal, 16) .padding(.vertical, 40) - - Spacer() - + } } .navigationDestination(isPresented: $PushToRecommandView) { diff --git a/Dietto/Dietto/Presentation/Dietary/View/RecommendView.swift b/Dietto/Dietto/Presentation/Dietary/View/RecommendView.swift index eb3ea67..f47dec4 100644 --- a/Dietto/Dietto/Presentation/Dietary/View/RecommendView.swift +++ b/Dietto/Dietto/Presentation/Dietary/View/RecommendView.swift @@ -80,7 +80,7 @@ struct RecommendView: View { .navigationTitle("추천 식사") .navigationBarTitleDisplayMode(.inline) .font(.pretendardBold16) - .loadingOverlay(isLoading: viewModel.isLoading) + .LogoProgressOverlay(isPresented: $viewModel.isPresneted) } } diff --git a/Dietto/Dietto/Presentation/Dietary/ViewModel/DietaryViewModel.swift b/Dietto/Dietto/Presentation/Dietary/ViewModel/DietaryViewModel.swift index b16298d..d303c7e 100644 --- a/Dietto/Dietto/Presentation/Dietary/ViewModel/DietaryViewModel.swift +++ b/Dietto/Dietto/Presentation/Dietary/ViewModel/DietaryViewModel.swift @@ -9,7 +9,7 @@ import SwiftUI class DietaryViewModel: ObservableObject { //로딩 여부 - @Published var isLoading : Bool = false + @Published var isPresneted : Bool = false //현재 @Published var presentIngredients: [IngredientEntity] = [] @@ -78,17 +78,17 @@ class DietaryViewModel: ObservableObject { //MARK: - 현재 재료를 통해 식단 추천 받기. func fetchRecommendations(ingredients: [IngredientEntity]) { - isLoading = true + isPresneted = true Task { do { let result = try await usecase.fetchRecommend(ingredients: ingredients) await MainActor.run { self.recommendList = result - isLoading = false + isPresneted = false } } catch { print(#file,#function,#line, error.localizedDescription) - isLoading = false + isPresneted = false } } } diff --git a/Dietto/Dietto/Presentation/Onboarding/View/TutorialView.swift b/Dietto/Dietto/Presentation/Onboarding/View/TutorialView.swift index c60368f..bc20ecc 100644 --- a/Dietto/Dietto/Presentation/Onboarding/View/TutorialView.swift +++ b/Dietto/Dietto/Presentation/Onboarding/View/TutorialView.swift @@ -13,7 +13,6 @@ struct TutorialView: View { @StateObject var viewModel : OnboardingViewModel @Environment(\.dismiss) private var dismiss - var body: some View { ZStack { Color(.backGround).ignoresSafeArea(edges: .all) From 6de6c4af2e56b311f7b80b1624d27edf4e44eade Mon Sep 17 00:00:00 2001 From: HISEHOONAN Date: Wed, 4 Jun 2025 09:42:51 +0900 Subject: [PATCH 3/5] [FEAT] #40 Add Toast Message --- .../Data/Repository/NetworkRepository.swift | 2 +- .../Dietto/Domain/Usecases/AlanUsecase.swift | 2 +- .../Article/ViewModel/ArticleViewModel.swift | 1 - .../Common/LoadingIndicator.swift | 39 ---- Dietto/Dietto/Presentation/Common/Toast.swift | 170 ++++++++++++++++++ .../Dietary/View/DietaryView.swift | 7 +- .../Dietary/View/RecommendView.swift | 2 +- .../Dietary/ViewModel/DietaryViewModel.swift | 3 - 8 files changed, 174 insertions(+), 52 deletions(-) delete mode 100644 Dietto/Dietto/Presentation/Common/LoadingIndicator.swift create mode 100644 Dietto/Dietto/Presentation/Common/Toast.swift diff --git a/Dietto/Dietto/Data/Repository/NetworkRepository.swift b/Dietto/Dietto/Data/Repository/NetworkRepository.swift index 336037c..3b88424 100644 --- a/Dietto/Dietto/Data/Repository/NetworkRepository.swift +++ b/Dietto/Dietto/Data/Repository/NetworkRepository.swift @@ -7,7 +7,7 @@ import Foundation -//MARK: - Alan Protocol +//MARK: - Alan Interface protocol NetworkRepository { func fetch(promptType: PromptType, rawValues: [Any], outputType: T.Type) async throws -> T} diff --git a/Dietto/Dietto/Domain/Usecases/AlanUsecase.swift b/Dietto/Dietto/Domain/Usecases/AlanUsecase.swift index e237769..e5db06f 100644 --- a/Dietto/Dietto/Domain/Usecases/AlanUsecase.swift +++ b/Dietto/Dietto/Domain/Usecases/AlanUsecase.swift @@ -7,7 +7,7 @@ import Foundation -//MARK: - Usecase +//MARK: - Interface protocol AlanUsecase { func fetchRecommend(ingredients: [IngredientEntity]) async throws -> [RecommendEntity] diff --git a/Dietto/Dietto/Presentation/Article/ViewModel/ArticleViewModel.swift b/Dietto/Dietto/Presentation/Article/ViewModel/ArticleViewModel.swift index ca7b8c6..6a2a4d3 100644 --- a/Dietto/Dietto/Presentation/Article/ViewModel/ArticleViewModel.swift +++ b/Dietto/Dietto/Presentation/Article/ViewModel/ArticleViewModel.swift @@ -65,6 +65,5 @@ final class ArticleViewModel: ObservableObject { addInterest(title) storageUsecase.insertInterests(InterestEntity(title: title)) } - print(selectedInterests) } } diff --git a/Dietto/Dietto/Presentation/Common/LoadingIndicator.swift b/Dietto/Dietto/Presentation/Common/LoadingIndicator.swift deleted file mode 100644 index 7d698c6..0000000 --- a/Dietto/Dietto/Presentation/Common/LoadingIndicator.swift +++ /dev/null @@ -1,39 +0,0 @@ -// -// LoadingIndicator.swift -// Dietto -// -// Created by 안세훈 on 6/1/25. -// - -import SwiftUI - -struct LoadingViewModifier: ViewModifier { - let isLoading: Bool - var tint: Color = .white - var scale: CGFloat = 1.5 - var overlayColor: Color = Color.white.opacity(0.3) - - func body(content: Content) -> some View { - ZStack { - content - if isLoading { - overlayColor.ignoresSafeArea() - ProgressView().progressViewStyle(CircularProgressViewStyle(tint: .appMain)) - .scaleEffect(1.5) - .padding() - } - } - } -} - - -extension View { - func loadingOverlay(isLoading: Bool, tint: Color = .appMain, scale: CGFloat = 1.5, overlayColor: Color = Color.black.opacity(0.15)) -> some View { - self.modifier(LoadingViewModifier(isLoading: isLoading, tint: tint, scale: scale, overlayColor: overlayColor)) - } -} - -#Preview { - ZStack { - }.loadingOverlay(isLoading: true) -} diff --git a/Dietto/Dietto/Presentation/Common/Toast.swift b/Dietto/Dietto/Presentation/Common/Toast.swift new file mode 100644 index 0000000..731021e --- /dev/null +++ b/Dietto/Dietto/Presentation/Common/Toast.swift @@ -0,0 +1,170 @@ +// +// Toast.swift +// Dietto +// +// Created by 안세훈 on 6/3/25. +// + +import SwiftUI + +enum ToastStyle { + case error + case warning + case success + case info +} + +extension ToastStyle { + var themeColor: Color { + switch self { + case .error: return Color.red + case .warning: return Color.orange + case .info: return Color.blue + case .success: return Color.appMain + } + } + + var iconFileName: String { + switch self { + case .info: return "info.circle.fill" + case .warning: return "exclamationmark.triangle.fill" + case .success: return "checkmark.circle.fill" + case .error: return "xmark.circle.fill" + } + } +} + +struct Toast: Equatable { + var type: ToastStyle + var title: String + var message: String + var duration: Double = 3 +} + +struct ToastView: View { + var type: ToastStyle + var title: String + var message: String + var onCancelTapped: (() -> Void) + var body: some View { + VStack(alignment: .leading) { + HStack(alignment: .top) { + Image(systemName: type.iconFileName) + .foregroundColor(type.themeColor) + + VStack(alignment: .leading) { + Text(title) + .font(.system(size: 14, weight: .semibold)) + + Text(message) + .font(.system(size: 12)) + .foregroundColor(Color.black.opacity(0.6)) + } + + Spacer(minLength: 10) + + Button { + onCancelTapped() + } label: { + Image(systemName: "xmark") + .foregroundColor(Color.black) + } + } + .padding() + } + .background(Color.white) + .overlay( + Rectangle() + .fill(type.themeColor) + .frame(width: 6) + .clipped() + , alignment: .leading + ) + .frame(minWidth: 0, maxWidth: .infinity) + .cornerRadius(8) + .shadow(color: Color.black.opacity(0.25), radius: 4, x: 0, y: 1) + .padding(.horizontal, 16) + } +} + +struct ToastModifier: ViewModifier { + @Binding var toast: Toast? + @State private var workItem: DispatchWorkItem? + + func body(content: Content) -> some View { + content + .frame(maxWidth: .infinity, maxHeight: .infinity) + .overlay( + ZStack { + mainToastView() + .offset(y: -30) + }.animation(.spring(), value: toast) + ) + + .onChange(of: toast){ + showToast() + } + } + + @ViewBuilder func mainToastView() -> some View { + if let toast = toast { + VStack { + Spacer() + ToastView( + type: toast.type, + title: toast.title, + message: toast.message) { + dismissToast() + } + } + .transition(.move(edge: .bottom)) + } + } + + private func showToast() { + guard let toast = toast else { return } + + UIImpactFeedbackGenerator(style: .light).impactOccurred() + + if toast.duration > 0 { + workItem?.cancel() + + let task = DispatchWorkItem { + dismissToast() + } + workItem = task + DispatchQueue.main.asyncAfter(deadline: .now() + toast.duration, execute: task) + } + } + + private func dismissToast() { + withAnimation { + toast = nil + } + + workItem?.cancel() + workItem = nil + } +} + +extension View { + func toastView(toast: Binding) -> some View { + self.modifier(ToastModifier(toast: toast)) + } +} + + +#Preview { + ToastView(type: .error, title: "에러", message: "에러 메세지") { + print("취소") + } + ToastView(type: .info, title: "정보표시", message: "정보표시 메세지") { + print("취소") + } + ToastView(type: .success, title: "성공", message: "성공 메세지") { + print("취소") + } + ToastView(type: .warning, title: "경고", message: "경고 메세지") { + print("취소") + } +} diff --git a/Dietto/Dietto/Presentation/Dietary/View/DietaryView.swift b/Dietto/Dietto/Presentation/Dietary/View/DietaryView.swift index 81574d2..6bda697 100644 --- a/Dietto/Dietto/Presentation/Dietary/View/DietaryView.swift +++ b/Dietto/Dietto/Presentation/Dietary/View/DietaryView.swift @@ -20,9 +20,6 @@ struct DietaryView: View { @State private var PushToRecommandView : Bool = false // 화면이동 - -#warning("상태값 관리.") - var body: some View { NavigationStack{ ZStack{ @@ -56,7 +53,6 @@ struct DietaryView: View { dietartViewModel.addpresentIngredients(newfood) newfood = "" } - } .font(.pretendardBold12) .foregroundStyle(.white) @@ -123,7 +119,6 @@ struct DietaryView: View { if !isFoldMyRefrigerlator{ FlowLayout(spacing: 4, lineSpacing: 3, contentHeight: $myRefrigerlatorflowlayout) { - ForEach(dietartViewModel.pastIngredients) { ingredient in PillText(text: ingredient.ingredient, onAdd:{ @@ -176,7 +171,7 @@ struct DietaryView: View { } .padding(.horizontal, 16) .padding(.vertical, 40) - + } } .navigationDestination(isPresented: $PushToRecommandView) { diff --git a/Dietto/Dietto/Presentation/Dietary/View/RecommendView.swift b/Dietto/Dietto/Presentation/Dietary/View/RecommendView.swift index f47dec4..a6a781e 100644 --- a/Dietto/Dietto/Presentation/Dietary/View/RecommendView.swift +++ b/Dietto/Dietto/Presentation/Dietary/View/RecommendView.swift @@ -80,7 +80,7 @@ struct RecommendView: View { .navigationTitle("추천 식사") .navigationBarTitleDisplayMode(.inline) .font(.pretendardBold16) - .LogoProgressOverlay(isPresented: $viewModel.isPresneted) + .LogoProgressOverlay(isPresented: $viewModel.isPresneted) //로딩 } } diff --git a/Dietto/Dietto/Presentation/Dietary/ViewModel/DietaryViewModel.swift b/Dietto/Dietto/Presentation/Dietary/ViewModel/DietaryViewModel.swift index d303c7e..0a77e75 100644 --- a/Dietto/Dietto/Presentation/Dietary/ViewModel/DietaryViewModel.swift +++ b/Dietto/Dietto/Presentation/Dietary/ViewModel/DietaryViewModel.swift @@ -94,6 +94,3 @@ class DietaryViewModel: ObservableObject { } } - -// Thread.isMainThread -// MainActor.preconditionIsolated() //메인엑터 확인. From eba0798c6a79f7986c2669346d54009986149d1b Mon Sep 17 00:00:00 2001 From: HISEHOONAN Date: Wed, 4 Jun 2025 13:50:07 +0900 Subject: [PATCH 4/5] [FEAT] #40 Error fix Toast, Logo ProgressView --- Dietto/Dietto/Data/Network/NetworkError.swift | 15 +++++++- .../Data/Repository/NetworkRepository.swift | 2 +- .../Dietto/Domain/Usecases/AlanUsecase.swift | 2 - Dietto/Dietto/Extensions/Font+.swift | 1 + .../Presentation/Common/ContainerView.swift | 2 +- .../Presentation/Common/LogoProgress.swift | 35 +++++++++++++----- .../Dietary/View/RecommendView.swift | 14 ++++--- .../Dietary/ViewModel/DietaryViewModel.swift | 37 +++++++++++++------ 8 files changed, 76 insertions(+), 32 deletions(-) diff --git a/Dietto/Dietto/Data/Network/NetworkError.swift b/Dietto/Dietto/Data/Network/NetworkError.swift index 8ab133a..08641c2 100644 --- a/Dietto/Dietto/Data/Network/NetworkError.swift +++ b/Dietto/Dietto/Data/Network/NetworkError.swift @@ -33,14 +33,25 @@ public enum NetworkError: LocalizedError { return "서버 오류가 발생했습니다. (\(code))" case .invalidResponse: return "서버 응답이 유효하지 않습니다." - case .decodingFailed(let error): + case .decodingFailed( _): return "잠시 후 다시 시도해주세요." case .requestCancelled: return "요청이 취소되었습니다." - case .unknown(let error): + case .unknown(let _): return "알 수 없는 에러가 발생했습니다. 관리자에게 문의하세요." case .unacceptableStatusCode: return "에러가 발생했습니다. 잠시후 다시 시도하세요." } } } + +extension NetworkError { + var asToast: Toast { + return Toast( + type: .error, + title: "오류 발생", + message: self.errorDescription ?? "알 수 없는 오류입니다.", + duration: 3 + ) + } +} diff --git a/Dietto/Dietto/Data/Repository/NetworkRepository.swift b/Dietto/Dietto/Data/Repository/NetworkRepository.swift index 3b88424..cbe02ba 100644 --- a/Dietto/Dietto/Data/Repository/NetworkRepository.swift +++ b/Dietto/Dietto/Data/Repository/NetworkRepository.swift @@ -11,7 +11,7 @@ import Foundation protocol NetworkRepository { func fetch(promptType: PromptType, rawValues: [Any], outputType: T.Type) async throws -> T} -//MARK: - Alan Protocol Implement +//MARK: - Alan Implement final class NetworkRepositoryImpl: NetworkRepository { private let promptManager: PromptManager diff --git a/Dietto/Dietto/Domain/Usecases/AlanUsecase.swift b/Dietto/Dietto/Domain/Usecases/AlanUsecase.swift index e5db06f..10a9752 100644 --- a/Dietto/Dietto/Domain/Usecases/AlanUsecase.swift +++ b/Dietto/Dietto/Domain/Usecases/AlanUsecase.swift @@ -26,14 +26,12 @@ final class AlanUsecaseImpl : AlanUsecase { //추천식단 func fetchRecommend(ingredients: [IngredientEntity]) async throws -> [RecommendEntity] { let wrapper = try await repository.fetch(promptType: .recommend, rawValues: ingredients, outputType: RecommendDTO.self) - return wrapper.recommendation } //아티클 func fetchArticle(topics: [InterestEntity]) async throws -> [ArticleEntity] { let wrapper = try await repository.fetch(promptType: .article, rawValues: topics, outputType: ArticleDTO.self) - return wrapper.articles } diff --git a/Dietto/Dietto/Extensions/Font+.swift b/Dietto/Dietto/Extensions/Font+.swift index 9465672..90d6a40 100644 --- a/Dietto/Dietto/Extensions/Font+.swift +++ b/Dietto/Dietto/Extensions/Font+.swift @@ -11,6 +11,7 @@ import SwiftUI extension Font { static let AppLogo: Font = .custom("NerkoOne-regular", size: 32) static let NerkoOne40: Font = .custom("NerkoOne-regular", size: 40) + static let NerkoOne80: Font = .custom("NerkoOne-regular", size: 80) static let pretendardBlack32: Font = .custom("Pretendard-Black", size: 32) static let pretendardBlack28: Font = .custom("Pretendard-Black", size: 28) diff --git a/Dietto/Dietto/Presentation/Common/ContainerView.swift b/Dietto/Dietto/Presentation/Common/ContainerView.swift index a41ae19..f6f44d3 100644 --- a/Dietto/Dietto/Presentation/Common/ContainerView.swift +++ b/Dietto/Dietto/Presentation/Common/ContainerView.swift @@ -10,7 +10,7 @@ import SwiftUI struct ContainerView: View { var paddingSize: CGFloat var height: CGFloat - let content: () -> Content // ✅ 변수명은 소문자! + let content: () -> Content init(paddingSize: CGFloat, height: CGFloat, @ViewBuilder content: @escaping () -> Content) { self.paddingSize = paddingSize diff --git a/Dietto/Dietto/Presentation/Common/LogoProgress.swift b/Dietto/Dietto/Presentation/Common/LogoProgress.swift index 418f80f..0e85ed7 100644 --- a/Dietto/Dietto/Presentation/Common/LogoProgress.swift +++ b/Dietto/Dietto/Presentation/Common/LogoProgress.swift @@ -9,7 +9,8 @@ import SwiftUI struct LogoProgress: View { @Binding var isAnimated: Bool - var width: CGFloat = 100 + var width: CGFloat = 190 + var message : String var body: some View { VStack { @@ -18,16 +19,30 @@ struct LogoProgress: View { //가려진 뷰 VStack(spacing: 3) { Text("Dietto") - .font(.NerkoOne40) + .font(.NerkoOne80) + .frame(width: width) + .scaledToFit() + .foregroundStyle(.black.opacity(0.3)) + Text(message) + .font(.pretendardBlack12) .frame(width: width) .scaledToFit() .foregroundStyle(.black.opacity(0.3)) } - //진행중인 뷰 VStack(spacing: 3) { Text("Dietto") - .font(.NerkoOne40) + .font(.NerkoOne80) + .frame(width: width) + .scaledToFit() + .foregroundStyle(.appMain) + .mask { + Rectangle() + .frame(width: isAnimated ? width : 0) + .offset(x: isAnimated ? 0 : -width) + } + Text(message) + .font(.pretendardBlack12) .frame(width: width) .scaledToFit() .foregroundStyle(.appMain) @@ -41,7 +56,7 @@ struct LogoProgress: View { Spacer() } .frame(maxWidth: .infinity) - .background(Color.black.opacity(0.01)) + .background(Color.black.opacity(0.25)) } } //MARK: - ViewModifer @@ -49,6 +64,7 @@ struct LogoProgress: View { struct LogoProgressModifier: ViewModifier { @Binding var isPresented: Bool @State private var isAnimated = false + @State var message: String = "TEST" var speed: Double = 0.3 var delayTime: Double = 0.3 @@ -61,7 +77,7 @@ struct LogoProgressModifier: ViewModifier { .fill(Color.black.opacity(0.3)) .ignoresSafeArea() - LogoProgress(isAnimated: $isAnimated) + LogoProgress(isAnimated: $isAnimated, message: message) .onAppear { isAnimated = true } @@ -78,8 +94,8 @@ struct LogoProgressModifier: ViewModifier { } extension View { - func LogoProgressOverlay(isPresented: Binding) -> some View { - self.modifier(LogoProgressModifier(isPresented: isPresented)) + func LogoProgressOverlay(isPresented: Binding, message: String = "") -> some View { + self.modifier(LogoProgressModifier(isPresented: isPresented, message: message)) } } @@ -91,9 +107,10 @@ extension View { struct AnimatedLoadingPreview: View { @State private var isAnimated = false + @State var message = "카리나카리나카리나카리나카리나" var body: some View { - LogoProgress(isAnimated: $isAnimated) + LogoProgress(isAnimated: $isAnimated, message: message) .onAppear { withAnimation( .easeInOut diff --git a/Dietto/Dietto/Presentation/Dietary/View/RecommendView.swift b/Dietto/Dietto/Presentation/Dietary/View/RecommendView.swift index a6a781e..3495616 100644 --- a/Dietto/Dietto/Presentation/Dietary/View/RecommendView.swift +++ b/Dietto/Dietto/Presentation/Dietary/View/RecommendView.swift @@ -80,12 +80,14 @@ struct RecommendView: View { .navigationTitle("추천 식사") .navigationBarTitleDisplayMode(.inline) .font(.pretendardBold16) - .LogoProgressOverlay(isPresented: $viewModel.isPresneted) //로딩 + .LogoProgressOverlay(isPresented: $viewModel.isPresneted,message: "Alan이 식단을 생성하고 있어요 !") //로딩 + .toastView(toast: $viewModel.toast) } } -#Preview { - NavigationStack{ - RecommendView() - } -} +//디버깅용 +//#Preview { +// NavigationStack{ +// RecommendView() +// } +//} diff --git a/Dietto/Dietto/Presentation/Dietary/ViewModel/DietaryViewModel.swift b/Dietto/Dietto/Presentation/Dietary/ViewModel/DietaryViewModel.swift index 0a77e75..5952352 100644 --- a/Dietto/Dietto/Presentation/Dietary/ViewModel/DietaryViewModel.swift +++ b/Dietto/Dietto/Presentation/Dietary/ViewModel/DietaryViewModel.swift @@ -8,13 +8,9 @@ import SwiftUI class DietaryViewModel: ObservableObject { - //로딩 여부 - @Published var isPresneted : Bool = false - - //현재 - @Published var presentIngredients: [IngredientEntity] = [] - - //과거 + @Published var isPresneted : Bool = false //로딩 여부 + @Published var toast: Toast? //toast팝업 + @Published var presentIngredients: [IngredientEntity] = [] //현재 @Published var pastIngredients : [IngredientEntity] = [ IngredientEntity(ingredient: "오징어"), IngredientEntity(ingredient: "꼴뚜기"), @@ -78,19 +74,38 @@ class DietaryViewModel: ObservableObject { //MARK: - 현재 재료를 통해 식단 추천 받기. func fetchRecommendations(ingredients: [IngredientEntity]) { + isPresneted = true + Task { do { let result = try await usecase.fetchRecommend(ingredients: ingredients) await MainActor.run { self.recommendList = result + self.toast = Toast( + type: .success, + title: "완료", + message: "식단 추천을 완료하였습니다.", + duration: 2 + ) + isPresneted = false + } + } catch let error as NetworkError { + await MainActor.run { + self.toast = error.asToast + isPresneted = false + } + }catch { + await MainActor.run { + self.toast = Toast( + type: .error, + title: "에러", + message: error.localizedDescription + ) isPresneted = false } - } catch { - print(#file,#function,#line, error.localizedDescription) - isPresneted = false } } + } - } From 76a222d333ff338b2141021b3174dfff76fc89c6 Mon Sep 17 00:00:00 2001 From: HISEHOONAN Date: Wed, 4 Jun 2025 14:11:02 +0900 Subject: [PATCH 5/5] [FEAT] #40 Add ToastEntity, Divide Codes --- Dietto/Dietto/Data/Network/NetworkError.swift | 4 +- .../Dietto/Domain/Entities/ToastEntity.swift | 15 ++++++ .../Presentation/Common/LogoProgress.swift | 53 +++++++++---------- .../Dietto/Presentation/Common/SubViews.swift | 20 +++++++ Dietto/Dietto/Presentation/Common/Toast.swift | 15 +----- .../Dietary/View/RecommendView.swift | 2 +- .../Dietary/ViewModel/DietaryViewModel.swift | 6 +-- 7 files changed, 66 insertions(+), 49 deletions(-) create mode 100644 Dietto/Dietto/Domain/Entities/ToastEntity.swift create mode 100644 Dietto/Dietto/Presentation/Common/SubViews.swift diff --git a/Dietto/Dietto/Data/Network/NetworkError.swift b/Dietto/Dietto/Data/Network/NetworkError.swift index 08641c2..68aab1c 100644 --- a/Dietto/Dietto/Data/Network/NetworkError.swift +++ b/Dietto/Dietto/Data/Network/NetworkError.swift @@ -46,8 +46,8 @@ public enum NetworkError: LocalizedError { } extension NetworkError { - var asToast: Toast { - return Toast( + var asToast: ToastEntity { + return ToastEntity( type: .error, title: "오류 발생", message: self.errorDescription ?? "알 수 없는 오류입니다.", diff --git a/Dietto/Dietto/Domain/Entities/ToastEntity.swift b/Dietto/Dietto/Domain/Entities/ToastEntity.swift new file mode 100644 index 0000000..07c986c --- /dev/null +++ b/Dietto/Dietto/Domain/Entities/ToastEntity.swift @@ -0,0 +1,15 @@ +// +// ToastEntity.swift +// Dietto +// +// Created by 안세훈 on 6/4/25. +// + +import Foundation + +struct ToastEntity: Equatable { + var type: ToastStyle + var title: String + var message: String + var duration: Double = 3 +} diff --git a/Dietto/Dietto/Presentation/Common/LogoProgress.swift b/Dietto/Dietto/Presentation/Common/LogoProgress.swift index 0e85ed7..983e809 100644 --- a/Dietto/Dietto/Presentation/Common/LogoProgress.swift +++ b/Dietto/Dietto/Presentation/Common/LogoProgress.swift @@ -93,33 +93,28 @@ struct LogoProgressModifier: ViewModifier { } } -extension View { - func LogoProgressOverlay(isPresented: Binding, message: String = "") -> some View { - self.modifier(LogoProgressModifier(isPresented: isPresented, message: message)) - } -} -//MARK: - 프리뷰 하려고 임시로 만들어놓은 것. - -#Preview { - AnimatedLoadingPreview() -} - -struct AnimatedLoadingPreview: View { - @State private var isAnimated = false - @State var message = "카리나카리나카리나카리나카리나" - - var body: some View { - LogoProgress(isAnimated: $isAnimated, message: message) - .onAppear { - withAnimation( - .easeInOut - .speed(0.3) - .delay(0.3) - .repeatForever(autoreverses: false) - ) { - isAnimated = true - } - } - } -} +////MARK: - 프리뷰 하려고 임시로 만들어놓은 것. +// +//#Preview { +// AnimatedLoadingPreview() +//} +// +//struct AnimatedLoadingPreview: View { +// @State private var isAnimated = false +// @State var message = "카리나카리나카리나카리나카리나" +// +// var body: some View { +// LogoProgress(isAnimated: $isAnimated, message: message) +// .onAppear { +// withAnimation( +// .easeInOut +// .speed(0.3) +// .delay(0.3) +// .repeatForever(autoreverses: false) +// ) { +// isAnimated = true +// } +// } +// } +//} diff --git a/Dietto/Dietto/Presentation/Common/SubViews.swift b/Dietto/Dietto/Presentation/Common/SubViews.swift new file mode 100644 index 0000000..ec46a78 --- /dev/null +++ b/Dietto/Dietto/Presentation/Common/SubViews.swift @@ -0,0 +1,20 @@ +// +// SubView.swift +// Dietto +// +// Created by 안세훈 on 6/4/25. +// + +import SwiftUI + +extension View { + //MARK: - ProgressView Modifer + func progressOverlay(isPresented: Binding, message: String = "") -> some View { + self.modifier(LogoProgressModifier(isPresented: isPresented, message: message)) + } + + //MARK: - ToastView Modifer + func toastView(toast: Binding) -> some View { + self.modifier(ToastModifier(toast: toast)) + } +} diff --git a/Dietto/Dietto/Presentation/Common/Toast.swift b/Dietto/Dietto/Presentation/Common/Toast.swift index 731021e..7e0dffe 100644 --- a/Dietto/Dietto/Presentation/Common/Toast.swift +++ b/Dietto/Dietto/Presentation/Common/Toast.swift @@ -34,13 +34,6 @@ extension ToastStyle { } } -struct Toast: Equatable { - var type: ToastStyle - var title: String - var message: String - var duration: Double = 3 -} - struct ToastView: View { var type: ToastStyle var title: String @@ -88,7 +81,7 @@ struct ToastView: View { } struct ToastModifier: ViewModifier { - @Binding var toast: Toast? + @Binding var toast: ToastEntity? @State private var workItem: DispatchWorkItem? func body(content: Content) -> some View { @@ -147,12 +140,6 @@ struct ToastModifier: ViewModifier { } } -extension View { - func toastView(toast: Binding) -> some View { - self.modifier(ToastModifier(toast: toast)) - } -} - #Preview { ToastView(type: .error, title: "에러", message: "에러 메세지") { diff --git a/Dietto/Dietto/Presentation/Dietary/View/RecommendView.swift b/Dietto/Dietto/Presentation/Dietary/View/RecommendView.swift index 3495616..76940b6 100644 --- a/Dietto/Dietto/Presentation/Dietary/View/RecommendView.swift +++ b/Dietto/Dietto/Presentation/Dietary/View/RecommendView.swift @@ -80,7 +80,7 @@ struct RecommendView: View { .navigationTitle("추천 식사") .navigationBarTitleDisplayMode(.inline) .font(.pretendardBold16) - .LogoProgressOverlay(isPresented: $viewModel.isPresneted,message: "Alan이 식단을 생성하고 있어요 !") //로딩 + .progressOverlay(isPresented: $viewModel.isPresneted,message: "Alan이 식단을 생성하고 있어요 !") //로딩 .toastView(toast: $viewModel.toast) } } diff --git a/Dietto/Dietto/Presentation/Dietary/ViewModel/DietaryViewModel.swift b/Dietto/Dietto/Presentation/Dietary/ViewModel/DietaryViewModel.swift index 5952352..d0e7ccb 100644 --- a/Dietto/Dietto/Presentation/Dietary/ViewModel/DietaryViewModel.swift +++ b/Dietto/Dietto/Presentation/Dietary/ViewModel/DietaryViewModel.swift @@ -9,7 +9,7 @@ import SwiftUI class DietaryViewModel: ObservableObject { @Published var isPresneted : Bool = false //로딩 여부 - @Published var toast: Toast? //toast팝업 + @Published var toast: ToastEntity? //toast팝업 @Published var presentIngredients: [IngredientEntity] = [] //현재 @Published var pastIngredients : [IngredientEntity] = [ IngredientEntity(ingredient: "오징어"), @@ -82,7 +82,7 @@ class DietaryViewModel: ObservableObject { let result = try await usecase.fetchRecommend(ingredients: ingredients) await MainActor.run { self.recommendList = result - self.toast = Toast( + self.toast = ToastEntity( type: .success, title: "완료", message: "식단 추천을 완료하였습니다.", @@ -97,7 +97,7 @@ class DietaryViewModel: ObservableObject { } }catch { await MainActor.run { - self.toast = Toast( + self.toast = ToastEntity( type: .error, title: "에러", message: error.localizedDescription