본문 바로가기

Project/30MinRead

🤔 실시간.. 버튼 활성화 해보기 (Combine)

📝 구현하려는 로직

독서 계획을 세울 때,  책 제목은 2글자 이상, 독서 시간은 5분 이상이어야지만 일정 생성 버튼이 활성화

(활성화 시 배경색이 green으로 변경)

 

근데 지금 문제는 다음과 같다. 

아래의 유효성 검사를 당하는 주체가 newCreatedItem 인데, 지금 이 주체는 저장을 해야하지만 활성화된다. 

따라서 이런 유효성 검사를 하면 계속 비활성화가 된다. 

 

따라서 AddPlanViewController에 있는 readItem을 ViewModel.newCreatedItem에 연결해야한다. 

 

 

📌 MVVM + Combine 패턴에서 saveButton이 비활성화되는 문제 해결

✅ readItem과 newCreatedItem을 실시간으로 동기화해야 유효성 검사가 정상적으로 동작함.

readItem에서 값이 변경될 때 newCreatedItem도 업데이트하고, validReadItemForm()을 실행하면 해결 가능!

 

🔴 saveButton이 계속 비활성화되는 이유

  1. validReadItemForm()이 newCreatedItem을 검사하지만, AddPlanViewController에서 readItem을 직접 사용 중.
  2. readItem이 변경되지만 newCreatedItem과 연결되지 않음 → isFormValid가 업데이트되지 않음.
  3. validReadItemForm()이 실행되지 않아 isFormValid 값이 변경되지 않음.
  4. 결과적으로 saveButton.isEnabled = false 상태가 계속 유지됨.

 

해결 방법: readItem과 newCreatedItem을 실시간으로 동기화

👉 방법: readItem이 변경될 때 newCreatedItem도 업데이트 + validReadItemForm() 실행

👉 Combine의 Published를 활용하여 실시간 데이터 바인딩

 

 

class ReadItemViewModel: ObservableObject {
    
    // MARK: - Variables
    // ✅ newCreatedItem에 기본 값 설정 
    @Published var newCreatedItem: ReadItemModel = ReadItemModel(
        title: "",
        startDate: Date(),
        endDate: Date(),
        dailyReadingTime: 0,
        isCompleted: false
    )
    
    @Published var readItems: [ReadItemModel] = []
    @Published var errorMessage: String?
    
    @Published var isFormValid: Bool = false
    
    private var cancellables: Set<AnyCancellable> = []
    private let coredataManager = CoreDataManager.shared
    
    
    // MARK: - Init
    // ✅ init을 통해 유효성 검사 실행 
    init() {
        setupBindings()
    }
    
    /// ✅ 유효성 검사 진행 - (newCreatedItem이 변경될 때 실행)
    private func setupBindings() {
        $newCreatedItem
            .sink { [weak self] _ in
                self?.validReadItemForm()
            }
            .store(in: &cancellables)
    }
    
    
    // MARK: - Function: 유효성 검사
    func validReadItemForm() {
        guard newCreatedItem.title.count >= 2,
              newCreatedItem.dailyReadingTime >= 300 else {
            isFormValid = false
            print("❌ 유효성 검사 실패: 제목 2자 이상, 독서 시간 1분 이상 필요")
            return
        }
        isFormValid = true
        print("✅ 유효성 검사 통과")
    }

 

 

📌 AddPlanViewController은 추후에 수정(Updated) 작업 시 사용할 예정 -> 이 점 고려

class AddPlanViewController: UIViewController {
    
    // MARK: - Variables
    private var viewModel: ReadItemViewModel = ReadItemViewModel()
    private var cancellables: Set<AnyCancellable> = []
    
    private var readItem: ReadItemModel
    private var isFormValid: Bool = false
    
    // MARK: - init
    // ✅ 독서 계획을 수정할 경우 데이터를 받아 처리하고 그게 아니라면 새 데이터 생성 
    init(readItem: ReadItemModel? = nil) {
        self.readItem = readItem ?? ReadItemModel(
            title: "",
            startDate: Date(),
            endDate: Date(),
            dailyReadingTime: 0,
            isCompleted: false
        )
        super.init(nibName: nil, bundle: nil)
        // ✅ viewModel.newCreatedItem과 연결
        self.viewModel.newCreatedItem = self.readItem
    }

 

viewModel의 isFormValid를 구독하여 실시간으로 데이터 반영

private func setupBinding() {
        viewModel.$isFormValid
            .sink { [weak self] isValid in
                self?.saveButton.isEnabled = isValid
                self?.saveButton.backgroundColor = isValid ? UIColor.systemGreen : UIColor.systemGray
                print("🔄 saveButton 상태 변경: \(isValid ? "활성화됨" : "비활성화됨")")
            }
            .store(in: &cancellables)
    }

 

각 셀에서부터 받아온 데이터를 viewModel의 newCreatedItem에 전달 

// MARK: - Extension: BookCellDelegate
extension AddPlanViewController: BookCellDelegate {
    func didUpdateTitle(_ title: String) {
        //readItem.title = title
        //self.validReadItemForm()
        viewModel.newCreatedItem.title = title
        viewModel.validReadItemForm()
    }
}



// MARK: - Extension: TimeCellDelegate
extension AddPlanViewController: TimeCellDelegate {
    func didUpdateReadingTime(_ time: Int) {
        //readItem.dailyReadingTime = TimeInterval(time)
        //self.validReadItemForm()
        viewModel.newCreatedItem.dailyReadingTime = TimeInterval(time)
        viewModel.validReadItemForm()
    }
}



// MARK: - Extension: DateCellDelegate
extension AddPlanViewController: DateCellDelegate {
    func didSelectedDate(with type: DateType, date: Date) {
        
        switch type {
        case .startDate:
            //readItem.startDate = date
            viewModel.newCreatedItem.startDate = date
            viewModel.validReadItemForm()
        case .endDate:
            //readItem.endDate = date
            viewModel.newCreatedItem.endDate = date
            viewModel.validReadItemForm()
        }
        // selectedDates[type] = date
        // printSelectedDates()
    }
}

400