From d2561272c0b4d86bafd00f450f41e038522fc635 Mon Sep 17 00:00:00 2001 From: Yu Sung Date: Tue, 18 Aug 2026 13:29:55 +0900 Subject: [PATCH] =?UTF-8?q?feat(home):=20=ED=99=88=20=ED=83=AD=20=EB=8B=B9?= =?UTF-8?q?=EA=B2=A8=EC=84=9C=20=EC=83=88=EB=A1=9C=EA=B3=A0=EC=B9=A8?= =?UTF-8?q?=EC=9D=84=20=EC=B6=94=EA=B0=80=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Following/MainHomeFollowingView.swift | 46 +++++++++++++++---- .../MainHomeFollowingViewModel.swift | 22 ++++++--- .../Home/Ranking/MainHomeRankingView.swift | 34 ++++++++++++-- .../Ranking/MainHomeRankingViewModel.swift | 22 ++++++--- .../MainHomeRecommendationView.swift | 22 +++++++-- .../MainHomeRecommendationViewModel.swift | 18 ++++++-- 6 files changed, 131 insertions(+), 33 deletions(-) diff --git a/SodaLive/Sources/V2/Main/Home/Following/MainHomeFollowingView.swift b/SodaLive/Sources/V2/Main/Home/Following/MainHomeFollowingView.swift index 6c43e55d..3e79df1f 100644 --- a/SodaLive/Sources/V2/Main/Home/Following/MainHomeFollowingView.swift +++ b/SodaLive/Sources/V2/Main/Home/Following/MainHomeFollowingView.swift @@ -46,18 +46,15 @@ struct MainHomeFollowingView: View { @ViewBuilder private var content: some View { - if viewModel.isLoading { - ProgressView() - .tint(.white) - .frame(maxWidth: .infinity, maxHeight: .infinity) - .background(Color.black) + if viewModel.isLoading && !viewModel.hasLoaded { + loadingContent } else if viewModel.isLoginRequired { - MainHomeFollowingEmptyStateView( + emptyContent( message: I18n.HomeFollowing.loginRequiredMessage, loginAction: onTapLogin ) } else if !viewModel.message.isEmpty { - MainHomeFollowingEmptyStateView(message: viewModel.message) + emptyContent(message: viewModel.message) } else if let response = viewModel.response { ScrollView(.vertical, showsIndicators: false) { VStack(spacing: 0) { @@ -107,8 +104,41 @@ struct MainHomeFollowingView: View { .padding(.bottom, SodaSpacing.s20) } .background(Color.black) + .refreshable { + await viewModel.refresh() + } } else { - MainHomeFollowingEmptyStateView(message: I18n.HomeFollowing.emptyStateMessage) + emptyContent(message: I18n.HomeFollowing.emptyStateMessage) + } + } + + private var loadingContent: some View { + GeometryReader { geometry in + ScrollView(showsIndicators: false) { + ProgressView() + .tint(.white) + .frame(maxWidth: .infinity) + .frame(minHeight: geometry.size.height) + } + .background(Color.black) + .refreshable { + await viewModel.refresh() + } + } + } + + private func emptyContent(message: String, loginAction: (() -> Void)? = nil) -> some View { + GeometryReader { geometry in + ScrollView(showsIndicators: false) { + MainHomeFollowingEmptyStateView( + message: message, + loginAction: loginAction + ) + .frame(minHeight: geometry.size.height) + } + .refreshable { + await viewModel.refresh() + } } } } diff --git a/SodaLive/Sources/V2/Main/Home/Following/MainHomeFollowingViewModel.swift b/SodaLive/Sources/V2/Main/Home/Following/MainHomeFollowingViewModel.swift index f202bcc0..8636b295 100644 --- a/SodaLive/Sources/V2/Main/Home/Following/MainHomeFollowingViewModel.swift +++ b/SodaLive/Sources/V2/Main/Home/Following/MainHomeFollowingViewModel.swift @@ -17,18 +17,16 @@ final class MainHomeFollowingViewModel: ObservableObject { repository.getFollowing() .sink { [weak self] result in - guard let self else { return } - switch result { case .finished: DEBUG_LOG("finish") case .failure(let error): ERROR_LOG(error.localizedDescription) - self.response = nil - self.isLoginRequired = false - self.message = I18n.HomeFollowing.loadFailedMessage - self.hasLoaded = true - self.isLoading = false + self?.response = nil + self?.isLoginRequired = false + self?.message = I18n.HomeFollowing.loadFailedMessage + self?.hasLoaded = true + self?.isLoading = false } } receiveValue: { [weak self] response in guard let self else { return } @@ -60,6 +58,16 @@ final class MainHomeFollowingViewModel: ObservableObject { } .store(in: &subscription) } + + func refresh() async { + if !isLoading { + fetchFollowing() + } + + for await isLoading in $isLoading.values { + if !isLoading { return } + } + } } private extension HomeFollowingTabResponse { diff --git a/SodaLive/Sources/V2/Main/Home/Ranking/MainHomeRankingView.swift b/SodaLive/Sources/V2/Main/Home/Ranking/MainHomeRankingView.swift index 2e483ae0..9fb7c28e 100644 --- a/SodaLive/Sources/V2/Main/Home/Ranking/MainHomeRankingView.swift +++ b/SodaLive/Sources/V2/Main/Home/Ranking/MainHomeRankingView.swift @@ -15,10 +15,9 @@ struct MainHomeRankingView: View { .ignoresSafeArea() if viewModel.isLoading && !viewModel.hasLoaded { - ProgressView() - .progressViewStyle(CircularProgressViewStyle(tint: .white)) + loadingContent } else if viewModel.items.isEmpty { - MainHomeRankingEmptyStateView() + emptyContent } else { rankingContent } @@ -30,6 +29,20 @@ struct MainHomeRankingView: View { } } + private var loadingContent: some View { + GeometryReader { geometry in + ScrollView(showsIndicators: false) { + ProgressView() + .progressViewStyle(CircularProgressViewStyle(tint: .white)) + .frame(maxWidth: .infinity) + .frame(minHeight: geometry.size.height) + } + .refreshable { + await viewModel.refresh() + } + } + } + private var rankingContent: some View { ScrollView(showsIndicators: false) { VStack(alignment: .leading, spacing: SodaSpacing.s8) { @@ -83,6 +96,21 @@ struct MainHomeRankingView: View { .padding(.horizontal, SodaSpacing.s8) .padding(.vertical, SodaSpacing.s20) } + .refreshable { + await viewModel.refresh() + } + } + + private var emptyContent: some View { + GeometryReader { geometry in + ScrollView(showsIndicators: false) { + MainHomeRankingEmptyStateView() + .frame(minHeight: geometry.size.height) + } + .refreshable { + await viewModel.refresh() + } + } } private var twoColumns: [GridItem] { diff --git a/SodaLive/Sources/V2/Main/Home/Ranking/MainHomeRankingViewModel.swift b/SodaLive/Sources/V2/Main/Home/Ranking/MainHomeRankingViewModel.swift index d07e8d33..802b6189 100644 --- a/SodaLive/Sources/V2/Main/Home/Ranking/MainHomeRankingViewModel.swift +++ b/SodaLive/Sources/V2/Main/Home/Ranking/MainHomeRankingViewModel.swift @@ -17,18 +17,16 @@ final class MainHomeRankingViewModel: ObservableObject { repository.getCreatorRankings() .sink { [weak self] result in - guard let self else { return } - switch result { case .finished: DEBUG_LOG("finish") case .failure(let error): ERROR_LOG(error.localizedDescription) - self.items = [] - self.showRankChange = false - self.emptyMessage = I18n.HomeRanking.emptyStateMessage - self.hasLoaded = true - self.isLoading = false + self?.items = [] + self?.showRankChange = false + self?.emptyMessage = I18n.HomeRanking.emptyStateMessage + self?.hasLoaded = true + self?.isLoading = false } } receiveValue: { [weak self] response in guard let self else { return } @@ -60,4 +58,14 @@ final class MainHomeRankingViewModel: ObservableObject { } .store(in: &subscription) } + + func refresh() async { + if !isLoading { + fetchRankings() + } + + for await isLoading in $isLoading.values { + if !isLoading { return } + } + } } diff --git a/SodaLive/Sources/V2/Main/Home/Recommendation/MainHomeRecommendationView.swift b/SodaLive/Sources/V2/Main/Home/Recommendation/MainHomeRecommendationView.swift index 724919ef..c38972f1 100644 --- a/SodaLive/Sources/V2/Main/Home/Recommendation/MainHomeRecommendationView.swift +++ b/SodaLive/Sources/V2/Main/Home/Recommendation/MainHomeRecommendationView.swift @@ -18,9 +18,8 @@ struct MainHomeRecommendationView: View { Color.black .ignoresSafeArea() - if viewModel.isLoading && viewModel.recommendations == nil { - ProgressView() - .progressViewStyle(CircularProgressViewStyle(tint: .white)) + if viewModel.isLoading && viewModel.recommendations == nil && viewModel.errorMessage.isEmpty { + loadingContent } else { ScrollView(showsIndicators: false) { VStack(alignment: .leading, spacing: SodaSpacing.s24) { @@ -83,6 +82,9 @@ struct MainHomeRecommendationView: View { } .padding(.vertical, SodaSpacing.s20) } + .refreshable { + await viewModel.refresh() + } } } .onAppear { @@ -114,6 +116,20 @@ struct MainHomeRecommendationView: View { } } } + + private var loadingContent: some View { + GeometryReader { geometry in + ScrollView(showsIndicators: false) { + ProgressView() + .progressViewStyle(CircularProgressViewStyle(tint: .white)) + .frame(maxWidth: .infinity) + .frame(minHeight: geometry.size.height) + } + .refreshable { + await viewModel.refresh() + } + } + } } struct MainHomeRecommendationView_Previews: PreviewProvider { diff --git a/SodaLive/Sources/V2/Main/Home/Recommendation/MainHomeRecommendationViewModel.swift b/SodaLive/Sources/V2/Main/Home/Recommendation/MainHomeRecommendationViewModel.swift index 49aef158..5cfcefb3 100644 --- a/SodaLive/Sources/V2/Main/Home/Recommendation/MainHomeRecommendationViewModel.swift +++ b/SodaLive/Sources/V2/Main/Home/Recommendation/MainHomeRecommendationViewModel.swift @@ -21,16 +21,14 @@ final class MainHomeRecommendationViewModel: ObservableObject { repository.getRecommendations() .sink { [weak self] result in - guard let self else { return } - switch result { case .finished: DEBUG_LOG("finish") case .failure(let error): ERROR_LOG(error.localizedDescription) - self.errorMessage = I18n.Common.commonError - self.isShowPopup = true - self.isLoading = false + self?.errorMessage = I18n.Common.commonError + self?.isShowPopup = true + self?.isLoading = false } } receiveValue: { [weak self] response in guard let self else { return } @@ -58,6 +56,16 @@ final class MainHomeRecommendationViewModel: ObservableObject { .store(in: &subscription) } + func refresh() async { + if !isLoading { + fetchRecommendations() + } + + for await isLoading in $isLoading.values { + if !isLoading { return } + } + } + func followAll(creatorIds: [Int], completionKey: String) { guard !creatorIds.isEmpty, !completionKey.isEmpty else { return } guard !completedFollowKeys.contains(completionKey), !followingKeys.contains(completionKey) else { return }