Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[TNT-221] API 포함 결과 처리 관련 오버레이 UI 로직 작성 #65

Merged
merged 8 commits into from
Feb 10, 2025
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
//
// ProgressIndicator.swift
// Data
//
// Created by 박민서 on 2/9/25.
// Copyright © 2025 yapp25thTeamTnT. All rights reserved.
//

import Foundation

import Domain

struct ProgressIndicatorInterceptor: Interceptor {
let priority: InterceptorPriority = .normal

func adapt(request: URLRequest) async throws -> URLRequest {
NotificationCenter.default.postProgress(visible: true)
return request
}

func validate(response: URLResponse, data: Data) async throws {
NotificationCenter.default.postProgress(visible: false)
}
}
24 changes: 15 additions & 9 deletions TnT/Projects/Data/Sources/Network/NetworkService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -32,15 +32,21 @@ public final class NetworkService {
decodingType: T.Type
) async throws -> T {
let pipeline: InterceptorPipeline = InterceptorPipeline(interceptors: target.interceptors)
// URL Request 생성
var request: URLRequest = try buildRequest(from: target)
request = try await pipeline.adapt(request)

// Request 수행
let data: Data = try await executeRequest(request, pipeline: pipeline)

// Data 디코딩
return try decodeData(data, as: decodingType)
do {
// URL Request 생성
var request: URLRequest = try buildRequest(from: target)
request = try await pipeline.adapt(request)

// Request 수행
let data: Data = try await executeRequest(request, pipeline: pipeline)

// Data 디코딩
return try decodeData(data, as: decodingType)
} catch {
// TODO: 추후 인터셉터 리팩토링 시 error middleWare로 분리
NotificationCenter.default.post(toast: .init(presentType: .text("⚠"), message: "서버 요청에 실패했어요"))
throw error
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,9 +54,9 @@ extension TraineeTargetType: TargetType {
return [
LoggingInterceptor(),
AuthTokenInterceptor(),
ProgressIndicatorInterceptor(),
ResponseValidatorInterceptor(),
RetryInterceptor(maxRetryCount: 0)
]
}
}

Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ extension TrainerTargetType: TargetType {
return [
LoggingInterceptor(),
AuthTokenInterceptor(),
ProgressIndicatorInterceptor(),
ResponseValidatorInterceptor(),
RetryInterceptor(maxRetryCount: 0)
]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -100,13 +100,15 @@ extension UserTargetType: TargetType {
return [
LoggingInterceptor(),
AuthTokenInterceptor(),
ProgressIndicatorInterceptor(),
ResponseValidatorInterceptor(),
RetryInterceptor(maxRetryCount: 2)
]
default:
return [
LoggingInterceptor(),
ResponseValidatorInterceptor(),
ProgressIndicatorInterceptor(),
RetryInterceptor(maxRetryCount: 2)
]
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ public struct TPopUpModifier<InnerContent: View>: ViewModifier {
private let defaultInnerPadding: CGFloat = 20
/// 팝업 배경의 기본 불투명도
/// 0.0 = 완전 투명, 1.0 = 완전 불투명
private let defaultBackgroundOpacity: Double = 0.8
private let defaultBackgroundOpacity: Double = 0.5
/// 팝업의 기본 배경 색상
private let defaultPopUpBackgroundColor: Color = .white
/// 팝업 모서리의 기본 곡률 (Corner Radius)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -83,13 +83,13 @@ public extension View {
func tToast<LeftView: View>(
isPresented: Binding<Bool>,
message: String,
leftView: @escaping () -> LeftView
leftViewType: TToastView.LeftViewType
) -> some View {
self.modifier(
TToastViewModifier(
isPresented: isPresented,
newContent: {
TToastView(message: message, leftView: leftView)
TToastView(message: message, leftViewType: leftViewType)
}
)
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,27 +10,27 @@ import SwiftUI

/// 앱 전반적으로 사용되는 토스트 메시지 뷰
/// - 짧은 시간 동안 하단에 나타났다가 사라지는 UI 컴포넌트.
public struct TToastView<LeftView: View>: View {
public struct TToastView: View {
/// 토스트 메세지
private let message: String
/// 토스트 좌측 뷰
private let leftView: () -> LeftView
private let leftViewType: LeftViewType

/// TToastView 초기화 메서드
/// - Parameters:
/// - message: 표시할 메시지
/// - leftView: 좌측 아이콘 또는 커스텀 뷰를 반환하는 클로저
public init(message: String, leftView: @escaping () -> LeftView) {
public init(message: String, leftViewType: LeftViewType) {
self.message = message
self.leftView = leftView
self.leftViewType = leftViewType
}

public var body: some View {
VStack {
Spacer()

HStack(spacing: 8) {
leftView()
LeftView(presentType: leftViewType)

Text(message)
.typographyStyle(.label1Medium, with: .neutral50)
Expand All @@ -45,3 +45,36 @@ public struct TToastView<LeftView: View>: View {
}
}
}

public extension TToastView {
/// 좌측 뷰로 표시될 내용
enum LeftViewType {
case text(String)
case image(ImageResource)
case processing
case none
}

/// 토스트 내용 좌측 뷰
struct LeftView: View {
let presentType: LeftViewType

public var body: some View {
switch presentType {
case .text(let text):
Text(text)
.typographyStyle(.body1Bold, with: .neutral50)
case .image(let imageSource):
Image(imageSource)
.resizable()
.frame(width: 24, height: 24)
case .processing:
ProgressView()
.tint(.red500)
.frame(width: 24, height: 24)
case .none:
EmptyView()
}
}
}
}
39 changes: 39 additions & 0 deletions TnT/Projects/Domain/Sources/Entity/Toast.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
//
// Toast.swift
// Domain
//
// Created by 박민서 on 2/9/25.
// Copyright © 2025 yapp25thTeamTnT. All rights reserved.
//

import SwiftUI

/// 개별 토스트 데이터 (좌측 뷰 타입, 메시지, 지속시간, 탭 시 해제 가능 여부)
public struct Toast: Identifiable, Equatable {
public let id = UUID()
public let presentType: PresentType
public let message: String
public let duration: TimeInterval
public let dismissibleOnTap: Bool

public init(
presentType: PresentType,
message: String,
duration: TimeInterval = 2.0,
dismissibleOnTap: Bool = true
) {
self.presentType = presentType
self.message = message
self.duration = duration
self.dismissibleOnTap = dismissibleOnTap
}
}

public extension Toast {
enum PresentType: Equatable {
case text(String)
case image(ImageResource)
case processing
case none
}
}
46 changes: 46 additions & 0 deletions TnT/Projects/Domain/Sources/Extension/Notification+.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
//
// Notification+.swift
// Presentation
//
// Created by 박민서 on 2/9/25.
// Copyright © 2025 yapp25thTeamTnT. All rights reserved.
//

import Foundation

public extension Notification.Name {
/// 토스트를 표시하기 위한 노티피케이션 이름
static let showToastNotification = Notification.Name("ShowToastNotification")
/// 토스트를 삭제하기 위한 노티피케이션 이름
static let deleteToastNotification = Notification.Name("DeleteToastNotification")
/// ProgressView를 표시하기 위한 노티피케이션 이름
static let showProgressNotification = Notification.Name("ShowProgressNotification")
/// ProgressView를 숨기기 위한 노티피케이션 이름
static let hideProgressNotification = Notification.Name("HideProgressNotification")
}

public extension NotificationCenter {
/// 토스트를 보내는 편의 메서드
func post(toast: Toast) {
NotificationCenter.default.post(
name: .showToastNotification,
object: nil,
userInfo: ["toast": toast]
)
}

/// 토스트 삭제를 요청하는 편의 메서드
func postDelete(toast: Toast) {
NotificationCenter.default.post(
name: .deleteToastNotification,
object: nil,
userInfo: ["toast": toast]
)
}

/// ProgressView 표시 여부를 보내는 편의 메서드
func postProgress(visible: Bool) {
let name: Notification.Name = visible ? .showProgressNotification : .hideProgressNotification
self.post(name: name, object: nil)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,25 +17,30 @@ public struct AppFlowCoordinatorView: View {
}

public var body: some View {
Group {
if let userType = store.userType {
switch userType {
case .trainee:
if let store = store.scope(state: \.traineeMainState, action: \.subFeature.traineeMainFlow) {
TraineeMainFlowView(store: store)
ZStack {
Group {
if let userType = store.userType {
switch userType {
case .trainee:
if let store = store.scope(state: \.traineeMainState, action: \.subFeature.traineeMainFlow) {
TraineeMainFlowView(store: store)
}
case .trainer:
if let store = store.scope(state: \.trainerMainState, action: \.subFeature.trainerMainFlow) {
TrainerMainFlowView(store: store)
}
}
case .trainer:
if let store = store.scope(state: \.trainerMainState, action: \.subFeature.trainerMainFlow) {
TrainerMainFlowView(store: store)
} else {
if let store = store.scope(state: \.onboardingState, action: \.subFeature.onboardingFlow) {
OnboardingFlowView(store: store)
}
}
} else {
if let store = store.scope(state: \.onboardingState, action: \.subFeature.onboardingFlow) {
OnboardingFlowView(store: store)
}
}
.animation(.easeInOut, value: store.userType)

OverlayContainer()
.environmentObject(OverlayManager.shared)
}
.animation(.easeInOut, value: store.userType)
.onAppear {
store.send(.onAppear)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,21 +40,26 @@ public struct TraineeHomeFeature {
}
/// 선택 바텀 시트 표시
var view_isBottomSheetPresented: Bool
/// 팝업 표시
// TODO: 3일 동안 보지 않기 로직 작성 때 추가
var view_isPopUpPresented: Bool

public init(
selectedDate: Date = .now,
events: [Date: Int] = [:],
sessionInfo: WorkoutListItemEntity? = nil,
records: [RecordListItemEntity] = [],
view_currentPage: Date = .now,
view_isBottomSheetPresented: Bool = false
view_isBottomSheetPresented: Bool = false,
view_isPopUpPresented: Bool = false
) {
self.selectedDate = selectedDate
self.events = events
self.sessionInfo = sessionInfo
self.records = records
self.view_currentPage = view_currentPage
self.view_isBottomSheetPresented = view_isBottomSheetPresented
self.view_isPopUpPresented = view_isPopUpPresented
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,16 @@ public struct TraineeMainTabFeature {
}
}

/// 하위 Feature에서 팝업이 활성화되었는지 여부를 전달
var isPopupActive: Bool {
switch self {
case .home(let homeState):
return homeState.view_isPopUpPresented
case .myPage(let myPageState):
return myPageState.view_isPopUpPresented
}
}

public init() {
self = .home(TraineeHomeFeature.State())
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ public struct TraineeMainTabView: View {
public init(store: StoreOf<TraineeMainTabFeature>) {
self.store = store
}

public var body: some View {
VStack(spacing: 0) {
switch store.state {
Expand Down Expand Up @@ -57,5 +57,14 @@ public struct TraineeMainTabView: View {
.frame(height: 54 + .safeAreaBottom)
.padding(.horizontal, 24)
.background(Color.white.shadow(radius: 5).opacity(0.5))
.overlay(
Group {
if store.isPopupActive {
Color.black.opacity(0.5)
.transition(.opacity)
}
}
)
.animation(.easeInOut, value: store.isPopupActive)
}
}

This file was deleted.

Loading