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
- success 시 단 한번의 event 만 emit 하는 함수를 wrapping 할 때
- 한번의 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 를 보여준다.
'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 |
댓글