본문 바로가기
iOS/RxSwift

RxSwift: Observable & Subject in practice

by HaningYa 2020. 7. 13.
728x90


View Controller 에 Disposebag과 BehaviorRelay를 선언

Disposebag를 사용하면 View Controller가 deallocation 할 때 같이 subscriptions들이 dispose되서 memory management 가 편하다.

 

*하지만 rootview 인 경우 앱이 종료되기 전까지 deallocation 하지 않으니 주의하자

 private let bag = DisposeBag()
 private let images = BehaviorRelay<[UIImage]>(value: [])
    

+ 버튼을 누를 때 마다 사진을 들고와 기존 사진과 콜라쥬 한다.

즉 이전에 선언한 images (BehaviorRelay) 에 추가한다.

UI 에 관련되었기 non-terminating sequence 인 Relay를 사용했고 이전 event 의 값을 알아야 하기 때문에 Behavior Subject를 wrapping 한 Behavior Relay를 쓴 것 같다.

 @IBAction func actionAdd() {
        let newImages = images.value + [UIImage(named: "IMG_1907.jpg")!]
        images.accept(newImages)
    }

Clear 버튼에 BehaviorRelay를 비우는 코드를 작성한다.

@IBAction func actionClear() {
        images.accept([])
    }

images를 UIImageView에 표시해보도록 하자

images (BehaviorRelay)는 Observable의 subclass 이므로 직접 subscribe를 할 수 있다.

override func viewDidLoad() {
        super.viewDidLoad()
        images.subscribe(onNext: { [weak imagePreview] photos in
            guard let preview = imagePreview else {return}
            preview.image = photos.collage(size: preview.frame.size)
        })
            .disposed(by: bag)
        
    }

 

.next 이벤트가 emit 하는 images를 subscribe 해서 helper method 인 collage 에게 UIImage 배열을 넘겨준다. 그 리턴값을 preview 라는 UIImageView에 표시하고 subscription은 viewController 의 DisposeBag에 담는다.

매번 actionAdd()가 호출되어 images에 사진이 emit 될 때 마다 viewDidLoad 에서의 subscription을 통해 preview에 images가 collage 되어 표시되는 것을 볼 수 있다.


UI에 부가적인 요소 추가하기

  • 사진이 아무것도 선택되지 않았을 때 clear 버튼 비활성화
  • 사진이 아무것도 선택되지 않았을 때 save 버튼 비활성화
  • 홀수 갯수의 사진이 선택되었을 때 save 버튼 비활성화(공백 때문)
  • 콜라쥬 할 수 있는 최대 사진 갯수 6개로 제한
  • View controller 의 title이 현재 선택된 사진을 반영

만약 위 기능들을 Reactive 하지 않게 구현한다면 addAction() 과 clearAction() 에 항상 images.count를 계산해 updateUI() 라는 함수를 통해 버튼들을 enable/disable 시켜야 했을 것이다.

Reactive 하게 구현하면 images를 바꾸는 함수마다 updateUI()를 호출하는게 아닌 images의 subscription에만 updateUI를 넣어주면 된다.

//in view controller
images.asObservable()
            .subscribe(onNext: {[weak self] photos in
                self?.updateUI(photos : photos)
            })
            .disposed(by: bag)
            
 private func updateUI(photos: [UIImage]) {
        buttonSave.isEnabled = photos.count > 0 && photos.count%2 == 0
        buttonClear.isEnabled = photos.count > 0
        itemAdd.isEnabled = photos.count < 6
        title = photos.count > 0 ? "\(photos.count) photos" : "Collage"
    }

Subjects를 통해 다른 View Controller 와 통신하기

addAction() 에 다른 view controller 를 push 하는 code 작성한다.

@IBAction func actionAdd() {
//        let newImages = images.value + [UIImage(named: "IMG_1907.jpg")!]
//        images.accept(newImages)
        let photosVC = storyboard!.instantiateViewController(withIdentifier: "PhotosViewController") as! PhotosViewController
        navigationController!.pushViewController(photosVC, animated: true)
    }

 

Reactive 하지 않다면 원래 pushed 된 viewController 에서 push 한 viewController 로 데이터를 전달 할 때 delegate protocol을 사용해야 했다. 근데 RxSwift는 어떠한 class 와도 통신할 수 있는 범용적인 방법이 있다. Observable을 사용하는 것이다. Observable은 어떤 데이터든 전달할 수 있어서 따로 protocol을 만들 필요가 없다.

push된 view controller 에 Subject와 observable을 만들어 준다.

 private let selectedPhotosSubject = PublishSubject<UIImage>()
  var selectedPhotos: Observable<UIImage> {
    return selectedPhotosSubject.asObservable()
  }

Subject는 외부 접근이 없기 때문에  private 으로 선언했고 해당 subject를 observable로 받을 수 있는 selectedPhotos는 public 프로퍼티로 선언했다.

selectedPhotos를 subscribe 함으로서 main Controller 가 선택된 photo sequence를 observe 할 수 있게 되었다.

이제 photo 를 select 하는 코드를 collectionView(_:didSelectItemAt:) 에 작성한다.

override func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
    let asset = photos.object(at: indexPath.item)
    
    if let cell = collectionView.cellForItem(at: indexPath) as? PhotoCell {
      cell.flash()
    }
    
    
    imageManager.requestImage(for: asset, targetSize: view.frame.size, contentMode: .aspectFill, options: nil, resultHandler: { [weak self] image, info in
      guard let image = image, let info = info else { return }
      if let isThumbnail = info[PHImageResultIsDegradedKey as NSString] as? Bool, !isThumbnail {
        self?.selectedPhotosSubject.onNext(image)
      }
      
    })
  }

image 가 에러없이 fullsize 이미지가 불러와지면 selectedPhotosSubject에 provide 한다.

다시 mainViewController 에서 photosVC 의 selectedPhotos를 subscribe 한다.

@IBAction func actionAdd() {
    //        let newImages = images.value + [UIImage(named: "IMG_1907.jpg")!]
    //        images.accept(newImages)
    let photosVC = storyboard!.instantiateViewController(withIdentifier: "PhotosViewController") as! PhotosViewController
    navigationController!.pushViewController(photosVC, animated: true)
    photosVC.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: bag)
  }

 


그런데 처음에 말했던 mainviewController 에서의 observable 들은 disposebag에 있어도 rootviewcontroller 이기 때문에 해제가 안된다.

그래서 PhotosVC 에 있던 selectedPhotosSubject viewWillDisappear 에서 onCompleted() event 를 emit 하여 observers 가 dispose 되게 하겠다.

 override func viewWillDisappear(_ animated: Bool) {
    super.viewWillDisappear(animated)
    selectedPhotosSubject.onCompleted()
  }

여기까지 사용한 것

  • BehaviorRelay
  • PublishSubject
  • Observable

Apple API 를 감싸 Observable로 만들어 보겠다.

구현할 기능은 collage된 사진을 photo 에 저장하는 것이다.

PhotoWriter class에 save function을 작성한다.

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 Disposables.create()
    }
  }

 

  • save 는 Observable<String> 을 리턴하는데 만들어진 asset 의 unique id 하나를 emit 하기 때문이다.
  • performChanges 에서 주어진 이미지로 asset을 만든다.  
  • PHAssetChangeRequest.creationRequestForAsset(from:) 을 통해 identifier 를 savedAssetId에 저장한다.
  • completionHanlder를 통해 성공했을 경우 onNext로 savedAssetId를 emit 하고 onCompleted() 한다.
  • 실패했을 경우 onError 를 호출한다.
  • 마지막으로 DIsposables.create()으로 Disposable 을 return 한다.

RxSwift traits in practice

specialized variations of Observable implementation

Single

represent a sequence which can emit just once either .sucess(value) event or .error

어느 상황에 쓰일까

  • saving file
  • downloading file
  • loading data from disk
  • any asynchronous operation that yields a value

두가지 use case

  1. success 시 단 한번의 event 만 emit 하는 함수를 wrapping 할 때
  2. 한번의 emit 만 받아야 할 경우 의도를 더 명확히 표현할 수 있고 sequence 에서 여러번의 emit 이 있을 때 에러를 방지하기 위해

Maybe

Single 과 비슷한데 completed 됬을 때 아무 값도 emit 안할 수 있다. (single 은 completed 되었을 때 값을 emit 함)

어느 상황에 쓰일까

  • 앨범에 이미 추가하려는 asset의 ID가 있을경우(또 저장안해도 될 때) .completed event 만 emit 할 때
  • 사용자가 앨범을 삭제한 후 새로운 앨범을 만들어 .next event로 새로운 ID 를 주고 UserDefaults에 유지할때
  • 어떤 문제 때문에 아예 Photos Libaray에 접근하지 못해서 .error event 을 emit 할 때

Completable

subscription 이 dispose 되기 전 단 한번의 .completed 나 .error 이벤트만 허용한다.

observable sequence 를 "ignoreElements()" operator 를 통해 completable 로 바꿀 수 있다.
Completable.create로도 만들 수 있다.

어느 상황에 쓰일까

  • 사용자가 계속 문서 작업하는 걸 자동 저장하는 기능을 생각해 보자. 백그라운드에서 자동 문서 저장이 완료되면 구석에서 살짝 "자동저장 완료" 라는 표시를 띄울때 쓰인다.

이제 actionSave()를 작성하자

 @IBAction func actionSave() {
    guard let image = imagePreview.image else {return}
    PhotoWriter.save(image)
      .asSingle()
      .subscribe(
        onSuccess: { [weak self] id in
          self?.showMessage("Saved with id: \(id)")
          self?.actionClear()
        },
        onError: { [weak self] error in
          self?.showMessage("Error",description: error.localizedDescription)
        }
    )
      .disposed(by: bag)
  }

PhotoWriter의 save observable 을 Single 로 바꾼 뒤 subscribe 를 한다.

onSuccess일 경우 id 를 보여주며 onError 일 경우 error 를 보여준다.

 

 

 

 

728x90

'iOS > RxSwift' 카테고리의 다른 글

RxSwift background knowledge  (0) 2021.03.25
RxSwift: Subjects  (0) 2020.07.10
RxSwift: Observable  (0) 2020.07.09
RxSwift: Hello RxSwift!🖐  (0) 2020.07.08
RxSwift를 사용한 비동기 프로그래밍 #4 [마지막]  (0) 2020.06.03

댓글