본문 바로가기
감정일기(가칭)

📌 화면 진입 시 감정 선택창 자동 오픈하기, 저장 완료 후 화면 닫기 처리

by 밤새는 탐험가89 2025. 11. 18.
728x90
SMALL

신규 작성(create) 모드에서는 화면에 들어오자마자 “감정 선택 BottomSheet”를 자동으로 띄우고,
수정(edit) 모드에서는 기존 감정을 유지하므로 자동으로 띄우지 않는다.


🔥 왜 이 기능이 필요한가?

감정일기를 작성할 때 사용자의 실제 흐름은:

 

1. 일기를 작성하러 들어옴 →

 2. 가장 먼저 감정을 선택 →

3. 그 아래 내용들을 채움

 

그래서 신규 작성 화면에서는 자동으로 감정 선택창을 띄우면 UX가 훨씬 자연스럽다.

반면 수정 모드에서는 이미 선택된 감정이 있으므로 자동으로 띄우면 흐름이 오히려 깨진다.


📚 1. 구현 개요

아래 로직을 기준으로 설계했다:

 

1. DiaryEditorViewModel.mode가 .create일 때만 자동 오픈

2. 이미 감정이 선택돼 있으면 오픈 안 함

3. 셀이 아직 렌더링되지 않았을 수 있으므로 안전하게 DispatchQueue로 체크

4. EmotionCell 내부에서 감정 선택 BottomSheet를

여는 메서드(triggerOpenEmotionPicker)를 호출


🧩 2. openEmotionSelectorIfNeeded() 실제 코드

override func viewDidAppear(_ animated: Bool) {
    super.viewDidAppear(animated)
    openEmotionSelectorIfNeeded()
}


// MARK: 자동 감정 선택창 오픈
private func openEmotionSelectorIfNeeded() {
    
    // 📌 edit 모드는 자동으로 열지 않음
    if case .edit = diaryEditorVM.mode {
        return
    }
    
    // 📌 이미 감정을 선택한 경우도 열지 않음
    if !diaryEditorVM.diary.emotion.isEmpty { return }

    let indexPath = IndexPath(item: 0, section: DiaryEditorSection.emotion.rawValue)
    
    guard let cell = diaryCollectionView.cellForItem(at: indexPath) as? EmotionCell else {
        // 셀이 아직 로딩되지 않았으면 0.05초 후 재시도
        DispatchQueue.main.asyncAfter(deadline: .now() + 0.05) {
            self.openEmotionSelectorIfNeeded()
        }
        return
    }
    
    // 📌 EmotionCell 내부의 Picker 오픈 함수 호출
    cell.triggerOpenEmotionPicker()
}

✨ 3. EmotionCell 내부에서 호출할 trigger 메서드

EmotionCell은 자신의 역할을 유지해야 한다.

그래서 아래 메서드를 만들어서 VC가 호출할 수 있게 했다:

func triggerOpenEmotionPicker() {
    onAddButtonTapped?()
}

 

즉,
“뷰컨트롤러가 직접 present하는 게 아니라”
감정 버튼을 눌렀을 때와 동일한 흐름을 타도록 구현했다.

 

➡️ UIKit에서 Cell이 직접 화면을 띄우지 않는다는 규칙 유지
➡️ VC → Cell → VC로 흐름이 자연스러움


📌 4. 전체 흐름 요약

모드 자동 오픈 여부 이유
Create 자동 오픈 새 일기는 감정 선택부터 시작하는 UX가 자연스러움
Edit 자동 오픈 ❌ 이미 감정이 선택된 상태이므로 불필요

 


 

📌 저장 완료 후 화면 닫기 처리 (MVVM 패턴 준수)

이 기능은 매우 중요하다.

ViewController가 직접 “저장 성공 → dismiss()” 처리하는 것이 아니라,
ViewModel이 저장 성공 이벤트를 발행하고
ViewController가 그걸 구독해 처리해야 MVVM 구조가 유지된다.


🎯 1. 핵심 설계 포인트

- 저장 로직(saveDiary)은 ViewModel에서 실행

- 저장 완료 시, ViewModel이 “저장 완료됨” 이벤트 발행

- ViewController는 Combine으로 이 이벤트를 구독

- 저장 성공 시 화면 dismiss 처리

 

이렇게 하면:

 

- VC는 “저장 진행 상황”이나 “DataStore와 통신”에 전혀 관여하지 않음

- ViewModel은 “화면을 닫는다” 같은 UI 로직을 몰라도 됨

 

역할이 완벽히 분리된 MVVM 구조


🧩 2. ViewModel에서 저장 완료 이벤트 발행

ViewModel 내부 코드:

@Published var saveCompleted: Bool = false

 

저장 성공 시:

private func saveDiary() {
    store.save(diary)
    saveCompleted = true
}

🧩 3. ViewController에서 이벤트 구독 후 dismiss 처리

private func bindViewModel() {

    // 유효성 검사 결과 구독 -> 참일 경우 창 닫힘
    diaryEditorVM.$saveCompleted
        .receive(on: RunLoop.main)
        .sink { [weak self] completed in
            guard let self, completed else { return }
            self.navigationController?.dismiss(animated: true)
        }
        .store(in: &cancellables)
}

4. 전체 저장 흐름 요약

사용자 → 저장 버튼 클릭

VC → ViewModel의 attemptSaveDiary 호출

ViewModel에서 입력값 유효성 검사

성공이면 saveDiary() 실행

ViewModel이 saveCompleted = true를 publish

VC가 이를 구독해서 화면 dismiss

 

➡️ VC는 저장 로직에 전혀 관여하지 않음 (MVVM 유지)
➡️ 사용자는 저장이 완료되면 자연스럽게 화면이 닫힌다

 

 

 

728x90
LIST