관리 메뉴

김종권의 iOS 앱 개발 알아가기

[RxSwift] 4. Observables and Subjects 실전 적용 본문

RxSwift/RxSwift 기본

[RxSwift] 4. Observables and Subjects 실전 적용

jake-kim 2020. 5. 25. 20:27

* 실습할 프로젝트의 내용은 여기를 참조하여 다운로드

 

실습할 프로젝트 내용

네비게이션 버튼 중 "+"버튼을 통해 이미지를 추가하며, save버튼을 추가 할 수 있도록 하는 것

RxSwift접근(BehaviorRelay와 PublishSubject이용)하여 구현해야될 내용

- "+"버튼을 누를 시 앨범으로 이동하는 기능, 선택시 뷰에 반영

- save버튼은 이미지가 있는 경우만 활성화

- 이미지를 선택할 때마다, 네비게이션 아이템 타이틀에 현재 입력한 총 이미지 갯수 표현

- save버튼을 누르면 저장되게끔

1. dispose bag

view controller이 dispose bag을 소유하고 있기 때문에, dispose의 ARC가 0이 될때, observable subscription들은 같이 disposed됨

(Rx subscription메모리 관리가 쉬운 장점, 단 해당 뷰 컨트롤러는 루트 뷰 컨트롤이기 때문에 앱이 종료되어야 사라지므로 다른 방법 모색)

<코드를 뷰 컨트롤러 전역에 작성>

// MainViewController.swift
private let bag = DisposeBag()

2. BehaviorRelay

- MainViewController에서 "+"버튼 기능

- BehaviorRelay변수에 추가하는 기능

- BehaviorRelay변수에 추가 된다면 이미지가 보여질 뷰에 삽입

 

1) BehaviorRelay객체에 구독요청 설정

보여질 이미지 업데이트, collage(size:)함수는 미리 내부적으로 정의해놓은 메소드

// MainViewController.swift, 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)

2) +버튼 클릭시, 이미지가 추가 되게끔 하는 기능 구현

<subject 객체를 전역에 선언>

- 초기화할 땐, BehaviorRelay<Type>(value : [])로 초기화

// MainViewController.swift
private let images = BehaviorRelay<[UIImage]>(value: [])

<"+"버튼 클릭시 subject에 이미지 추가>

- 기존 객체에 추가 : subject객체.value + 추가할 객체

- 추가를 subejct에게 알림 : subject객체.accept([])

// MainViewController.swift, addAction
let newImages = images.value
  + [UIImage(named: "IMG_1907.jpg")!]
images.accept(newImages)

3) 이미지가 홀수개이면 이미지가 짤리므로, 짝수일 경우만 저장할 수 있게끔 구현

최대 6개 이미지만 추가 가능하드록, 6개가 되면 "+"버튼 비활성화

3개의 이미지를 추가한 경우(홀수)

<구독요청 코드 삽입>

- updateUI는 +버튼과 save버튼 제약을 주는 메소드

// MainViewController.swift, viewDidLoad()
images.asObservable()
  .subscribe(onNext: { [weak self] photos in
      self?.updateUI(photos: photos)
  })
  .disposed(by: bag)

* subject객체.asObservable()이란? 

 - subject는 observer와 observable 둘의 역할을 다 하는데, 외부에서 observer에 접근하지 못하도록 설정하며 observable에만 접근할 수 있도록 나눠서 접근 가능하도록 하기위함

private let subject = BehaviorRelay<[UIImage]>(value : [])
open let observable = subject.asObservable()

<updateUI메소드 작성>

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"
}

3. 앨범에서 사진을 택하는 뷰 컨트롤러로 화면전환

- PhotosVIewController는 미리 정의된 뷰 컨트롤러

let photosViewController = storyboard!.instantiateViewController(
  withIdentifier: "PhotosViewController") as! PhotosViewController

navigationController!.pushViewController(photosViewController, animated: true)

4. PublishSubject

구독된 순간 새로운 이벤트 수신을 알림

- collection view로 이루어진 PhotoViewController에서 이미지 선택하면 이벤트 발생()

 

<PublishSubject객체 생성>

// PhotosViewController.swift

// subject의 observer를 외부에서 접근하는 것 방지
private let selectedPhotosSubject = PublishSubject<UIImage>()

// Observable만 외부에서 접근할 수 있도록 internal 접근제한자로 선언
var selectedPhotos: Observable<UIImage> {
  return selectedPhotosSubject.asObservable()
}

<PublishSubject구독 요청>

MainViewController에서 이벤트를 처리해야 하므로(이미지 업데이트), push하기 전에 subscribe에게 구독요청

// MainViewController.swift, actionAdd()
// (...중략...)
photosViewController.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)

navigationController!.pushViewController(photosViewController, animated: true)

<컬렉션뷰에서 아이템을 선택하면 이미지 추가>

collectionView(_:didSelectItemAt)에 subject.onNext(_:)로 이벤트 발생

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 }

  // 이 부분만 주목 : .onNext(_:)
  if let isThumbnail = info[PHImageResultIsDegradedKey as NSString] as? Bool, !isThumbnail {
    self?.selectedPhotosSubject.onNext(image)
    
  }

})

<할 일을 끝내고 뷰 컨트롤러가 사라질 때 메모리에 남아있는 subject관련 메모리 종료>

// PhotosViewController.swift, viewWillDisappear(_:)
selectedPhotosSubject.onCompleted()

<save버튼 활성화 - 버튼 누르는 부분>

- 성공과 실패만이 관심사이기 때문에 Observable의 Traits중 single()사용 ... Traits관련 내용은 밑에서 추가로 설명

@IBAction func actionSave() {
  guard let image = imagePreview.image else { return }

  // PhotoWriter.save(_:)는 observable sequence를 반환하므로 바로 구독요청
  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)
}

<save버튼 활성화 - 이미지를 업데이트 시키는 부분>

- save함수에서 리턴된 값은 sequence값인 것이 핵심

- 여기서는 이벤트만 발생(onNext, onCompleted, onError)

static func save(_ image: UIImage) -> Observable<String> {
 
  // Observable.create() : 리턴되는 값은 observable sequence (바로 이 객체를 통해 subscribe가능)
  return Observable.create({ observer in
    var savedAssetId: String?
    
    // PHPhotoLibrary는 사용자가 공유하고 있는 photo library를 관리하는 공유 클래스
    PHPhotoLibrary.shared().performChanges({
      
      // PHAssetChangeRequest를 통해 이미지 수정을 요청
      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()
  })
}

5. Observable의 Traits

1) Single(파일 쓰기와 같은 곳에 사용)

두 가지의 완전한 상태밖에 없는 것 : .success(value), .error(value)

single

2) Maybe

세 가지의 상태 모두가 있는 것

 

* success와 completed의 차이점

 - completed는 성공하지 못해도 언제나 호출되지만, success는 어떤 일을 성공적으로 마친 경우만 호철

 - completed는 파라미터가 없음(value를 emit하지 않음)

 

3) Completable(일을 잘 마무리 했는지 검사할 때 사용 ... alert)

 

*참조 : RxSwift: Reactive Programming with Swift, by raywenderlich team's book

'RxSwift > RxSwift 기본' 카테고리의 다른 글

[RxSwift] 6.Filtering Operators 실습  (0) 2020.05.29
[RxSwift] 5.Filtering Operators  (0) 2020.05.26
[RxSwift] 3. Subjects  (0) 2020.05.22
[RxSwift] 2. Observables  (0) 2020.05.21
[RxSwift] 1. RxSwift의 개념  (0) 2020.05.20
Comments