diff --git a/Keychy/Keychy/Presentation/Bundle/Views/Detail/BundleDetailView.swift b/Keychy/Keychy/Presentation/Bundle/Views/Detail/BundleDetailView.swift index a9d2b55d..231ac50b 100644 --- a/Keychy/Keychy/Presentation/Bundle/Views/Detail/BundleDetailView.swift +++ b/Keychy/Keychy/Presentation/Bundle/Views/Detail/BundleDetailView.swift @@ -43,6 +43,7 @@ struct BundleDetailView: View { @State var isNavigatingDeeper: Bool = true @State private var dismissTask: Task? @State private var readyDelayTask: Task? + @State private var sceneReloadTrigger = UUID() /// MultiKeyringScene에 전달할 키링 데이터 리스트 @State var keyringDataList: [MultiKeyringScene.KeyringData] = [] @@ -90,12 +91,6 @@ struct BundleDetailView: View { withAnimation(.easeOut(duration: 0.3)) { isSceneReady = true } - // 마지막으로 로드한 구성 id를 뷰모델에 저장 (뷰모델이 다음 진입 시 동일 구성 판정) - bundleVM.updateLastConfigIds( - background: bundleVM.selectedBackground, - carabiner: bundleVM.selectedCarabiner, - keyringDataList: keyringDataList - ) } } } @@ -103,14 +98,7 @@ struct BundleDetailView: View { ) .animation(.easeInOut(duration: 0.3), value: isSceneReady) /// 씬 재생성 조건을 위한 ID 설정 -> 배경, 카라비너, 키링 구성이 변경되면 씬을 완전히 재생성 - .id("scene_\(background.id ?? "")_\(carabiner.id ?? "")_\(keyringDataList.map { "\($0.index)_\($0.bodyImageURL.hashValue)" }.joined(separator: "_"))") - .onAppear { - bundleVM.returnBackgroundId = bundle.selectedBackground - bundleVM.returnCarabinerId = bundle.selectedCarabiner - bundleVM.returnKeyringsId = bundle.keyrings - .sorted() - .joined(separator: "|") - } + .id(sceneId(background: background, carabiner: carabiner)) VStack { Spacer() @@ -222,6 +210,17 @@ extension BundleDetailView { let newKeyringDataList = await bundleVM.createKeyringDataList(bundle: bundle, carabiner: carabiner) keyringDataList = newKeyringDataList + // 항상 lastConfigIds 업데이트 + let bgId = bundleVM.makeBackgroundId(bundleVM.selectedBackground) + let cbId = bundleVM.makeCarabinerId(bundleVM.selectedCarabiner) + let krId = bundleVM.makeKeyringsId(newKeyringDataList) + + bundleVM.lastBackgroundIdForDetail = bgId + bundleVM.lastCarabinerIdForDetail = cbId + bundleVM.lastKeyringsIdForDetail = krId + + sceneReloadTrigger = UUID() + // 키링 데이터까지 불러오고 난 후에도 키링의 개수가 0개라면 바로 씬을 준비 완료 상태로 체크 if newKeyringDataList.isEmpty { isSceneReady = true @@ -289,12 +288,17 @@ extension BundleDetailView { @MainActor private func handleBundleChange() async { guard let bundle = bundleVM.selectedBundle else { return } + // 동일 구성인지 확인(+변경 감지) - if bundleVM.shouldSkipReloadForReturnedConfig() { + let shouldSkip = bundleVM.shouldSkipReloadForReturnedConfig() + + if shouldSkip { restoreSceneIfNeeded(bundle) isSceneReady = true return } + + // 리로드 진행 - loadBundleData 호출 isSceneReady = false readyDelayTask?.cancel() readyDelayTask = nil @@ -312,6 +316,19 @@ extension BundleDetailView { if bundleVM.selectedCarabiner == nil { bundleVM.selectedCarabiner = bundleVM.resolveCarabiner(from: bundle.selectedCarabiner) } + + // keyringDataList도 복원 (NameEditView에서 돌아올 때 필요) + if keyringDataList.isEmpty, let carabiner = bundleVM.selectedCarabiner { + Task { + let newKeyringDataList = await bundleVM.createKeyringDataList(bundle: bundle, carabiner: carabiner) + await MainActor.run { + keyringDataList = newKeyringDataList + isSceneReady = true + } + } + return + } + readyDelayTask?.cancel() readyDelayTask = nil isSceneReady = true @@ -329,6 +346,19 @@ extension BundleDetailView { } .joined(separator: ";") } + + private func sceneId( + background: Background, + carabiner: Carabiner + ) -> String { + let bgId = background.id ?? "" + let cbId = carabiner.id ?? "" + let krIds = keyringDataList + .map { "\($0.index)_\($0.bodyImageURL.hashValue)" } + .joined(separator: "_") + + return "scene_\(bgId)_\(cbId)_\(krIds)_\(sceneReloadTrigger.uuidString)" + } } diff --git a/Keychy/Keychy/Presentation/Bundle/Views/Edit/BundleEditView.swift b/Keychy/Keychy/Presentation/Bundle/Views/Edit/BundleEditView.swift index 6e0075c5..4836d09d 100644 --- a/Keychy/Keychy/Presentation/Bundle/Views/Edit/BundleEditView.swift +++ b/Keychy/Keychy/Presentation/Bundle/Views/Edit/BundleEditView.swift @@ -246,6 +246,10 @@ extension BundleEditView { private var customNavigationBar: some View { CustomNavigationBar { BackToolbarButton { + bundleVM.lastBackgroundIdForDetail = "" + bundleVM.lastCarabinerIdForDetail = "" + bundleVM.lastKeyringsIdForDetail = "" + isNavigatingAway = true router.pop() } diff --git a/Keychy/Keychy/Presentation/Bundle/Views/Edit/BundleNameEditView.swift b/Keychy/Keychy/Presentation/Bundle/Views/Edit/BundleNameEditView.swift index 06ab3bbf..8a1367bb 100644 --- a/Keychy/Keychy/Presentation/Bundle/Views/Edit/BundleNameEditView.swift +++ b/Keychy/Keychy/Presentation/Bundle/Views/Edit/BundleNameEditView.swift @@ -54,6 +54,16 @@ struct BundleNameEditView: View { morePadding = 40 } TabBarManager.hide() + + Task { + await withCheckedContinuation { (continuation: CheckedContinuation) in + bundleVM.fetchAllBackgrounds { _ in + bundleVM.fetchAllCarabiners { _ in + continuation.resume() + } + } + } + } } // 키보드 높이 변경 시 SwiftUI 애니메이션 비활성화 .onReceive(NotificationCenter.default.publisher(for: UIResponder.keyboardWillShowNotification)) { notification in @@ -150,16 +160,61 @@ extension BundleNameEditView { private var customNavigationBar: some View { CustomNavigationBar { BackToolbarButton { - router.pop() + + if let bundle = bundleVM.selectedBundle { + // resolveBackground/Carabiner가 nil을 반환할 수 있으므로 확인 + guard let bg = bundleVM.resolveBackground(from: bundle.selectedBackground), + let cb = bundleVM.resolveCarabiner(from: bundle.selectedCarabiner) else { + router.pop() + return + } + + let bgId = bundleVM.makeBackgroundId(bg) + let cbId = bundleVM.makeCarabinerId(cb) + + bundleVM.returnBackgroundId = bgId + bundleVM.returnCarabinerId = cbId + + Task { + var krList: [MultiKeyringScene.KeyringData] = [] + krList = await bundleVM.createKeyringDataList(bundle: bundle, carabiner: cb) + let krId = bundleVM.makeKeyringsId(krList) + + await MainActor.run { + bundleVM.returnKeyringsId = krId + // returnIds 설정 완료, pop + router.pop() + } + } + } else { + // bundle이 nil, 바로 pop + router.pop() + } } } center: { EmptyView() } trailing: { - NextToolbarButton { - handleCheckButtonTap() - } - .disabled(isUpdating || bundleName.isEmpty || bundleName.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty || hasProfanity) + completeButton + } + } + + private var completeButton: some View { + let isCompleteEnabled = !isUpdating && + !bundleName.isEmpty && + !bundleName.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty && + !hasProfanity + + return Button { + guard isCompleteEnabled else { return } + handleCheckButtonTap() + } label: { + Text("완료") + .typography(.suit17B) + .foregroundStyle(isCompleteEnabled ? .main500 : .gray200) } + .frame(width: 62, height: 44) + .glassEffect(.regular.interactive(), in: .capsule) + .disabled(!isCompleteEnabled) } private func handleCheckButtonTap() { @@ -182,8 +237,8 @@ extension BundleNameEditView { let bgId = bundleVM.makeBackgroundId(bg) let cbId = bundleVM.makeCarabinerId(cb) - // keyringsId는 현재 번들의 구성 기반으로 재구성 Task { + // returnIds 먼저 설정 var krList: [MultiKeyringScene.KeyringData] = [] if let carabiner = cb { krList = await bundleVM.createKeyringDataList(bundle: bundle, carabiner: carabiner) @@ -195,14 +250,15 @@ extension BundleNameEditView { bundleVM.returnCarabinerId = cbId bundleVM.returnKeyringsId = krId } - } - - bundleVM.updateBundleName(bundle: bundle, newName: bundleName.trimmingCharacters(in: .whitespacesAndNewlines)) { success in - DispatchQueue.main.async { - self.isUpdating = false - if success { - bundleVM.selectedBundle?.name = self.bundleName.trimmingCharacters(in: .whitespacesAndNewlines) - router.pop() + + // returnIds 설정 완료 후 이름 업데이트 + bundleVM.updateBundleName(bundle: bundle, newName: bundleName.trimmingCharacters(in: .whitespacesAndNewlines)) { success in + DispatchQueue.main.async { + self.isUpdating = false + if success { + bundleVM.selectedBundle?.name = self.bundleName.trimmingCharacters(in: .whitespacesAndNewlines) + router.pop() + } } } }