본문 바로가기

Swift

ViewModel

https://explorer89.tistory.com/82

 

ObservableObject와 @Published

개체가 변경되기 전에 내보내는 게시자가 있는 개체 형식이다.?  @Published와 ObservableObject는 스위프트의 Combine 프레임워크에서 사용되는 속성 래퍼와 프로토콜이다.  ✅ 주로 SwiftUI와 함꼐 사용

explorer89.tistory.com

 

ObservableObject와 @Published

ObservableObject

  • ObservableObject는 Combine 프레임워크에서 제공하는 프로토콜로, 객체의 상태가 변경될 때 이를 외부에 알리는 역할을 합니다.
  • UIKit에서는 ObservableObject를 사용하더라도 SwiftUI와 같은 UI 자동 갱신은 없으므로, 변경된 데이터를 반영하려면 수동으로 UI를 업데이트하는 코드가 필요합니다.

@Published

  • @Published는 ObservableObject와 함께 사용되는 속성 래퍼로, 해당 프로퍼티의 값이 변경되면 Combine을 통해 변경 사항이 외부로 알림됩니다.
  • UIKit 환경에서는 @Published로 알림을 받고, 이를 통해 데이터 변경 사항에 따라 UI를 갱신할 수 있습니다.
final class AuthenticationViewViewModel: ObservableObject {
    
    @Published var email: String?
    @Published var password: String?
    @Published var isAuthenticationFormValid: Bool = false
    ...
}

 

 

🔴 ObservableObject와 @Published의 역할 차이

ObservableObject

  • 클래스 단위로 변경 사항을 외부에 알리는 역할을 합니다.
  • 해당 클래스의 프로퍼티 중에서 변경 감지가 필요한 것을 @Published로 선언해야만 작동합니다. 즉, ObservableObject는 전체 클래스가 변경 사항을 외부로 알릴 준비가 되어 있음을 나타내는 프로토콜일 뿐입니다.

@Published

  • ObservableObject와 함께 사용되며, 특정 프로퍼티의 변경을 감지하고 외부로 알립니다.
  • 프로퍼티 단위로 작동하며, ObservableObject 없이 단독으로 사용할 수는 없습니다.

 

둘 다 필요한 이유

  1. ObservableObject는 클래스 자체가 변경 사항을 알릴 수 있는 객체임을 정의합니다.
  2. @Published는 어떤 프로퍼티가 변경 사항을 감지할 대상인지를 명시합니다.
    • 모든 프로퍼티가 변경 사항을 알릴 필요는 없기 때문에, 변경 감지가 필요한 프로퍼티만 @Published로 선언합니다.
final class ViewModel: ObservableObject {
    @Published var name: String = "" // 외부에서 변경 사항 감지
    var nonPublishedProperty: String = "" // 변경 사항 감지 안 함
}

 

private var subscription: Set<AnyCancellable> = []

AnyCancellable

  • Combine 프레임워크에서 제공하는 객체로, 비동기 스트림의 구독을 관리합니다.
  • 특정 구독이 필요 없어질 때 이를 해제하려면 AnyCancellable의 인스턴스를 취소(cancellation)해야 합니다.

Set<AnyCancellable>

  • 여러 AnyCancellable 객체를 저장하는 컬렉션으로, Combine 구독을 일괄 관리합니다.
  • 뷰 모델이 메모리에서 해제되면, Set<AnyCancellable>도 함께 소멸되며 모든 구독이 자동으로 취소됩니다.
AuthManager.shared.registerUser(with: email, password: password)
    .sink { completion in
        switch completion {
        case .finished:
            print("User registration completed.")
        case .failure(let error):
            print("Failed to register user: \(error)")
        }
    } receiveValue: { user in
        print("Registered user: \(user)")
    }
    .store(in: &subscription)

 

🔴 Set<AnyCancellable>에 대한 이해

역할

  • Combine에서 생성된 모든 구독(sink, map, flatMap 등)을 저장하고 관리합니다.
  • 구독을 저장하지 않으면 해당 구독이 생성되자마자 해제됩니다. 따라서 Set<AnyCancellable>을 사용해 구독의 수명을 명시적으로 관리합니다.

Combine을 사용하는 모든 경우에 필요한가?

  • 꼭 Set<AnyCancellable>이 필요한 것은 아닙니다.
  • SwiftUI에서는 구독을 onAppear 등에서 생성하고 SwiftUI가 뷰의 생명주기를 자동으로 관리해 주기 때문에, 명시적으로 Set<AnyCancellable>을 관리하지 않아도 되는 경우가 있습니다.
  • 하지만 UIKit에서는 뷰 컨트롤러나 뷰 모델이 Combine 구독을 수동으로 관리해야 하므로 Set<AnyCancellable>이 거의 필수적으로 사용됩니다.

 

 

UIKit에서의 데이터 상태 관리

  • @Published를 활용한 데이터 상태 관리: UIKit에서는 Combine의 sink 또는 assign 메서드를 사용하여 뷰 모델의 @Published 프로퍼티 변경 사항을 감지하고, UI 업데이트를 직접 처리해야 합니다.
  • 예를 들어, isRegistrationFormValid가 변경될 때 버튼 활성화 상태를 업데이트하려면 다음과 같은 방식으로 구현합니다.
viewModel.$isRegistrationFormValid
    .sink { [weak self] isValid in
        self?.registerButton.isEnabled = isValid
    }
    .store(in: &subscriptions)

 

실제로 사용한 코드

private func bindViews() {
    emailTextField.addTarget(self, action: #selector(didChangeEmailField), for: .editingChanged)
    passwordTextField.addTarget(self, action: #selector(didChangePasswordField), for: .editingChanged)
    viewModel.$isAuthenticationFormValid.sink { [weak self] validationState in
        self?.registerButton.isEnabled = validationState
    }
    .store(in: &subscription)

    viewModel.$user.sink { [weak self] user in
        // 계정 생성, 로그인이 완료 되면 등록창 끄기
        guard user != nil else { return }
        //self?.dismiss(animated: true)
        guard let onBoardingVC = self?.navigationController?.viewControllers.first as? OnboardingViewController else { return }
        onBoardingVC.dismiss(animated: true)
    }
    .store(in: &subscription)

    viewModel.$error.sink { [weak self] errorString in
        guard let error = errorString else { return }
        self?.presentAlert(with: error)
    }
    .store(in: &subscription)
}