Project/HiddenGem
🔨 화면 UI을 동시에 나오게 해서 사용자 친화적으로 해보기
밤새는 탐험가89
2025. 5. 19. 14:21
❌ 각 UI 가 동시에 나오지 않음 -> 보기 불편함...
🎯 문제 요약
- 섹션 헤더 -> 즉시 표시죔 -> 하드코딩되어 있기 때문
- 음식점 리스트 -> 비동기 API 호출 후 표시 -> fetchEateries() 가 완료되어야 표시됨
- 다른 데이터 (예: 카테고리) -> 또 다른 ViewModel에서 별도 로딩 -> 동시에 로딩 중이지만, 완료 타이밍이 다름
✅ 사용자 경험 개선 목표
- 모든 필요한 데이터가 준비된 후 한 번에 UI를 표시하고 싶음 → "플래시 화면 없이 완전한 콘텐츠"
- 중간에 뜨는 일부 UI 요소 제거 → UI의 일관성과 신뢰감 유지
🛠 해결 전략 제안
✅ @Published var isLoading: Bool = true 사용
- EateryViewModel, CategoryViewModel 각각에 @Published var isLoading: Bool 있음
- LoadingViewModel에서 Combine을 사용하여 둘의 로딩 상태를 통합
- ViewController는 LoadingViewModel의 isLoading을 관찰해서
- true → 로딩 화면 (ActivityIndicator)
- false → collectionView.reloadData()
✅ CategroyViewModel
@MainActor
class CategoryViewModel: ObservableObject {
// MARK: - Variable
@Published var categories: [StoreCategory] = []
@Published var emojiCategories: [CategoryEmogi] = []
// ✅ isLoading 변수
@Published var isLoading: Bool = false
@Published var errorMessage: String? = nil
private var cancellables: Set<AnyCancellable> = []
// MARK: - Function
/// 음식점 카테고리 정보를 받아오는 메서드
func fetchCategories() async {
isLoading = true
errorMessage = nil
do {
let result = try await NetworkManager.shared.getStoreCategories()
self.categories = result
print("✅ ViewModel: \(result.count)개 카테고리 로딩")
} catch {
self.errorMessage = error.localizedDescription
print("❌ ViewModel: 에러 \(error.localizedDescription)")
}
isLoading = false
}
}
✅ EateryViewModel
@MainActor
class EateryViewModel: ObservableObject {
// MARK: - Variable
@Published var eateries: [EateryItem] = []
@Published var gyeonggiEateries: [EateryItem] = []
@Published var errorMessage: String? = nil
// ✅ isLoading 변수
@Published var isLoading = true
// MARK: - Function
func fetchEateries() async {
errorMessage = nil
isLoading = true
// ✅ async let → 병렬 실행
async let nationwideTask: [EateryItem] = NetworkManager.shared.getEateryLists()
async let gyeonggiTask: [EateryItem] = NetworkManager.shared.getEateryLists(areaCode: 31)
do {
let (nationwide, gyeonggi) = try await (nationwideTask, gyeonggiTask)
self.eateries = nationwide
self.gyeonggiEateries = gyeonggi
print("✅ ViewModel: 전국 \(nationwide.count)개, 경기 \(gyeonggi.count)개 음식점 로딩")
} catch {
self.errorMessage = error.localizedDescription
print("❌ ViewModel 에러: \(error.localizedDescription)")
}
self.isLoading = false // 성공, 실패 모두
}
}
✅ LoadingViewModel
@MainActor
final class LoadingViewModel {
@Published var isLoading: Bool = true
private var cancellables = Set<AnyCancellable>()
// ✅init을 통해 각 값을 감지
init(eateryVM: EateryViewModel, categoryVM: CategoryViewModel) {
Publishers.CombineLatest(
eateryVM.$isLoading,
categoryVM.$isLoading
)
.map { $0 || $1 } // 하나라도 로딩 중이면 true
.assign(to: \.isLoading, on: self)
.store(in: &cancellables)
}
}
✅ HomeViewController
class HomeViewController: UIViewController {
// MARK: - Variable
private let categoriesViewModel: CategoryViewModel = CategoryViewModel()
private let eateriesViewModel: EateryViewModel = EateryViewModel()
private var loadingViewModel: LoadingViewModel!
private var cancellables: Set<AnyCancellable> = []
...
}
extension HomeViewController {
// MARK: - Functions
// ✅ ViewModel의 데이터 변경 시 CollectionView Snapshot 갱신
private func bindViewModel() {
Publishers.CombineLatest3(
categoriesViewModel.$emojiCategories,
eateriesViewModel.$eateries,
eateriesViewModel.$gyeonggiEateries
)
.receive(on: DispatchQueue.main)
.sink { [weak self] _, _, _ in
self?.reloadData()
}
.store(in: &cancellables)
}
// ✅ 각 ViewModel에서 데이터를 받았는지 여부를 isLoading에 저장, 비교하여 감지하는 메서드
private func bindLoading() {
loadingViewModel = LoadingViewModel(
eateryVM: eateriesViewModel,
categoryVM: categoriesViewModel
)
// ✅ 각 로직으로 확인
loadingViewModel.$isLoading
.receive(on: DispatchQueue.main)
.sink { [weak self] isLoading in
if isLoading {
self?.activityIndicator.startAnimating()
self?.recommendationCollectionView.isHidden = true
} else {
self?.activityIndicator.stopAnimating()
self?.recommendationCollectionView.isHidden = false
self?.recommendationCollectionView.reloadData()
}
}
.store(in: &cancellables)
}
private func fetchCategories() {
// 각각의 함수가 독립적으로 실행
Task {
async let categoriesFetch: () = categoriesViewModel.fetchCategories()
async let eateriesFetch: () = eateriesViewModel.fetchEateries()
await categoriesFetch
categoriesViewModel.updateCategories()
await eateriesFetch
//print("🍛 음식점 목록:")
//eateriesViewModel.eateries.forEach { print("- \($0.title)") }
}
}
}