Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ public struct GoalDetailReducer {
public var myHasEmoji: Bool { isFrontMyCard && selectedReactionEmoji != nil }
public var isShowReactionBar: Bool { !isFrontMyCard && isCompleted }
public var isLoading: Bool { item == nil }
public var isFetchFailed: Bool = false
public var isEditing: Bool = false
public var isSavingPhotoLog: Bool = false
public var pendingEditedImageData: Data?
Expand Down Expand Up @@ -145,6 +146,7 @@ public struct GoalDetailReducer {
case dimmedBackgroundTapped
case proofPhotoDismissed
case cameraPermissionAlertDismissed
case dataRetryTapped
}

// MARK: - Internal
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ extension GoalDetailReducer {
case .view(.onAppear):
let date = state.verificationDate
let goalId = state.goalId
state.isFetchFailed = false

return .run { send in
do {
Expand All @@ -106,6 +107,9 @@ extension GoalDetailReducer {

case .view(.onDisappear):
return .none

case .view(.dataRetryTapped):
return .send(.view(.onAppear))

// MARK: - Action
case .view(.bottomButtonTapped):
Expand Down Expand Up @@ -202,6 +206,7 @@ extension GoalDetailReducer {
// MARK: - State Update
case let .response(.fethedGoalDetailItem(item)):
state.item = item
state.isFetchFailed = false
if let goalIndex = state.completedGoalItems.firstIndex(where: {
$0.myPhotoLog?.goalId == state.goalId || $0.yourPhotoLog?.goalId == state.goalId
}) {
Expand All @@ -216,7 +221,8 @@ extension GoalDetailReducer {
return .none

case .response(.fetchGoalDetailFailed):
return .send(.presentation(.showToast(.warning(message: "목표 상세 조회에 실패했어요"))))
state.isFetchFailed = true
return .none

case let .response(.updateCurrentCardReaction(photoLogId: photoLogId, reaction: reaction)):
guard let item = state.item else { return .none }
Expand Down
38 changes: 22 additions & 16 deletions Projects/Feature/GoalDetail/Sources/Detail/GoalDetailView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -118,24 +118,30 @@ private extension GoalDetailView {
navigationBar
.zIndex(1)

if store.item != nil {
cardView
.padding(.horizontal, 27)
.padding(.top, Constants.cardTopPadding)

if store.isCompleted {
completedBottomContent
} else if store.currentCompletedGoal?.status != .completed {
bottomButton
.padding(.top, 105)
.overlay(alignment: .bottomLeading) {
pokeImage
.offset(x: 79, y: -45)
}
if store.isFetchFailed {
DataRetryView {
store.send(.view(.dataRetryTapped))
}
} else {
if store.item != nil {
cardView
.padding(.horizontal, 27)
.padding(.top, Constants.cardTopPadding)

if store.isCompleted {
completedBottomContent
} else if store.currentCompletedGoal?.status != .completed {
bottomButton
.padding(.top, 105)
.overlay(alignment: .bottomLeading) {
pokeImage
.offset(x: 79, y: -45)
}
}
}
}

Spacer()
Spacer()
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ public struct EditGoalListReducer {

public struct UIState: Equatable {
public var isLoading: Bool = true
public var isFetchFailed: Bool = false
public init() { }
}

Expand All @@ -73,6 +74,7 @@ public struct EditGoalListReducer {
public var modal: TXModalStyle? { get { presentation.modal } set { presentation.modal = newValue } }
public var toast: TXToastType? { get { presentation.toast } set { presentation.toast = newValue } }
public var isLoading: Bool { get { ui.isLoading } set { ui.isLoading = newValue } }
public var isFetchFailed: Bool { get { ui.isFetchFailed } set { ui.isFetchFailed = newValue } }
public var pendingGoalId: Int64? { get { data.pendingGoalId } set { data.pendingGoalId = newValue } }
public var pendingAction: PendingAction? { get { data.pendingAction } set { data.pendingAction = newValue } }
public var hasCards: Bool { !(cards?.isEmpty ?? true) }
Expand Down Expand Up @@ -109,6 +111,7 @@ public struct EditGoalListReducer {
case backgroundTapped
case modalConfirmTapped
case toastButtonTapped
case dataRetryTapped
}

// MARK: - Internal
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ public struct HomeReducer {

public struct UIState: Equatable {
public var isLoading: Bool = true
public var isFetchFailed: Bool = false
public var mainTitle: String = "KEEPILUV"
public var calendarMonthTitle: String = ""
public var isRefreshHidden: Bool = true
Expand Down Expand Up @@ -84,6 +85,10 @@ public struct HomeReducer {
get { ui.isLoading }
set { ui.isLoading = newValue }
}
public var isFetchFailed: Bool {
get { ui.isFetchFailed }
set { ui.isFetchFailed = newValue }
}
public var mainTitle: String {
get { ui.mainTitle }
set { ui.mainTitle = newValue }
Expand Down Expand Up @@ -206,6 +211,7 @@ public struct HomeReducer {
case proofPhotoDismissed
case addGoalButtonTapped(GoalCategory)
case cameraPermissionAlertDismissed
case dataRetryTapped
case perfToastShowTapped
case perfToastDismissTapped
case perfCalendarNextTapped
Expand All @@ -223,7 +229,7 @@ public struct HomeReducer {
// MARK: - Response
public enum Response {
case fetchGoalsCompleted(GoalList, date: TXCalendarDate)
case fetchGoalsFailed
case fetchGoalsFailed(date: TXCalendarDate)
case authorizationCompleted(id: Int64, isAuthorized: Bool)
case deletePhotoLogCompleted(goalId: Int64)
case deletePhotoLogFailed
Expand Down
10 changes: 10 additions & 0 deletions Projects/Feature/Home/Sources/Goal/EditGoalListReducer+Impl.swift
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,9 @@ extension EditGoalListReducer {

case .view(.toastButtonTapped):
return .send(.delegate(.goToCompletedStats))

case .view(.dataRetryTapped):
return .send(.internal(.fetchGoals))

// MARK: - Update State
case let .internal(.setCalendarDate(date)):
Expand All @@ -170,6 +173,7 @@ extension EditGoalListReducer {

case .internal(.fetchGoals):
state.isLoading = true
state.isFetchFailed = false
let date = state.calendarDate
return .run { send in
do {
Expand Down Expand Up @@ -201,6 +205,7 @@ extension EditGoalListReducer {
return .none
}
state.isLoading = false
state.isFetchFailed = false
state.editableGoals = goals
let items = goals.map {
GoalEditCardItem(
Expand Down Expand Up @@ -238,6 +243,11 @@ extension EditGoalListReducer {
state.isLoading = false
state.pendingGoalId = nil
state.pendingAction = nil

if state.cards == nil {
state.isFetchFailed = true
return .none
}
return .send(.presentation(.showToast(.warning(message: message))))

case let .presentation(.showToast(toast)):
Expand Down
24 changes: 14 additions & 10 deletions Projects/Feature/Home/Sources/Goal/EditGoalListView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21,20 +21,24 @@ struct EditGoalListView: View {
navigationBar
weekCalendar
.padding(.top, 4)
if let cards = store.cards, !cards.isEmpty {
cardScrollView
.padding(.bottom, 1)

if store.isFetchFailed {
DataRetryView {
store.send(.view(.dataRetryTapped))
}
} else if let cards = store.cards {
if cards.isEmpty {
emptyContent
} else {
cardScrollView
.padding(.bottom, 1)
}
} else {
Spacer()
}

Spacer()
}
.ignoresSafeArea(.container, edges: .bottom)
.frame(maxWidth: .infinity, maxHeight: .infinity)
.overlay {
if let cards = store.cards, cards.isEmpty {
emptyContent
}
}
.toolbar(.hidden, for: .navigationBar)
.onAppear {
store.send(.view(.onAppear))
Expand Down
13 changes: 10 additions & 3 deletions Projects/Feature/Home/Sources/Home/HomeReducer+Impl.swift
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,9 @@ extension HomeReducer {
case .view(.refreshPulled):
return .send(.internal(.fetchGoals))

case .view(.dataRetryTapped):
return .send(.internal(.fetchGoals))

case .view(.perfToastShowTapped):
return .send(.presentation(.showToast(.warning(message: "perf-toast"))))

Expand Down Expand Up @@ -335,15 +338,18 @@ extension HomeReducer {
}

state.isLoading = false
state.isFetchFailed = false

if state.items != items {
state.items = items
}
return .none

case .response(.fetchGoalsFailed):
case let .response(.fetchGoalsFailed(date)):
guard date == state.calendarDate else { return .none }
state.isLoading = false
return .send(.presentation(.showToast(.warning(message: "목표 조회에 실패했어요"))))
state.isFetchFailed = true
return .none
Comment on lines -344 to +352

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

여기는 조금 어려운데, 캐시 데이터가 있는 경우에도 refresh가 실패하면 state.isFetchFailed가 true가 되어서
로딩 인디케이터가 화면을 꽉 덮을 거 같은데 어떻게 해결해야 할 지 잘 모르겠네... 토스트를 띄우면 되려나?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

그러게 지금 txDataRetry 구조가 overlay로 띄우는거라 캐시된 데이터랑 재시도뷰가 겹처 보이네
그래서 overlay로 덮는 구조로 안하고 재시도가 필요한 상황일 때 기존 콘텐츠가 아닌 DataRetryView 보여주면 안겹치게 할 수 있을 거 같은데 어떻게 생각해?

as-is

.txDataRetry(
    isPresented: store.isFetchFailed,
    onRetry: { store.send(.view(.dataRetryTapped)) }
)
5668E0FB-FA44-4D2F-B3FD-FD446F5961B9_1_102_o

to-be

if store.isFetchFailed {
    DataRetryView {
        store.send(.view(.dataRetryTapped))
    }
} else if store.hasCards {
    HomeContentSection(store: store)
} else if store.isEmptyVisible {
    HomeEmptyContentSection(store: store)
}
BD2452A3-357F-4318-8B21-3F58ED0EB21E_1_102_o

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

맞아 overlay로 하면 화면이 겹쳐서 어색할 거 같다
to-be 방향으로 필요에 따라 DataRetryView를 보여주는 게 더 좋은 거 같아 !!
반영하고 머지하자 !!


case let .internal(.setCalendarDate(date)):
guard date != state.calendarDate else { return .none }
Expand Down Expand Up @@ -384,6 +390,7 @@ extension HomeReducer {
} else {
state.isLoading = true
}
state.isFetchFailed = false
return .run { send in
// 읽지 않은 알림 여부 체크
if let hasUnread = try? await notificationClient.fetchUnread() {
Expand All @@ -394,7 +401,7 @@ extension HomeReducer {
let goalList = try await goalClient.fetchGoals(cacheKey)
await send(.response(.fetchGoalsCompleted(goalList, date: date)))
} catch {
await send(.response(.fetchGoalsFailed))
await send(.response(.fetchGoalsFailed(date: date)))
}
}

Expand Down
12 changes: 8 additions & 4 deletions Projects/Feature/Home/Sources/Home/HomeView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import SwiftUI

import ComposableArchitecture
import FeatureHomeInterface
import SharedDesignSystem
import SharedPerfTestingSupport

/// 홈 화면을 렌더링하는 View입니다.
Expand Down Expand Up @@ -61,10 +62,13 @@ public struct HomeView: View {
}
HomeNavigationBarSection(store: store)
HomeCalendarSection(store: store)
// The branch reads `hasCards` / `isEmptyVisible` so it stays in
// the parent body. Both are cheap derived booleans. Items /
// headerRow read-set lives entirely inside the child sub-view.
if store.hasCards {
// The branch reads presentation booleans so it stays in the parent
// body. Each section owns the rest of its read-set.
if store.isFetchFailed {
DataRetryView {
store.send(.view(.dataRetryTapped))
}
} else if store.hasCards {
HomeContentSection(store: store)
} else if store.isEmptyVisible {
HomeEmptyContentSection(store: store)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,19 +19,22 @@ public struct NotificationReducer {
public struct State: Equatable {
public var notifications: IdentifiedArrayOf<NotificationItem>
public var isLoading: Bool
public var isFetchFailed: Bool
public var isLoadingMore: Bool
public var hasNext: Bool
public var lastId: Int64?

public init(
notifications: IdentifiedArrayOf<NotificationItem> = [],
isLoading: Bool = false,
isFetchFailed: Bool = false,
isLoadingMore: Bool = false,
hasNext: Bool = false,
lastId: Int64? = nil
) {
self.notifications = notifications
self.isLoading = isLoading
self.isFetchFailed = isFetchFailed
self.isLoadingMore = isLoadingMore
self.hasNext = hasNext
self.lastId = lastId
Expand All @@ -48,6 +51,7 @@ public struct NotificationReducer {
case backButtonTapped
case notificationTapped(NotificationItem)
case loadMore
case dataRetryTapped
}

// MARK: - Response
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,9 @@ private func reduceView(

case .loadMore:
return handleLoadMore(state: &state, notificationClient: notificationClient)

case .dataRetryTapped:
return handleOnAppear(state: &state, notificationClient: notificationClient)
}
}

Expand All @@ -75,6 +78,7 @@ private func reduceResponse(
switch action {
case .fetchListResponse(.success(let result)):
state.isLoading = false
state.isFetchFailed = false
state.notifications = IdentifiedArray(
uniqueElements: result.notifications.map { NotificationItem(from: $0) }
)
Expand All @@ -84,6 +88,7 @@ private func reduceResponse(

case .fetchListResponse(.failure):
state.isLoading = false
state.isFetchFailed = true
return .none

case .fetchMoreResponse(.success(let result)):
Expand Down Expand Up @@ -117,6 +122,7 @@ private func handleOnAppear(
) -> Effect<NotificationReducer.Action> {
guard !state.isLoading else { return .none }
state.isLoading = true
state.isFetchFailed = false
return .run { send in
do {
let result = try await notificationClient.fetchList(nil, 10)
Expand Down
17 changes: 12 additions & 5 deletions Projects/Feature/Notification/Sources/NotificationView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,19 @@ public struct NotificationView: View {
VStack(spacing: 0) {
navigationBar

ZStack {
if filteredNotifications.isEmpty {
emptyView
} else {
contentView
if store.isFetchFailed {
DataRetryView {
store.send(.view(.dataRetryTapped))
}
} else {
ZStack {
if filteredNotifications.isEmpty {
emptyView
} else {
contentView
}
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
}
.ignoresSafeArea(.container, edges: .bottom)
Expand Down
Loading
Loading