RxSwift

4. Observables & Subjects in Practice

kangwook 2022. 12. 13. 20:47
  • RxSwift의 Observable과 Subject를 배웠지만 UI를 데이터 모델에 바인딩하거나 새 컨트롤러를 제시하고 출력을 다시 가져오는 등 실제 사용은 어려울 수 있다.
  • 프로젝트에 RxSwift 프레임워크를 추가하고 RxSwift의 기술을 사용하여 기능을 추가

 


Getting Started

기본 UI 구성


Using a Subject in a View Controller

  • MainViewController.swift에 다음과 같은 코드를 추가
  • BehaviorRelay<[UIImage]> 는 일반 변수에 익숙한 것처럼 동작
  • DisposeBag은 ViewController가 소유하므로 ViewController가 해제되는 즉시 모든 구독도 삭제
  • 이를 통해 Rx 구독 메모리 관리가 매우 쉬워진다. → 구독을 DisposeBag에 넣기만 하면 ViewController의 할당 해제와 함께 처리
  • 하지만 현재 상황에서는 MainViewController가 RootViewController이고 앱이 종료되기 전에 릴리즈되지 않기 때문에 적용되지 않는다.
// MainViewController.swift
private let bag = DisposeBag()
private let images = BehaviorRelay<[UIImage]>(value: [])

IBAction func actionAdd(_ sender: Any) {
    let newImages = images.value + [UIImage(named: "sunset")!]
    images.accept(newImages)
}
  • 우측 상단의 Add 버튼을 통해 하나 이상의 이미지를 추가
  • accept(_)를 사용하여 Subject를 구독하는 Observer에게 업데이트 된 이미지 세트를 보낸다.
  • 이미지 Subject의 초기 값은 빈 배열이고 사용자가 Add(+) 버튼을 누를 때마다 이미지에서 생성된 Observable 시퀀스가 새 배열을 아이템으로 사용하여 .next 이벤트를 내보낸다.
IBAction func actionClear(_ sender: Any) {
    images.accept([])
}
  • 사용자가 현재 선택을 지우도록 하기위해 clear 버튼을 클릭한 경우 구현

Adding photos to the collage

  • viewDidLoad() 에서 이미지에 대한 구독을 만든다.
  • Subject임에도 불구하고 실제로 Observable의 하위 클래스이기 때문에 직접 구독할 수 있다.
override func viewDidLoad() {
    //...
 
    images
        .subscribe(onNext: { [weak imageView] photos in
        guard let preview = imageView else { return }
 
        preview.image = photos.collage(size: preview.frame.size)
    })
    .disposed(by: disposeBag)
}

Add(+) 버튼을 네 번 누르게 되면 다음과 같은 화면으로 바뀐다.


Driving a complex View Controller UI

  • 현재 앱을 사용하면서 UI가 사용자 환경을 개선하기 위해 조금 더 스마트해질 수 있다는 것을 알 수 있다.
    • 방금 선택한 사진이 없거나 사용자가 방금 선택을 지운 경우 지우기 버튼을 비활성화
    • 마찬가지로 선택한 사진이 없는 경우 저장 버튼을 비활성화
    • 홀수 사진에 대해 저장을 비활성화 하면 콜라주에 빈 공간이 남아있을 수 있다.
    • 더 많은 사진이 약간 이상하게 보이기 때문에 콜라주의 사진 수를 6개로 제한할 수 있다.
    • 마지막으로 ViewController의 title을 현재 선택을 반영하면 좋다.
  • RxSwift를 사용하여 이미지를 한번 더 구독하면 코드의 단일 위치에서 UI를 업데이트 하기만 하면 된다.
override func viewDidLoad() {
    // ...
     
    images.asObservable()
        .subscribe(onNext: { [weak self] photos in
            self?.updateUI(photos: photos)
        })
        .disposed(by: disposeBag)
}
 
private func updateUI(photos: [UIImage]) {
    saveButton.isEnabled = photos.count > 0 && photos.count % 2 == 0
    clearButton.isEnabled = photos.count > 0
    itemAdd.isEnabled = photos.count < 6
    title = photos.count > 0 ? "\(photos.count) photos" : "Collage"
}

이렇게 updateUI(photos:) 함수를 구현하고 이미지에 대해 한번 더 구독을 해서 처리할 수 있다.


Talking to other View Controller via Subjects

  • 사진을 사용자가 카메라에서 임의의 사진을 선택할 수 있도록 할 수 있다.(PhotosViewController)
  • PhotosViewController를 생성하고 Add(+) 버튼을 탭 했을 때 해당 ViewController로 View를 넘길 수 있다.
IBAction func actionAdd(_ sender: Any) {
    guard let photosViewController = storyboard!.instantiateViewController(withIdentifier: "PhotosViewController") as? PhotosViewController else { return }
    navigationController?.pushViewController(photosViewController, animated: true)
}

  • 설정된 Cocoa 패턴을 사용하여 앱을 빌드하는 경우 PhotosViewController가 MainViewController가 연결될 수 있도록 Delegate 프로토콜을 추가하는 것
  • 그러나 RxSwift를 사용하면 두 클래스 간에 Observable을 통해 연결할 수 있다.
  • Observable은 어떤 종류의 메시지를 하나 이상의 이해당사자인 Observer에게 전달할 수 있기 때문에 특별한 프로토콜을 정의할 필요가 없다.

Creating an Observable out of the selected photos

  • PhotosViewController 상단에 다음 코드를 추가
import RxSwift
  • 선택한 사진을 노출하기 위해 PublishSubject를 추가하고 싶지만 다른 클래스가 onNext(_)를 호출하고 아이템 값을 방출하도록 허용하므로 Subject는 private으로 선언
  • selectedPhotos는 private으로 선언하지 않음.
  • 이 속성을 구독하면 MainViewController가 사진 시퀀스를 방해하지 않고 관찰할 수 있다.
private let selectedPhotosSubject = PublishSubject<UIImage>()
var selectedPhotos: Observable<UIImage> {
    return selectedPhotosSubject.asObservable()
}
  • info Dictionary를 사용하여 이미지가 미리보기 이미지인지 Assets의 전체 버전인지 확인
  • imageManager.requestImage(...)는 각 크기에 대해 한 번씩 해당 클로저를 호출
  • 원본 크기 이미지를 받으면 onNext()를 호출하여 전체 사진을 제공
if let isThumbnail = info[PHImageResultIsDegradeKey as NSString] as? Bool, !isThumbnail {
    self?.selectedPhotosSubject.onNext(image)
}
  • 이것이 하나의 ViewController에서 다른 ViewController로 Observable 시퀀스를 노출하는데 필요한 전부

Observing the sequence of selected photos

  • 이제 MainViewController.swift에서 스키마의 마지막 부분을 완료하는 코드를 추가
  • 즉, 선택한 사진 시퀀스를 Observing 하는 것
IBAction func actionAdd(_ sender: Any) {
    photosViewController.selectedPhotos                             // Observable 타입인 selectedPhotos 에 대한 이벤트 구독
        .subscribe(
            onNext: { [weak self] newImage in                       // 사용자가 사진을 탭했을 때
                guard let images = self?.images else { return }
                images.accept(images.value + [newImage])
            },
            onDisposed: {                                           // 구독이 끝났을 때
                print("completed photo selection")
            }
        )
        .disposed(by: disposeBag)
}

Disposing subscriptions - review

  • "completed photo selection" 이라는 문구를 Dispose 될 때 출력하도록 작성했는데 출력이 호출되지 않음.
  • 즉, subscribe 이 폐기되지 않아 메모리가 해제되지 않고 있다.
  • 현재의 앱에선 MainViewController가 이 DisposeBag을 갖고 있기 때문에 MainViewController가 메모리에서 해제되기 전까지는 유지된다.
  • Observer에게 종료 이벤트를 주기 위해 컨트롤러가 화면에서 사라질 때, .completed 이벤트를 생성할 수 있다.
  • 이것은 자동적으로 dispose 할 수 있도록 모든 observer에게 알린다.
override func viewWillDisappear(_ animated: Bool) {
    selectedPhotosSubject.onCompleted()
}

Creating a custom observable

  • Custom Observable을 만들고 평범한 이전 Apple API를 반응형 클래스로 전환
  • 사진 프레임워크를 사용하여 사진 콜라주를 저장
  • PHPhotoLibrary 자체에 반응 확장을 추가할 수 있지만 간단하게 유지하기 위해 PhotoWriter 라는 새로운 사용자 정의 클래스를 만든다.

  • Observable을 생성하여 사진을 저장하는 것은 쉽다.
  • 이미지가 디스크에 성공적으로 기록되면 Asset ID와 .completed 이벤트 또는 .error 이벤트가 발생

Wrapping an existing API

class PhotoWriter {
    enum Errors: Error {
        case couldNotSavePhoto
    }
 
    static func save(_ image: UIImage) -> Observable<String> {
        return Observable.create { observer in
            var savedAssetId: String?
            PHPhotoLibrary.shared().performChanges({
                let request = PHAssetChangeRequest.creationRequestForAsset(from: image)
                savedAssetId = request.placeholderForCreatedAsset?.localIdentifier
            }, completionHandler: { success, error in
                DispatchQueue.main.async {
                    if success, let id = savedAssetId {
                        observer.onNext(id)
                        observer.onCompleted()
                    } else {
                        observer.onError(error ?? Errors.couldNotSavePhoto)
                    }
                }
            })
 
            return Disposable.create()
        }
    }
}
  • save(_: ) 는 Observable<String>을 반환
  • 왜냐하면 사진을 저장한 후 생성된 Asset의 고유한 Local Identifier인 단일 요소를 방출하기 때문
  • Observable.create()는 새로운 Observable을 생성하고 마지막 클로저 안에 모든 중요한 로직을 추가
  • 첫번째 클로저 매개변수에서 제공된 이미지에서 사진 Asset을 생성
  • 두번째 클로저에서 Asset ID 또는 .error 이벤트를 내보낸다.
  • 마지막 단계로 해당 외부 클로저에서 Disposable을 반환해야 하므로 Observable.create({})에 return 키워드 추가

 

  • Observable은 0개 이상의 .next 이벤트 조합을 생성할 수 있으며 .completed 또는 .error 에 의해 종료될 수 있다.
  • PhotoWriter 클래스의 특별한 경우에는 저장 작업이 한번만 완료되기 때문에 하나의 이벤트에만 관심(.next + .completed 또는 .error)
  • 이럴 때 Single을 사용할 수 있다.

RxSwift traits in practice

Single

  • Single은 .success(value) 이벤트 또는 .error 이벤트를 한번만 내보낼 수 있는 시퀀스를 나타낸다.
  • 내부적으로 .success 이벤트는 .next 이벤트와 .completed 이벤트의 쌍

  • 이러한 종류의 특성은 파일 저장, 파일 다운로드, 디스크에서 데이터 로드 또는 기본적으로 값을 생성하는 모든 비동기 작업과 같은 상황에서 유용
    • 시퀀스에서 단일 요소를 사용하려는 의도를 더 잘 표현하고 시퀀스가 둘 이상의 아이템을 방출하는지 확인하기 위해 subscribe 오류가 발생
  • 이를 위해 Observable을 subscribe하고 .asSingle()을 사용하여 Single로 변환할 수 있다.

 

Maybe

  • Observable이 성공적으로 완료되면 값을 방출하지 않을 수 있다는 유일한 차이점을 제외하면 Single과 매우 유사

  • Custom 사진 앨범에 사진을 저장하는 경우
    • 지정된 ID의 앨범이 여전히 존재하는 경우 .completed 이벤트 생성
    • 사용자가 앨범을 삭제한 경우 새 앨범을 만들고 UserDefaults에서 유지할 수 있도록 새 ID로 .next 이벤트를 생성
    • 문제가 발생하여 사진 라이브러리에 전혀 접근할 수 없는 경우 .error 이벤트
  • Maybe는 코드를 작성할 때와 나중에 코드를 변경하는 프로그래머에게 더 많은 컨텍스트를 제공
  • Single과 마찬가지로 Maybe.create({...})를 사용하여 직접 Maybe를 만들거나 .asMaybe()를 통해 Observable Sequence를 변환할 수 있다.

Completable

  • 이 Observable의 변형은 subscribe이 삭제되기 전에 단일 .completed 또는 .error 이벤트만 생성하도록 허용

  • ignoreElements() 연산자를 사용하여 Observable Sequence를 Completable로 변환할 수 있다.
  • 이 경우 Completable에 필요한 것처럼 완료되거나 오류 이벤트만 발생하여 모든 다음 이벤트가 무시
  • Completable.create {...} 를 사용하여 Observable Sequence를 생성할 수 있다.
  • Completable은 비동기 작업이 성공했는지 여부만 알면 되는 다양한 곳에서 사용할 수 있다.

 

Subscribing to your custom observable

  • 현재 PhotoWriter.save(_) observable은 한번만 방출하거나 오류가 발생하므로 Single에 적합
@IBAction func actionSave(_ sender: Any) {
    guard let image = imageView.image else { return }
 
    PhotoWriter.save(image)
        .asSingle()                                         // Single로 변환
        .subscribe { [weak self] (id) in
            self?.showMessage("Saved with id: \(id)")
            self?.actionClear(sender)
        },
        onError: { [weak self] (error) in
            self?.showMessage("Error")
        },
        onDisposed: {
            print("Disposed")
        }
        .disposed(by: disposeBag)
}