Raywenderlich/RxSwift: Reactive Programming

Chapter 4: Observables & Subjects in Practice

헬조선의알파고 2020. 9. 20. 02:35

이 책에서 observables과 다양한 유형(types)의 subjects가 작동하는 방식을 이해하고, Swift playground에서 이를 생성하고 확인(experiment)하는 방법을 배웠다.

그러나 UI를 데이터 모델(model)에 바인딩(binding)하거나 새 컨트롤러(controller)를 제시(presenting)하고 출력(output)을 다시 가져 오는 것과 같은 일상적인(everyday) 개발(development) 상황(situations)에서 observables의 실제(practical) 사용을 확인하는 것은 다소 어려운(challenging) 일일 수 있다.

이러한 새로 습득한(acquired) 기술(skills)을 실제로 어떻게 적용해야 하는지 조금은 확신할 수 없을 수 있다. 이 책에서는 2장(chapter), "Observables"과 3장(chapter), "Subjects"와 같은 이론적인(theoretical) 장(chapters)과 이 장(chapters)과 같은 단계별(step-by-step)로 배우는 실제(practical) 적용 장(chapters)이 나눠져 있다.

"... in practice" 장(chapters)에서는 완전한(complete) 앱(app)에 대한 작업을 하게된다. 시작(starter) Xcode 프로젝트(project)에는 모든 비 Rx 코드가 포함(include)되어 있다. RxSwift 프레임 워크(framework)를 추가(add)하고 새로 습득한(newly-acquired) 반응형(reactive) 기술을 사용하여 다른 기능(features)을 추가한다.

 

그렇다고 해서 그 과정에서 몇 가지 새로운 것을 배우지 못할 것이라는 의미는 아니다. 오히려 그 반대(contraire)이다.

이 장(chapter)에서는 RxSwift와 새로운 observable의 기능(superpowers)을 사용하여, 반응형(reactive) 방식으로 멋진 사진 콜라주(collages)를 생성할 수 있는 앱(app)을 만들 것이다.

 

Getting started

이 장(chapter)의 시작(starter) 프로젝트(project)인 Combinestagram을 연다. 이 발음하기 힘든 이름은 아마 가장 시장성(marketable)있는 앱은 아니지만 그렇게 될 수도 있을 것이다.

모든 pods를 설치하고 Combinestagram.xcworkspace를 연다. 이를 수행하는 방법에 대한 자세한(details) 내용은 1장(chapter), "Hello RxSwift"를 참조한다.

Assets/Main.storyboard를 선택하면 실행될 앱의 인터페이스(interface)가 표시된다:

 

첫 번째 화면(screen)에서 사용자는 현재 사진의 콜라주(collage)를 볼 수 있으며, 현재(current) 사진 목록(list)을 지우(clear)거나 완성된(finished) 콜라주(collage)를 디스크(disk)에 저장(save)할 수 있는 버튼(buttons)이 있다. 또한 사용자가 오른쪽 상단(top-right)의 + 버튼(button)을 탭(taps)하면 스토리 보드(storyboard)의 두 번째 뷰 컨트롤러(view controller)로 이동하여, 카메라 롤(Camera Roll)의 사진 목록(list)을 볼 수 있다. 사용자는 썸네일(thumbnails)을 탭(tapping)하여 콜라주(collage)에 사진을 추가 할 수 있다.

뷰 컨트롤러(view controllers)와 스토리 보드(storyboard)는 이미 연결(wired up)되어 있으며, UIImage+Collage.swift를 들여다 보면(peek at) 실제(actual) 콜라주(collage)가 어떻게 구성되는지 확인할 수 있다.

이 장(chapter)에서는 새로운 기술을 연습(practice)하는 데 초점을 맞출(focus on) 것이다. 이제 시작할 시간이다.

 

Using a subject/relay in a view controller

먼저 BehaviorRelay<[UIImage]> 속성(property)을 ​​컨트롤러(controller) 클래스(class)에 추가하고, 선택한 사진을 해당 값(value)에 저장(store)한다. 3장(chapter), "Subjects"에서 학습한 것처럼 BehaviorRelay 클래스(class)는 익숙한 일반(plain) 변수(variables)처럼 작동한다. 언제든지 value 속성(property)을 수동으로(manually) 변경할 수 있다. 이 간단한 예제(example)로 시작하여, 차후에 subjects와 사용자 정의(custom) observables으로 옮겨갈 것이다.

MainViewController.swift를 열고, MainViewController 본문(body)에 다음을 추가한다:

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

다른 클래스(class)는 이 두 상수(constants)를 사용하지 않으므로, private으로 정의(define)한다. 캡슐화(encapsulation)를 항상 염두에 둔다. FTW(For The Win).

dispose bag은 뷰 컨트롤러(view controller)가 소유(owned)한다. 뷰 컨트롤러(view controller)가 해제(released)되는 즉시 모든 observable의 구독(subscriptions)도 폐기(disposed)된다:

 

이를 사용하면, Rx 구독(subscription)의 메모리(memory) 관리(management)가 매우 쉬워진다. 단순히 구독(subscriptions)을 bag에 넣으면 뷰 컨트롤러(view controller)의 할당 해제(deallocation)와 함께(alongside) 폐기(disposed)된다.

그러나 여기서 특정(specific) 뷰 컨트롤러(view controller)는 기본(root) 뷰 컨트롤러(view controller)이고 앱(app)이 종료(quits)되기 전에 해제(released)되지 않기 때문에 이러한 일이 발생하지는 않는다. 스토리 보드(storyboard)의 다른 컨트롤러(controller)에 대해 이 장(chapter)의 뒷부분(later)에서 작동하는 영리한(clever) 할당 해제시 폐기(dispose-upon-deallocation) 메커니즘(mechanism)을 볼 수 있을 것이다.

처음에 앱(app)은 항상 동일한 사진을 기반으로(based) 콜라주(collage)를 만든다. 이미 앱의 자산 카탈로그(Asset Catalog)에 포함되어 있는 바르셀로나의 멋진 사진이다. 사용자가 +를 탭(taps)할 때마다, 동일한 사진을 한 번 더 이미지에 추가한다.

actionAdd()를 찾아 다음을 추가한다:

let newImages = images.value
  + [UIImage(named: "IMG_1907.jpg")!]
images.accept(newImages)

먼저, relay에서 내보낸(emitted) 최신(latest) 이미지 컬렉션(collection)을 value 속성(property)을 사용해 가져온(fetching) 다음, 이미지를 하나 더 추가한다. UIImage 초기화(initialization) 후 옵셔널을 강제 해제(force-unwrapping) 하는 것에 신경 쓰지 않는다. 이 장(chapter)에서는 오류 처리(error handling)를 건너뛰어(skipping) 작업을 간단하게 유지한다.

다음으로 relay의 accept(_)를 사용하여 업데이트된(updated) 이미지 집합(set)을 relay를 구독(subscribed)하는 observers에게 내보낸다(emit).

images relay의 초기값(initial value)은 빈 배열(empty array)이며 사용자가 + 버튼(button)을 누를 때마다, images에서 생성되는 observable 시퀀스(sequence)가 ​​새 배열(array)을 요소(element)로 하여 새 .next 이벤트(event)를 내보낸다(emits).

사용자가 현재 선택(selection) 항목을 지울(clear) 수 있도록 하려면 위로 스크롤(scroll up)하여 actionClear()에 다음을 추가한다:

images.accept([])

해당 섹션(section)에서 몇 줄(lines)의 코드로 사용자 입력(input)을 깔끔하게 처리(handled)했다. 이제 images를 observing하고, 화면에 결과(result)를 표시(displaying)할 수 있다.

 

Adding photos to the collage

이제 images가 연결(wired up)되었으므로 변경 사항(changes)을 observe하고, 그에 따라(accordingly) 콜라주(collage) 미리보기(preview)를 업데이트 (update)할 수 있다.

viewDidLoad()에서 images에 대한 다음 구독(subscription)을 생성한다. relay이지만 Observable처럼 ObservableType을 준수(conforms)하므로, 직접(directly) 구독(subscribe)할 수 있다:

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

images에서 내보내는(emitted) .next 이벤트(events)를 구독(subscribe)한다. 모든 이벤트(event)에 대해 UIImage 유형(type)의 배열(arrays)에 제공된(provided) 도우미(helper) 메서드(method) collage(images:size:)을 사용하여 콜라주(collage)를 만든다. 마지막으로(finally), 이 구독(subscription)을 뷰 컨트롤러(view controller)의 dispose bag에 추가한다.

이 장(chapter)에서는 viewDidLoad()에서 observables을 구독(subscribe)할 것이다. 이 책의 뒷부분에서는 이를 개별(separate) 클래스(classes)로 추출(extracting)하는 방법을 살펴보고, 마지막 장(chapter)에서 MVVM 아키텍처(architecture)로 구성(structure into)한다. 이제 콜라주(collage) UI를 사용할 수 있다. 사용자는 + 버튼(또는 Clear)을 눌러 images를 업데이트(update)하고, 차례대로 UI를 업데이트(update)한다.

앱을 실행(run)하고 시도(try)해 본다. 사진을 4번 추가하면, 콜라주(collage)가 다음과 같이 표시된다:

 

간단하게 구현할 수 있었다.

물론 지금은 앱(app)이 약간(a bit) 지루(boring)하지만 걱정할 것 없다. 카메라 롤(Camera Roll)에서 사진을 선택(select)하는 기능(ability)을 조만간 추가하게 될 것이다.

 

Driving a complex view controller UI

현재 앱(current app)을 사용하면, 사용자 경험(user experience)을 개선(improve)하기 위해 UI가 조금 더 깔끔해(smarter)질 수 있다는 것을 알 수 있다. 예를 들면:

  • 선택한(selected) 사진이 아직 없거나 사용자가 선택(selection) 항목을 방금 지운(cleared) 경우, 지우기(Clear) 버튼(button)을 비활성화(disable)할 수 있다.
  • 마찬가지로(similarly), 선택한(selected) 사진이 없는 경우 저장(Save) 버튼(button)을 활성화(enabled)할 필요가 없다.
  • 콜라주(collage)에 빈 공간이 남게되므로, 홀수(odd) 개의 사진에 대한 저장(Save)을 비활성화(disable)할 수 있다. 
  • 단순히 사진이 더 많아 지면 조금 이상하게(weird) 보이기 때문에, 단일(single) 콜라주(collage)의 사진 수(amount)를 6개로 제한(limit)하는 것이 좋다.
  • 마지막으로(finally), 뷰 컨트롤러(view controller)의 제목(title)이 현재(current) 선택(selection)을 반영(reflected)하면 좋을 것이다.

잠시 시간을 내어 위의 목록(list)을 한 번 더 읽어 본다면, 비 반응형(non-reactive) 방식으로 이러한 수정(modifications)을 구현(implement)하려면 상당히 번거로울(hassle) 수 있다는 것을 분명히(certainly) 알 수 있을 것이다.

고맙게도(thankfully) RxSwift를 사용하면, 단순히 images를 한 번 더 구독(subscribe)하고 코드의 단일 위치(single place)에서 UI를 업데이트(update)하기만 하면 된다.

viewDidLoad()에서 다음 구독(subscription)을 추가(add)한다:

images
    .subscribe(onNext: { [weak self] photos in
        self?.updateUI(photos: photos)
    })
    .disposed(by: bag)

사진 선택(selection) 항목이 변경(change)될 때마다, updateUI(photos:)를 호출(call)한다. 아직 해당 메서드(method)가 없으므로 클래스(class) 본문(body) 내부(inside)의 어디에서든 다음을 추가한다:

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 > 6 ? "\(photos.count) photos" : "Collage"
}

코드는 위에서 언급한 규칙(ruleset)에 따라 전체(complete) UI를 업데이트(update)한다. 모든 논리(logic)가 한곳에 있고, 쉽게 읽을 수 있다. 앱(app)을 다시 실행(run)하면 UI가 변경될 때 모든 규칙이 적용(kick in)되는 것을 볼 수 있다:

 

지금쯤이면 Rx를 iOS 앱(apps)에 적용(applied to)했을 때의 진정한 이점(benefits)이 보이기 시작했을 것이다. 이 장(chapter)에서 작성한 모든 코드를 살펴보면, 전체(entire) UI를 구동(drive)하는 간단한 몇 개의 행(lines)만 있음을 알 수 있다.

 

Talking to other view controllers via subjects

이 섹션(section)에서는 사용자가 카메라 롤(Camera Roll)에서 임의(arbitrary)의 사진을 선택할 수 있도록(in order to), PhotosViewController 클래스(class)를 기본 뷰 컨트롤러(main view controller)에 연결(connect)한다. 그러면 훨씬 더 흥미로운(interesting) 콜라주(collages)가 만들어질 것이다.

먼저, PhotosViewController를 navigation 스택(stack)에 푸시(push)해야 한다. MainViewController.swift를 열고 actionAdd()를 찾는다. 기존(existing) 코드를 주석 처리(comment out)하고, 그 자리에 다음 코드를 추가한다:

let photosViewController = storyboard!.instantiateViewController(withIdentifier: "PhotosViewController") as! PhotosViewController
navigationController!.pushViewController(photosViewController, animated: true)

프로젝트(project)의 스토리 보드(storyboard)에서 PhotosViewController를 인스턴스화(instantiate)하고, navigation 스택(stack)으로 이를 푸시(push)한다. 앱을 실행(run)하고 +를 눌러(tap) 카메라 롤(Camera Roll)을 확인한다.

이 작업을 처음 수행할 때 사진 라이브러리(Photo Library)에 대한 접근(access) 권한이 필요하다:

 

확인(OK)을 누르면(tap), photos controller를 확인할 수 있다. 기기(device)에 따라 실제 모습이 다를 수 있으며, 접근 권한을 부여(granting access)한 후 돌아가서 다시 시도해야(go back and try again) 할 수도 있다.

두 번째로 시도했을 때에는 iPhone 시뮬레이터(Simulator)에 포함된(included) 샘플(sample) 사진을 볼 수 있다.

 

기존의(established) Cocoa 패턴(patterns)을 사용하여 앱(app)을 빌드(building)하는 경우, 다음 단계(next step)는 photos controller가 기본 컨트롤러(main controller)와 상호작용(talk back)할 수 있도록 delegate 프로토콜(protocol)을 추가하는 것이다. 즉, 비 반응형(non-reactive) 방식이다:

 

그러나 RxSwift를 사용하면, 두 클래스(classes)간에 통신(talk)할 수 있는 보편적인(universal) 방법인 Observable이 있다. Observable은 하나 이상의 이해 당사자(interested parties)인 observers에게 어떤 종류(kind)의 메시지라도 전달(deliver)할 수 있기 때문에, 특별한 프로토콜(protocol)을 정의(define)할 필요가 없다.

 

Creating an observable out of the selected photos

다음으로 사용자가 카메라 롤(Camera Roll)에서 사진을 탭(taps)할 때마다 .next 이벤트(event)를 내보내(emits)는 subject를 PhotosViewController에 추가한다. PhotosViewController.swift를 열고 상단 근처에 다음을 추가한다:

import RxSwift

선택한(selected) 사진을 드러내기(expose) 위해 PublishSubject를 추가하고 싶지만, public으로 선언하면 다른 클래스(classes)가 onNext(_)를 호출(call)하여 subject가 값(values)을 방출(emit)하도록 할 수 있으므로 이를 피해야한다. 다른 곳(elsewhere)에서 이를 수행해야 할 때도 있지만, 이 경우에는 그렇지 않다.

PhotosViewController에 다음 속성(properties)을 추가한다:

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

여기에서 선택한(selected) 사진을 내보내는(emit) private PublishSubject와 subject의 observable을 노출(exposes)하는 selectedPhotos라는 public 속성(property)을 모두 정의(define)한다.

이 속성(property)을 구독(subscribing)하면, 기본 컨트롤러(main controller)가 photo 시퀀스(sequence)를 간섭(interfere)하지 않고 observe 할 수 있다.

PhotosViewController에는 카메라 롤(Camera Roll)에서 사진을 읽고 컬렉션 뷰(collection view)에 표시하는 코드가 이미 포함(contains)되어 있다. 사용자가 컬렉션 뷰 셀(collection view cell)을 탭(taps)할 때, 선택한(selected) 사진을 내보내는(emit) 코드를 추가하기만 하면된다.

collectionView(_:didSelectItemAt:)까지 아래로 스크롤(scroll down)한다. 내부 코드는 선택한 이미지를 가져오고(fetches), 컬렉션 셀(collection cell)을 깜박여(flashes) 사용자에게 약간의 시각적(visual) 피드백(feedback)을 제공한다.

다음으로, imageManager.requestImage(...)는 선택한(selected) 사진을 가져와 완료(completion) 클로저(closure)에서 사용할 image와 info 매개 변수(parameters)를 제공한다. 이 클로저(closure)에서 selectedPhotosSubject의 .next 이벤트(event)를 내보낸다(emit).

클로저(closure) 내부(inside)의 guard 문(statement) 바로 뒤에 다음을 추가한다:

if let isThumbnail = info[PHImageResultIsDegradedKey as NSString] as? Bool, !isThumbnail {
    self?.selectedPhotosSubject.onNext(image)
}

info 딕셔너리(dictionary)를 사용하여 이미지가 자산(asset)의 섬네일(thumbnail)인지, 전체 버전(full version)인지 확인한다. imageManager.requestImage(...)는 각 크기(size)에 대해 한 번씩 해당 클로저(closure)를 호출(call)한다. 원본 크기(full-size)의 이미지를 받으면(receive), subject의 onNext(_)를 호출(call)하고 전체 사진을 제공(provide)한다.

이것이 하나의 뷰 컨트롤러(view controller)에서 다른 뷰 컨트롤러로 observable 시퀀스(sequence)를 노출(expose)하는 데 필요한 전부이다. delegate 프로토콜(protocols)이나 그런 종류의 다른 흉내(shenanigans)는 필요하지 않다.

추가적으로, 프로토콜(protocols)을 제거(remove)하면 컨트롤러(controllers) 관계(relationship)가 매우 간단해진다:

 

Observing the sequence of selected photos

다음 작업은 MainViewController.swift로 돌아가 위 스키마(schema)의 마지막 부분(last part)을 완료(complete)하는 코드를 추가하는 것이다. 즉(namely), selected photos sequence를 관찰(observing)하는 것이다.

actionAdd()를 찾아 컨트롤러(controller)를 navigation 스택(stack)으로 푸시(push)하는 행(line) 바로 앞에 다음을 추가한다:

photosViewController.selectedPhotos
    .subscribe(onNext: { [weak self] newImage in
      
    },onDisposed: {
        print("Completed photo selection")
    })
    .disposed(by: bag)

컨트롤러(controller)를 푸시(push)하기 전에 selectedPhotos observable에 대한 이벤트(events)를 구독(subscribe)한다. 사용자가 사진을 탭(tapped)했음을 의미하는 .next와 구독(subscription)이 폐기(disposed)되었을 때의 두 가지 이벤트(events)에 관심이 있다. 이것이 왜 필요한지는 잠시 후에 알게 될 것이다.

onNext 클로저(closure) 안(inside)에 다음 코드를 삽입(insert)하여 모든 것이 작동하도록 한다. 이전에 사용했던 코드와 동일하지만, 이번에는 카메라 롤(Camera Roll)에서 사진을 추가(adds)한다:

guard let images = self?.images else { return }
images.accept(images.value + [newImage])

앱(app)을 실행(run)하고, 카메라 롤(Camera Roll)에서 몇 장의 사진을 선택(select)한 다음 다시 돌아가서 결과를 확인한다. 

 

Disposing subscriptions — review

코드가 겉보기에는(seemingly) 예상대로(expected) 작동하는 것 같지만, 다음을 시도(try)해 본다. 콜라주(collage)에 사진을 몇 장 추가하고 기본 화면(main screen)으로 돌아가 콘솔(console)을 검사(inspect)한다.

"Completed photo selection"라는 메시지(message)가 표시되지 않는다. 마지막 구독(subscription)의 onDispose 클로저(closure)에 출력(print)을 추가했지만, 호출(called)되지 않는다. 즉, 구독(subscription)이 폐기(disposed)되지 않고, 메모리가 해제(frees)되지 않는다!

observable 시퀀스(sequence)를 구독(subscribe)하고, 기본 화면(main screen)의 dispose bag에 넣는다. 이 구독(subscription)은 이전 장(chapters)에서 설명했듯이, bag 객체(object)가 해제(released)될 때 또는 시퀀스(sequence)가 ​​error 또는 completed 이벤트로 완료(completes)될 때 폐기(disposed)된다.

bag 속성(property)을 해제(release)하는 기본 뷰 컨트롤러(main view controller)가 사라(destroy)지지 않고 photos 시퀀스(sequence)도 완료(complete)되지 않기 때문에, 해당 구독(subscription)은 앱(app)의 수명(lifetime)동안 계속 유지(hangs)된다.

observers에 클로저(closure)를 추가해, 컨트롤러(controller)가 화면에서 사라질 때(disappears) .completed 이벤트(event)를 내보낼(emit) 수 있다. 이렇게 하면, 자동(automatic) 폐기(disposal)를 위해 구독(subscription)이 완료(completed)되었음을 모든 observers에게 알린다(notify).

PhotosViewController.swift를 열고 컨트롤러(controller)의 viewWillDisappear(_:)에서 subject의 onComplete() 메서드(method)에 대한 호출(call)을 추가한다:

selectedPhotosSubject.onCompleted()

이제 이 장(chapter)의 마지막 부분(part)인 평범(plain)하고 지루한(boring) 함수(function)를 멋지고(super-awesome) 환상적인(fantastical) 반응형(reactive) 클래스(class)로 변환(converting)할 준비가 되었다.

 

Creating a custom observable

지금까지 BehaviorRelay, PublishSubject, Observable을 사용했다. 마지막으로(wrap up), 사용자 정의(custom) Observable을 만들고 일반 이전 콜백(callback) API를 반응형(reactive) 클래스(class)로 전환한다. Photos 프레임 워크(framework)를 사용하여 사진 콜라주(collage)를 저장(save)한다. 이미 RxSwift에 경험이 있으므로, 반응형(reactive) 방식으로 이를 수행할 것이다.

PHPhotoLibrary 자체에 반응형(reactive) 확장(extension)을 추가할 수 있지만, 간단하게(simple) 구현하기 위해 이 장(chapter)에서는 PhotoWriter라는 새로운 사용자 정의(custom) 클래스(class)를 만든다:

 

Observable을 생성하여 사진을 저장(save)하는 것은 쉽다. 이미지가 디스크에 성공적으로(successfully) 기록되면(written), 자산(asset) ID와 .completed 또는 .error 이벤트(event)가 발생한다.

 

Wrapping an existing API

Classes/PhotoWriter.swift을 연다. 이 파일에는 시작하는 데 도움이 되는 몇 가지 정의(definitions)가 포함(includes)되어 있다.

항상 그렇듯이 먼저 RxSwift 프레임 워크(framework)를 가져온다:

import RxSwift

그런 다음 PhotoWriter에 새로운 정적(static) 메서드(method)를 추가하여, 사진을 저장(save)하려는 코드에 observable을 만든다:

static func save(_ image: UIImage) -> Observable<String> {
    return Observable.create { observer in
    
    }
}

save(_:)는 Observable<String>을 반환(return)한다. 사진을 저장(saving)한 후 단일(single) 요소(element)인 생성된 자산(asset)의 고유한 로컬 식별자(unique local identifier)를 내보내기(emit) 때문이다.

Observable.create(_)는 새로운 Observable을 생성하고, 마지막 클로저(closure)안에 모든 중요한(meaty) 논리(logic)를 추가해야 한다.

Observable.create(_)의 매개 변수(parameter) 클로저(closure)에 다음을 추가한다:

var savedAssetId: String?
PHPhotoLibrary.shared().performChanges({
  
}, completionHandler: { success, error in
  
})

performChanges(_:completionHandler:)의 첫 번째 클로저(closure) 매개 변수(parameter)는 제공된(provided) 이미지에서 사진 자산(asset)을 생성한다. 두 번째에서는 자산(asset) ID 또는 .error 이벤트를 내보낸다(emit).

첫 번째 클로저(closure) 내부(inside)에 추가한다:

let request = PHAssetChangeRequest.creationRequestForAsset(from: image)
savedAssetId = request.placeholderForCreatedAsset?.localIdentifier

PHAssetChangeRequest.creationRequestForAsset(from:)을 사용하여, 새 사진 자산(asset)을 만들고 해당 식별자(identifier)를 savedAssetId에 저장(store)한다. 다음으로 completeHandler 클로저(closure)를 삽입(insert)한다:

DispatchQueue.main.async {
    if success, let id = savedAssetId {
        observer.onNext(id)
        observer.onCompleted()
    } else {
        observer.onError(error ?? Errors.couldNotSavePhoto)
    }
}

success 응답(response)을 받고 savedAssetId에 유효한(valid) 자산(asset) ID가 포함(contains)된 경우, .next 이벤트(event)와 .completed 이벤트(event)를 내보낸다(emit). 오류(error)가 발생하면 사용자 정의(custom) 또는 기본(default) 오류(error)를 내보낸다(emit).

이렇게 observable 시퀀스(sequence) 논리(logic)가 완성(completed)된다.

Xcode는 이미 return 문(statement)이 빠졌다(miss)고 경고(warning)하고 있어야 한다. 마지막 단계(step)로 외부(outer) 클로저(closure)에서 Disposable을 반환(return)해야하므로, Observable.create({})에 마지막 행(line)을 추가한다:

return Disposables.create()

이것으로 클래스(class)는 멋지게 마무리된다. 완전한(complete) save() 메서드(method)는 다음과 같아야 한다:

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()
    }
}

주의를 기울이고(paying attention) 있었다면 "단지(just) 하나(single)의 .next 이벤트(event)만 내보내는 Observable이 왜 필요한가?"라고 자문(asking yourself)할 수도 있을 것이다.

이전 장(chapters)에서 배운 내용을 잠시 생각(reflect)해본다. 예를 들어(For example), 다음 중 하나를 사용하여 Observable을 만들 수 있다.

  • Observable.never() : 어떤 요소(elements)도 내보내지(emits) 않는 observable 시퀀스(sequences)를 생성한다.
  • Observable.just(_:) : 하나의 요소(element)와 .completed 이벤트(event)를 내보낸다(emits).
  • Observable.empty() : 요소(elements)가 없는 .completed 이벤트(event)를 내보낸다(emits).
  • Observable.error(_) : 요소(elements)가 없는 단일(single) .error 이벤트(event)를 내보낸다(emits).

보다시피(As you see) observables은 0개 이상(zero or more)의 .next 이벤트(events) 조합(combination)을 생성(produce)할 수 있으며, .completed 또는 .error로 종료(terminated)될 수 있다.

PhotoWriter의 경우(case)에는 저장(save) 작업(operation)이 한 번만(just once) 완료(completes)되기 때문에, 하나의 이벤트(event)에만 관심(interested)이 있다. 쓰기(writes)가 성공(successful)하면 .next + .completed를 사용하고, 특정 쓰기가 실패(failed)하면 .error를 사용한다.

"하지만 Single은 어떨까?"라고 외친다면(screaming) 큰 점수(bonus point)를 얻을 수 있다. 실제로 Single을 사용하는 것은 어떤지 확인해 본다.

 

RxSwift traits in practice

2장(chapter) "Observables"에서 특정 경우(cases)에 매우 편리한(handy) Observable 구현(implementation)의 특수한(specialized) 변형(variations)인 RxSwift의 traits에 대해 배웠다.

이 장(chapter)에서는 간단히 복습(review)하고, Combinestagram 프로젝트(project)에서 일부 traits을 사용할 것이다. Single부터 시작한다.

 

Single

2장(chapter)에서 학습했듯이, Single은 전문화(specialization)된 Observable이다. .success(Value) 이벤트(event) 또는 .error를 한 번만 내보낼(emit) 수 있는 하나의 시퀀스(sequence)를 나타낸다. 내부적으로(under the hood) .success는 .next + .completed 쌍(pair)이다.

 

이러한 종류의 trait은 파일 저장(saving a file), 파일 다운로드(downloading a file), 디스크에서 데이터 불러오기(loading data from disk), 기본적으로 값을 생성하는 모든 비동기 작업(basically any asynchronous operation that yields a value)과 같은 상황(situations)에서 유용하다. Single의 고유한(distinct) 사용 사례(use-cases)는 두 가지로 분류(categorize)할 수 있다:

  1. 이 장(chapter)의 앞부분에 있는 PhotoWriter.save(_)처럼, success 시 정확히 하나의 요소(element)를 내보내는(emit) 래핑(wrapping) 작업(operations)에 사용된다.
    Observable 대신 Single을 바로(directly) 만들 수 있다. 실제로 이 장(chapter)의 챌린지(challenges) 중 하나에서 Single을 만들기 위해 PhotoWriter의 save(_) 메서드(method)를 업데이트(update)할 것이다.
  2. 시퀀스(sequence)에서 단일(single) 요소(element)를 사용하려는 의도(intention)를 더 잘 표현(express)하고, 시퀀스(sequence)가 ​​둘 이상의 요소를 내보내는(emits) 경우 해당 구독(subscription)에서 오류가 발생(emits)하는 것을 보장(ensure)한다.
    이를 위해 Observable을 구독(subscribe)하고 .asSingle()을 사용하여 Single로 변환(convert)할 수 있다. 이 섹션(section)을 마친 후에 시도(try)해 볼 수 있을 것이다.

 

Maybe

Maybe는 observable이 성공적으로(successful) 완료(completion)되었을 때, 값(value)을 내보내지(emit) 않을 수 있다는 유일한 차이점(only difference)을 제외하면 Single과 매우 유사하다.

 

사진 관련(photograph-related) 예(examples)에서, Maybe의 사용 사례(use-case)를 생각(imagine)해보면 앱이(app) 자체 사용자 정의(custom) 앨범(album)에 사진을 저장(storing)하는 것이다. UserDefaults에 앨범 식별자(album identifier)를 유지(persist)하고, 매번 해당 ID를 사용하여 앨범(album)을 "열고(open)" 내부(inside)에 사진을 저장(write)한다. 다음과 같은 상황을 처리(handle)하는 open(albumId:) -> Maybe<String> 메서드(method)를 설계(design)할 것이다:

  • 주어진 ID의 앨범(album)이 여전히 존재(exists)하는 경우, .completed 이벤트만 내보낸다(emit).
  • 그동안에(meanwhile) 사용자가 앨범(album)을 삭제(deleted)한 경우, 새 앨범(album)을 만들고 UserDefaults에서 유지(persist)할 수 있도록 새로운 ID로 .next 이벤트(event)를 내보낸다(emit).
  • 문제(wrong)가 있어 Photos 라이브러리(library)에 전혀 접근(access)할 수 없는 경우, .error 이벤트(event)를 내보낸다(emit).

다른 traits과 마찬가지로 "기본(vanilla)" Observable을 사용하여 동일한 기능(functionality)을 구현할(achieve) 수 있지만, Maybe는 코드를 작성할 때와 나중에 이를 변경(alter)할 때 프로그래머(programmers)에게 더 많은 컨텍스트(context)를 제공할 수 있다.

Single과 마찬가지로 Maybe.create({...})를 사용하여 직접(directly) Maybe를 생성하거나, .asMaybe()를 사용해 observable 시퀀스(sequence)를 변환(converting)할 수 있다.

 

Completable

마지막으로 다룰 trait은 Completable이다. 이 Observable 변형(variation)은 구독(subscription)이 폐기(disposed of)되기 전에 단일(single) .completed 또는 .error 이벤트(event)만 내보내도록 허용한다.

 

ignoreElements() 연산자(operator)를 사용하여 observable 시퀀스(sequence)를 completable로 변환(convert)할 수 있다. 이 경우 Completable의 요구사항(required)대로 completed 혹은 error 이벤트(event)만 발생하며(emitted), 모든 next 이벤트(events)가 무시(ignored)된다.

다른 observables이나 traits을 생성할 때 사용하는 코드와 매우 유사하게 Completable.create{...} 를 사용하여 completable 시퀀스(sequence)를 만들 수도 있다.

Completable이 단순히(simply) 어떤 값(values)도 내보내지(emitting) 않는다는 것을 알 수 있으며, 왜 그런 시퀀스(sequence)가 ​​필요한지 궁금할 것이다. 비동기(async) 작업(operation)의 성공 여부(succeeded or not)만 알면되는 수 많은 사용 사례(use-cases)가 있다.

Combinestagram으로 돌아가기 전에 예(example)를 들어 살펴 본다. 사용자가 문서(document)를 작업하는 동안 앱(app)이 자동 저장(auto-saves)한다. 문서를 백그라운드(background) 대기열(queue)에 비동기적(asynchronously)으로 저장(save)하고, 완료(completed) 후 작업(operation)이 실패(fails)하면 화면에(onscreen) 작은 알림(notification)이나 경고(alert) 상자(box)를 표시하려고 한다.

저장(saving) 논리(logic)를 saveDocument() -> Completable 함수(function)로 래핑(wrapped)했다고 가정하면, 나머지 논리(logic)는 다음과 같이 쉽게 표현(express)할 수 있다:

saveDocument() 
    .andThen(Observable.from(createMessage)) 
    .subscribe(onNext: { message in
        message.display() 
    }, onError: { e in
        alert(e.localizedDescription) 
    })

andThen 연산자(operator)를 사용하면 success 이벤트(event)에 대해 더 많은 completables 또는 observables을 연결(chain)하고 최종 결과를 구독(subscribe)할 수 있다. 그들 중 하나라도 오류(error)를 발생(emits)시키는 경우, 코드는 최종 onError 클로저(closure)로 이동한다.

이 책의 뒷부분에 나오는 두 개의 장(chapters)에서 Completable을 사용할 것이다. 그리고 이제 다시 Combinestagram과 당면한(at hand) 문제(problem)로 돌아간다.

 

Subscribing to your custom observable

사진을 Photos 라이브러리(library)에 저장(saving)하는 현재 기능(feature)은 특별한(special) trait을 사용할 수 있는 특수(special) 사용 사례(use-cases) 중 하나에 속한다. PhotoWriter.save(_) observable은 새 자산(asset) ID를 한 번만 내보내거나(emits), 오류(errors)가 발생하므로 Single에 적합하다.

이제 가장 핵심(sweetest) 부분이다. 맞춤형으로 설계된(custom-designed) Observable을 사용하여, 이전의 기본 Observable을 대체한다(kicking serious butt, 이기기 위해 동기부여 하다).

MainViewController.swift를 열고 Save 버튼(button)의 actionSave() 액션(action) 메서드(method) 안에 다음을 추가한다:

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(image)를 호출(call)하여 현재 콜라주(collage)를 저장(save)한다. 그런 다음 반환된(returned) Observable을 Single로 변환(convert)하여 구독(subscription)이 최대 하나의 요소(element)를 가져오고 성공(succeeds)하거나, 오류(errors)가 발생하면 메시지(message)를 표시하도록 한다. 또한(additionally), 쓰기(write) 작업(operation)이 성공(success)하면 현재(current) 콜라주(collage)를 지운다(clear).

asSingle()은 원본(source) 시퀀스(sequence)가 ​​둘 이상의 요소(element)를 내보내는(emits) 경우, 오류(error)를 발생시켜 최대 하나의 요소(element)를 얻도록 보장(ensures)한다.

큰 성공(triumphant)과 함께 앱을 실행(run)한 다음, 멋진 사진 콜라주(collage)를 만들어(build up) 디스크에 저장(save)한다.

 

사진(Photos) 앱(app)에서 결과를 확인(check)하는 것을 잊어선 안 된다.

 

이것으로이 책의 섹션(Section) 1을 완료하였다.

더 이상 젊은 Padawan이 아니라 경험 많은 RxSwift Jedi이다. 그러나 아직 다크 사이드(Dark Side)에 도전하려는 유혹(tempted)에 빠져선 안 된다. 곧, 네트워킹(networking), 스레드 전환(thread switching), 오류 처리(error handling)에 대한 전투를 벌이게 될 것이다.

그 전에 훈련(training)을 계속하고, RxSwift의 가장 강력한 측면(aspects) 중 하나에 대해 배워야 한다. 섹션(Section) 2,“Operators and Best Practices”에서 연산자(operators)를 사용하면 Observable의 초능력(superpowers)을 완전히 새로운 수준(level)으로 끌어 올릴 수 있다.

 

Challenges

다음 섹션(section)으로 넘어 가기 전에 두 가지 챌린지(challenges)가 있다. 다시 한 번 사용자 정의(custom) Observable을 생성할 것이다. 하지만 이번에는 약간 다르다(twist).

 

Challenge 1: It's only logical to use a Single

카메라 롤(Camera Roll)에 사진을 저장(saving)할 때, .asSingle()을 사용하여 얻는 것이 많지 않다는 것을 눈치 챘을(noticed) 것이다. observable 시퀀스(sequence)는 이미 최대 하나의 요소(element)를 내보낸다(emits).

맞는 말이지만, 요점(point)은 .asSingle()에 대한 부드러운(gentle) 소개(introduction)를 는 것이었다. 이제 바로 이 챌린지(challenge)에서 스스로 코드를 개선(improve)할 수 있다.

PhotoWriter.swift를 열고 save(_)의 반환(return) 유형(type)을 Single<String>으로 변경(change)한다. 그런 다음 Observable.create를 Single.create로 바꾼다(replace).

이렇게 하면 대부분의 오류(errors)가 해결(clear)된다. 마지막으로 처리(take care of)해야 할 것은 Observable.create이 여러(multiple) 값(values)을 내보내거나(emit) 이벤트(events)를 종료(terminating)할 수 있도록 observer를 매개 변수(parameter)로 받는 것이다. Single.create는 .success(T) 또는 .error(E) 값(values)을 내보내는(emit) 데 한 번만 사용할 수 있는 클로저(closure)를 매개 변수(parameter)로  받는다.

변환(conversion)을 직접 완료(complete)하고, 매개 변수(parameter)가 observer 객체(object)가 아닌 클로저(closure)임을 기억해야 한다. 따라서 이를 single(.success(id))와 같이 호출(call)해야 한다.

 

Challenge 2: Add custom observable to present alerts

MainViewController.swift를 열고 파일 하단으로 스크롤(scroll)한다. 시작(starter) 프로젝트(project)와 함께 제공된 showMessage(_:description:) 메서드(method)를 찾는다.

이 메서드(method)는 화면에(onscreen) 알림(alert)을 표시하고, 사용자가 닫기(Close) 버튼(button)을 탭(taps)하여 알림(alert)을 닫을 때(dismiss) 콜백(callback)을 실행(runs)한다. PHPhotoLibrary.performChanges(_) 에서 이미 수행한 작업과 매우 유사(similar)하다.

이 챌린지(challenge)를 완료(complete)하려면 다음 코드를 작성해야 한다:

  • 주어진 제목(title)과 메시지(message)로 화면에(onscreen) 알림(alert)을 표시하고 Completable을 반환(returns)하는 확장(extension) 메서드(method)를 UIViewController에 추가한다.
  • 사용자가 알림(alert)을 닫을 수 있도록 닫기(Close) 버튼(button)을 추가한다.
  • 구독(subscription)이 해제(dismissed)되면, alert controller를 해제(dismiss)하여 연결되어 있는(dangling) 알림(alerts)이 표시되지 않도록한다.

마지막으로 새로운 completable을 사용하여 showMessage(_:description:) 내에서 알림(alert)을 표시한다.

항상 그렇듯이 문제(trouble)가 발생하거나 제공된(provided) 해결책(solution)이 궁금하다면, 이 장(chapter)의 프로젝트(projects) 폴더(folder)에서 완성된(completed) 프로젝트(project)와 챌린지(challenge) 코드를 확인할 수 있다. 언제든 그것들을 엿볼 수(peek) 있지만, 먼저 최선을 다해보는 것이 좋다.