iOS/Swift

ObservableObject와 @Published

밤새는 탐험가89 2024. 6. 3. 23:37

 

개체가 변경되기 전에 내보내는 게시자가 있는 개체 형식이다.?

 

 

@Published와 ObservableObject는 스위프트의 Combine 프레임워크에서 사용되는 속성 래퍼와 프로토콜이다. 

 

✅ 주로 SwiftUI와 함꼐 사용되어 상태 관리를 쉽게 하고, 데이터 변경 시 UI가 자동으로 업데이트 되도록 한다. 

 

 

🟥 @Published

 ObservableObject에 속하는 프로퍼티가 변경될 때마다 자동으로 변경 사항을 알리는 역할을 한다. 

이를 통해, 프로퍼티를 구독(Subscribe)하고 있는 UI 컴포넌트들이 자동으로 업데이트 된다. 

 

 

🟥 ObservableObject

 Combine 프레임워크의 프로토콜로, 객체가 변경될 수 있음을 나타낸다.

이 프로토콜을 준수하는 클래스는 프로퍼티가 변경될 때 Combine 프레임워크를 통해 변경 사항을 알릴 수 있다. 

 

 

 

🟥 사용 예제

 

1️⃣ ViewModel

  • ObservableObject 프로토콜을 준수하는 CounterViewModel 클래스 정의 
  • @Published var count는 count 프로퍼티가 변경될 때마다 ViewModel을 구독하고 있는 모든 뷰 들에게 알림을 보낸다.
import Combine

class CounterViewModel: ObservableObject {
    @Published var count: Int = 0
}

 

 

2️⃣ ViewController 

  • setupBindings() 메서드를 통해  viewModel.$count를 구독하여 count 값이 변경될 때마다 countLabel의 텍스트를 업데이트한다. 
import UIKit
import Combine

class CounterViewController: UIViewController {
    private var viewModel = CounterViewModel()
    private var cancellables = Set<AnyCancellable>()
    
    private let countLabel: UILabel = {
        let label = UILabel()
        label.translatesAutoresizingMaskIntoConstraints = false
        return label
    }()
    
    private let incrementButton: UIButton = {
        let button = UIButton(type: .system)
        button.setTitle("Increment", for: .normal)
        button.translatesAutoresizingMaskIntoConstraints = false
        return button
    }()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        view.backgroundColor = .white
        
        setupViews()
        setupBindings()
    }
    
    private func setupViews() {
        view.addSubview(countLabel)
        view.addSubview(incrementButton)
        
        NSLayoutConstraint.activate([
            countLabel.centerXAnchor.constraint(equalTo: view.centerXAnchor),
            countLabel.centerYAnchor.constraint(equalTo: view.centerYAnchor),
            
            incrementButton.topAnchor.constraint(equalTo: countLabel.bottomAnchor, constant: 20),
            incrementButton.centerXAnchor.constraint(equalTo: view.centerXAnchor)
        ])
        
        incrementButton.addTarget(self, action: #selector(incrementButtonTapped), for: .touchUpInside)
    }
    
    private func setupBindings() {
        viewModel.$count
            .receive(on: RunLoop.main)
            .sink { [weak self] count in
                self?.countLabel.text = "Count: \(count)"
            }
            .store(in: &cancellables)
    }
    
    @objc private func incrementButtonTapped() {
        viewModel.count += 1
    }
}

 

 

✅ RunLoop.main 이란?

메인 스레드에서 실행되는 런 루프를 뜻한다.

런 루프는 실행 루프라고도 하며, 이벤트를 처리하고 애플리케이션의 지속적인 실행을 유지하는 역할을 한다. 

 

'RunLoop.main'을 사용하면 특정 작업을 메인 스레드에서 실행하도록 보장할 수 있다. 

 

'viewModel.$count.receive(on: RunLoop.main)' 은 Combine 프레임워크에서 데이터 스트림의 변경사항을 수신할 때, 해당 작업을 메인 스레드에서 수행하도록 보장하는 것이다. 이는 UI 업데이트가 메인 스레드에서 이루어져야 하는 iOS 애플리케이션에서 중요하다.

 

viewModel.$count
    .receive(on: RunLoop.main)
    .sink { [weak self] count in
        self?.countLabel.text = "Count: \(count)"
    }
    .store(in: &cancellables)

 

이 코드는 다음과 같은 의미를 갖는다.

  1. viewModel.$count:
    • @Published 속성인 count의 변경 사항을 퍼블리셔로 받는다.
  2. receive(on: RunLoop.main):
    • 데이터 변경 사항을 수신할 때, 해당 작업이 메인 스레드에서 실행한다.
    • 이는 UI 업데이트가 메인 스레드에서만 안전하게 이루어질 수 있기 때문에 중요하다
  3. sink { [weak self] count in ... }:
    • 퍼블리셔에서 방출된 값을 처리하는 클로저로, 여기서는 countLabel의 텍스트를 업데이트한다.
  4. .store(in: &cancellables):
    • sink 구독을 cancellables에 저장하여 메모리 관리를 용이하게 한다.

 

✅ RunLoop.main 외에도 Combine에서 데이터 스트림을 처리할 때 사용할 수 있는 다양한 방법이 있다.

✅ 대표적인 예로는 DispatchQueue.mainOperationQueue.main 등이 있고, 이들은 모두 작업을 메인 스레드에서 실행하도록 한다.

 

DispatchQueue.main

viewModel.$count
    .receive(on: DispatchQueue.main)
    .sink { [weak self] count in
        self?.countLabel.text = "Count: \(count)"
    }
    .store(in: &cancellables)

 

✅ OperationQueue.main

viewModel.$count
    .receive(on: OperationQueue.main)
    .sink { [weak self] count in
        self?.countLabel.text = "Count: \(count)"
    }
    .store(in: &cancellables)

 

  • RunLoop.main:
    • 런 루프 기반으로, 주로 이벤트 처리와 관련된 작업을 메인 스레드에서 수행하는 데 사용된다.
    • 특정 런 루프 모드에서 작업을 수행할 수 있다.
  • DispatchQueue.main:
    • GCD 기반으로, 메인 스레드에서 작업을 처리하는 디스패치 큐이다.
    • 작업이 비동기적으로 수행되며, 주로 동시성 처리와 관련된 작업에 사용된다.
  • OperationQueue.main:
    • 운영 큐 기반으로, 메인 스레드에서 작업을 처리하는 운영 큐다.
    • Operation 객체를 사용하여 작업을 관리하고 실행한다.

 

주로 DispatchQueue.main이 가장 많이 사용되고,  비동기 작업과 메인 스레드 간의 간단한 전환을 위해 활용된다.

 

3️⃣ SceneDelegate

import UIKit

class SceneDelegate: UIResponder, UIWindowSceneDelegate {

    var window: UIWindow?

    func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
        guard let windowScene = (scene as? UIWindowScene) else { return }
        
        window = UIWindow(windowScene: windowScene)
        window?.rootViewController = CounterViewController()
        window?.makeKeyAndVisible()
    }
}