XLOG

[UIKit] RxSwift 이론 + 적용 본문

Swift/UIKit

[UIKit] RxSwift 이론 + 적용

X_PROFIT 2024. 6. 10. 11:09

1. Observable

  • Observable = Observable sequence = seqquenece
  • 비동기
  • 이벤트를 만들고, emitting 한다.
  • 관찰 가능한 함수형태의 연산자
  • next 를 통해 이벤트를 방출
  • completed 이벤트를 통해 종료
  • error 가 발생되더라도 종료된다.
// just: 하나의 요소를 방출하는 Observable sequence 를 생성
let just = Observable<Int>.just(1)
// of: 여러 요소를 순서대로 방출하는 Observable sequence 를 생성
let of = Observable<Int>.of(1,2,3,4,5)
// from: array 를 취하며 array 내부 요소를 하나씩 방출하는 시퀀스를 생성
let from = Observable.from([1, 2, 3, 4, 5])
// create: 클로져 형식이며 다양한 값(onNext, onError...)을 생성
let create = Observable<String>.create({ observer -> Disposable in 
	observer.onNext("1")
	observer.onNext("2")
	observer.onCompleted()
	observer.onNext("3")
	return Disposables.create()
})

이 외에 empty(), never(), range(start: , end:)

2. Subscribe

  • Observer에 대한 구독이다
  • 옵저버에 담긴 이벤트들을 방출하는 것
  • 이벤트 객체를 파라미터로 하는 클로져 형태이며 반환값은 Disposable

just.subscribe { event in 
	print(event)
}
/*
next(1)
completed
*/

let _ = create.subscribe({
    print($0)
})
/*
next(1)
next(2)
completed
*/

let _ = create.subscribe({
    print($0.element)
})
/*
Optional("1")
Optional("2")
nil
*/

3. Disposing

subscribe 가 Observables 의 이벤트를 emit 하는 것이라면, disposing 은 subscribe 를 취소하는 것, Disposing 을 해주지 않게 되면 메모리 이슈를 발생시킬 수 있다. 구독이 취소되지 않아 이벤트가 발생하지 않더라도 계속 변화를 감시하고 있기 때문에..

4. Subject

  • Observable 과 Observer 기능을 둘 다 하는 것이 바로 Subjects!!
  • 즉 Data를 Control( onNext, onError )

 

  • PublishSubject
    • 구독 후 벌어진 이벤트만 emit

let subject = PublishSubject<String>()
subject.onNext("0")

let subscirbe = subject.subscribe(onNext: {
    print($0)
})
subject.on(.next("1"))
subject.onNext("2")

/*
1
2
*/
  1.  
  • ReplaySubject
    • 구독 시점 이전에 발생한 이벤트들은 버퍼사이즈만큼 구독 시점부터 받는다.

let subject = ReplaySubject<String>.create(bufferSize: 3)
subject.onNext("0")
subject.onNext("10")
subject.onNext("20")
subject.onNext("30")

let subscirbe = subject.subscribe(onNext: {
    print($0)
})
subject.on(.next("1"))
subject.onNext("2")

/*
10
20
30
1
2
*/
  • BehaviorSubject
    • 구독 시점 바로 이전에 발생한 event 부터 emit

let subject = BehaviorSubject(value: "start")
let subscribe1 = subject.subscribe(onNext: {
    print("subscribe1: \($0)")
})
subject.onNext("0")
subject.onNext("10")
subject.onNext("20")
subject.onNext("30")

let subscirbe2 = subject.subscribe(onNext: {
    print("subscribe2: \($0)")
})
subject.on(.next("1"))
subject.onNext("2")


/*
subscribe1: start
subscribe1: 0
subscribe1: 10
subscribe1: 20
subscribe1: 30
subscribe2: 30
subscribe1: 1
subscribe2: 1
subscribe1: 2
subscribe2: 2
*/
  • AsyncSubject
    • 구독 시점과 상관없이 Observable 이 complete 되는 시점에 가장 최근 발생한 이벤트만을 emit 한다.

let subject = AsyncSubject<String>()
let subscribe1 = subject.subscribe(onNext: {
    print("subscribe1: \($0)")
})
subject.onNext("0")
subject.onNext("10")
subject.onNext("20")
subject.onNext("30")

let subscirbe2 = subject.subscribe(onNext: {
    print("subscribe2: \($0)")
})
subject.on(.next("1"))
subject.onNext("2")
subject.onCompleted()
/*
subscribe1: 2
subscribe2: 2
*/

 

5. Observable vs Subject

위에서 Subject 는 Observable, Observer 의 두가지 기능을 가지고 있다고 설명을 했지만 정확한 차이를 모르겠어서 찾아봤다.

출처: https://sujinnaljin.medium.com/rxswift-subject-99b401e5d2e5

Observable 은 위에서도 설명을 적었지만 함수이다. 따로 state 가 존재하지 않고, 그렇기에 매번 실행이 된다. 즉 각 View 에 데이터를 적용을 하기 위해선 Subject를 사용해야 한다. 같은 값을 바라보고 있어야 하기 때문이다.

6. Observable 의 Life Cycle

  • Subscribed
  • Next
  • Completed / Error
  • Disposed

6.  간단히 프로젝트에 적용

사용 프로젝트 : https://github.com/profit0124/TodoListWithUIKit/tree/feature/rxSwift

 

GitHub - profit0124/TodoListWithUIKit

Contribute to profit0124/TodoListWithUIKit development by creating an account on GitHub.

github.com

이전에 DiffableDataSource, CoreData 를 사용했던 TodoList 에 rxSwift를 간단하게 적용해봤다.

간단히 RxSwift의 원리를 생각해보면 Observable 을 Subscribe 하여, 해당 이벤트를 방출하면 이벤트를 받아서 자동으로 처리를 해주는 것이다. 그렇기에 기존에 ViewModel 에서 사용했던 [Todo] 를 Subject로 변경하여 값이 바뀔때마다, snapshot을 변경하여 datasource에 적용해주도록 변경하였다.

import UIKit
import RxSwift
import CoreData

final class TodosViewModel {
    var todos: BehaviorSubject<[Todo]> = BehaviorSubject<[Todo]>(value: [])
    private let disposeBag = DisposeBag()
    
    var dataSource: UICollectionViewDiffableDataSource<CollectionViewSection, Todo>!
    
    init(_ collectionView: UICollectionView) {
        self.dataSource = .init(collectionView: collectionView) { (collectionView, indexPath, todo) -> UICollectionViewCell? in
            guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: TodoCellViewCollectionViewCell.id, for: indexPath) as? TodoCellViewCollectionViewCell else { return UICollectionViewCell() }
            cell.configuration(todo: todo, delegate: self)
            return cell
        }
        bind()
        fetchTodos()
    }
    
    func bind() {
        todos.asObservable()
            .subscribe(onNext: { [weak self] items in
                guard let self = self else { return }
                var snapShot = NSDiffableDataSourceSnapshot<CollectionViewSection, Todo>()
                snapShot.appendSections([.main])
                snapShot.appendItems(items.filter({ !$0.isCompleted }))
                self.dataSource.apply(snapShot, animatingDifferences: true)
            })
            .disposed(by: disposeBag)
    }
    
    func fetchTodos() {
        let request = Todo.fetchRequest()
        request.sortDescriptors = [NSSortDescriptor(keyPath: \Todo.deadline, ascending: false)]
        do {
            let todoItems = try PersistenceManager.shared.container.viewContext.fetch(request)
            self.todos.onNext(todoItems)
        } catch {
            print(error)
        }
    }
    
    func addTodo(_ todo: Todo) {
        do {
            var values = try todos.value()
            values.append(todo)
            self.todos.onNext(values)
        } catch {
            print(error)
        }
    }
	...
}

이렇게 해주게 되면 todos의 이벤트를 방출하기만 하면 todos 의 변화가 있을 때마다 자동으로 snapShot을 업데이트 하여 CollectionView의 UI를 변경할 수 있다.

물론 DiffableDataSource가 아닌 RxDataSource를 사용할 수도 있다.