iOS/Swift

Combine (MVVM 패턴)

밤새는 탐험가89 2024. 5. 30. 09:08

Combine | Apple Developer Documentation

 

Combine | Apple Developer Documentation

Customize handling of asynchronous events by combining event-processing operators.

developer.apple.com

 

 

🟥 Combine 이란?

  • 이벤트를 처리하는 operators들을 결합함으로써 비동기 이벤트들을 커스텀하게 다룬다.
  • Combine을 사용하면 비동기 작업과 데이터 흐름을 선언적으로 작성할 수 있으며, 특히 비동기 이벤트와 데이터 스트림을 처리하는 데 매우 유용하다.

 

🟥 핵심 개념

  • Publisher:
    • Publisher는 이벤트를 발행하는 객체이다
    • Publisher는 데이터를 제공하고, 구독자가 있을 때 이를 전달한다.
    • 예: URLSession.DataTaskPublisher, NotificationCenter.Publisher, Just, PassthroughSubject, CurrentValueSubject 등
public protocol Publisher {
    associatedtype Output
    associatedtype Failure: Error

    func receive<S>(subscriber: S) where S : Subscriber, Self.Failure == S.Failure, Self.Output == S.Input
}

 

  • Publisher 데이터 타입
    • Output: Publisher가 발행하는 데이터의 타입
    • Failure: Publisher가 발행할 수 있는 오류의 타입
  • Publisher 예제
import Combine

// Just는 단일 값을 발행하는 Publisher입니다.
let publisher = Just("Hello, Combine!")

// Just의 Output 타입은 String이고, Failure 타입은 Never입니다.

 

 

  • Subscriber:
    • Subscriber는 Publisher로부터 이벤트를 수신하는 객체이다.
    • Subscriber는 Publisher와 연결되어 데이터를 받거나 에러를 처리한다.
    • sink(receiveCompletion:receiveValue:) 메서드로 직접 구독할 수 있다.
public protocol Subscriber: CustomCombineIdentifierConvertible {
    associatedtype Input
    associatedtype Failure: Error

    func receive(subscription: Subscription)
    func receive(_ input: Self.Input) -> Subscribers.Demand
    func receive(completion: Subscribers.Completion<Self.Failure>)
}

 

  • Subscriber 데이터 타입
    • Input: Subscriber가 수신하는 데이터의 타입
    • Failure: Subscriber가 수신할 수 있는 오류의 타입
  • Subscriber 예제
import Combine

// Subscriber 생성
let subscriber = Subscribers.Sink<String, Never>(
    receiveCompletion: { completion in
        print("Completion: \(completion)")
    },
    receiveValue: { value in
        print("Received value: \(value)")
    }
)

// Publisher를 Subscriber에 연결 (구독)
publisher.subscribe(subscriber)

// 출력
// Received value: Hello, Combine!
// Completion: finished

 

  • Operators:
    • Operator는 Publisher의 출력을 변환하거나 필터링하는 데 사용되는 함수이다.
    • map, filter, flatMap, combineLatest, merge, zip 등 다양한 연산자가 제공된다.
  • Cancellable:
    • Cancellable은 구독을 취소하는 데 사용된다.
    • 구독이 더 이상 필요하지 않을 때, 메모리 누수를 방지하기 위해 구독을 취소할 수 있다. 

 

🟥 Combine을 이용한 예제

🟥 기본 사용법

import Combine
import Foundation

// Publisher 생성
let publisher = Just("Hello, Combine")

// Subscriber 생성 및 구독
let subscription = publisher
    .sink(receiveCompletion: { completion in
        switch completion {
        case .finished:
            print("Finished")
        case .failure(let error):
            print("Error: \(error)")
        }
    }, receiveValue: { value in
        print("Received value: \(value)")
    })

// 출력
// Received value: Hello, Combine
// Finished

 

🟥 여러 Operator 사용

import Combine
import Foundation

// Example Publisher
let numbers = [1, 2, 3, 4, 5].publisher

// 여러 연산자를 연결하여 데이터 스트림을 변환
let subscription = numbers
    .filter { $0 % 2 == 0 }  // 짝수만 필터링
    .map { $0 * 10 }         // 값을 10배로 변환
    .sink(receiveCompletion: { completion in
        print("Completion: \(completion)")
    }, receiveValue: { value in
        print("Received value: \(value)")
    })

// 출력
// Received value: 20
// Received value: 40
// Completion: finished

 

 

🟥 MVVM 패턴 - Combine을 이용한 예제

1️⃣ Model

struct User: Codable {
    let name: String
}

 

2️⃣ ViewModel

import Foundation
import Combine

class UserViewModel: ObservableObject {
    @Published var userName: String = ""
    @Published var greeting: String = ""

    private var cancellables = Set<AnyCancellable>()

    init() {
        $userName
            .map { "Hello, \($0)!" }
            .assign(to: \.greeting, on: self)
            .store(in: &cancellables)
    }
}

 

3️⃣ View

import UIKit

class UserView: UIView {
    let textField: UITextField = {
        let textField = UITextField()
        textField.borderStyle = .roundedRect
        return textField
    }()
    
    let label: UILabel = {
        let label = UILabel()
        label.textAlignment = .center
        return label
    }()
    
    override init(frame: CGRect) {
        super.init(frame: frame)
        setupViews()
    }
    
    required init?(coder: NSCoder) {
        super.init(coder: coder)
        setupViews()
    }
    
    private func setupViews() {
        addSubview(textField)
        addSubview(label)
        
        textField.translatesAutoresizingMaskIntoConstraints = false
        label.translatesAutoresizingMaskIntoConstraints = false
        
        NSLayoutConstraint.activate([
            textField.centerXAnchor.constraint(equalTo: centerXAnchor),
            textField.topAnchor.constraint(equalTo: topAnchor, constant: 20),
            textField.widthAnchor.constraint(equalTo: widthAnchor, multiplier: 0.8),
            
            label.centerXAnchor.constraint(equalTo: centerXAnchor),
            label.topAnchor.constraint(equalTo: textField.bottomAnchor, constant: 20),
            label.widthAnchor.constraint(equalTo: widthAnchor, multiplier: 0.8)
        ])
    }
}

 

4️⃣ ViewController

import UIKit
import Combine

class UserViewController: UIViewController {
    private let userView = UserView()
    private let viewModel = UserViewModel()
    private var cancellables = Set<AnyCancellable>()

    override func loadView() {
        view = userView
    }
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        bindViewModel()
        
        userView.textField.addTarget(self, action: #selector(textFieldDidChange(_:)), for: .editingChanged)
    }
    
    private func bindViewModel() {
        viewModel.$greeting
            .receive(on: DispatchQueue.main)
            .assign(to: \.text, on: userView.label)
            .store(in: &cancellables)   // 구독 저장
    }
    
    @objc private func textFieldDidChange(_ textField: UITextField) {
        viewModel.userName = textField.text ?? ""
    }
}

 

Set<AnyCancellable>()

  • Combine 프레임워크에서 비동기 작업을 관리하기 위해 사용되는 구독 취소 객체(Cancellable)를 저장하는 컨테이너이다. 
  • Combine에서는 Publisher를 구독하면 Cancellable 객체가 생성되는데, 이 객체를 통해 언제든지 구독을 취소할 수 있다. 
  • AnyCancellable은 Cancellable 프로토콜을 따르는 모든 객체를 래핑할 수 있는 타입이다.
  • 구체적으로 Set<AnyCancellable>은 구독의 생명주기를 관리하는 데 사용된다.
  • 구독을 취소하지 않으면 메모리 누수가 발생할 수 있기 때문에, 구독을 저장하고 필요할 때 취소하는 것이 중요하다.

 

아래 코드에서 cancellables 는 AnyCancellable 객체를 저장하기 위한 집합으로, 구독이 완료 또는 뷰모델이 해제될 떄 자동으로 구독을 취소할 수 있다. 

 

✅ store(in: ): 구독 저장 

  • store(in:) 메서드는 Cancellable 객체를 컬렉션에 저장하여, 해당 컬렉션의 생명주기가 끝날 때 자동으로 구독을 취소한다. 
  • 주로 Set<AnyCancellable>과 함께 사용된다.
  • 이를 통해 메모리 누수를 방지하고, 수동으로 구독을 취소할 필요가 없다.

 

⭐ 구독을 저장하고 취소하는 이유

  1. 메모리 관리:
    • 구독을 저장하지 않으면 메모리 누수가 발생할 수 있다.
    • Set<AnyCancellable>에 저장하면, 뷰컨트롤러가 해제될 때 자동으로 구독이 취소된다.
  2. 구독 관리:
    • 여러 구독을 관리하기 쉽다.
    • 구독을 하나씩 따로 관리하는 대신, 하나의 컬렉션(Set<AnyCancellable>)에 모두 저장할 수 있다.
  3. 자동 취소:
    • Set<AnyCancellable>에 저장된 구독은 해당 객체가 해제될 때 자동으로 취소된다.
    • 이는 뷰모델이나 뷰컨트롤러의 생명주기를 따르므로, 수동으로 구독을 취소할 필요가 없다.