Udemy에서 Mastering RxSwift in iOS 수업을 듣고 난 후 정리한 글입니다. 개인적으로 다른 기능들도 추가하였습니다.
네비게이션 바 우측의 +버튼을 누르면 사진 화면 라이브러리를 띄운다. 사진을 선택하면 선택한 사진이 원래 화면에 보인다. 필터 적용 버튼을 누르면 필터가 적용된다.
위의 화면처럼 UI를 세팅하였다.
# Pods for HelloRxSwift
pod 'RxSwift'
pod 'Snap'
요청이 허용된 후 우선 요청 결과에 관계 없이 PhotoCollectionView와 연결하였다.
weak reference(약한 참조)는 Reference Cycle(강력 순환 참조)를 벗어나기 위해 사용한다. 클로저 내부에 self를 사용하는 경우가 존재하는데, 특수한 상황에서는 문제가 될 소지가 있다.
class Thing {
var disposable: Disposable?
var total: Int = 0
deinit {
disposable?.dispose()
}
init(producer: SignalProducer<Int, NoError>) {
disposable = producer.startWithNext { number in
self.total += number print(self.total)
}
}
}
클로저 내부에 total를 사용하기 위해 self를 명시해 주고 있다. self는 retain count 를 증가시키게 되는데 위의 코드 역시 클로저가 self를 해제하여 retain count를 다시 낮춰준다면 문제가 없이 작동하게 된다. 그러나 closure에 대한 참조가 disposable 프로퍼티에 의해 붙잡혀 있다면 클로저는 self가 해제 될 때까지 기다리고 self는 클로저가 해제될 때까지 기다리는 strong reference cycle 상황이 발생한다. 이러한 상황을 해결하기 위해 [weak self]를 사용한다.
disposable = producer.startWithNext { [weak self] number in
self?.total += number
print(self?.total)
}
출처: https://greenchobo.tistory.com/3
요청을 한 후 허용되면 사진을 가져온다.
private var photoAssets: [PHAsset] = []
func requestPhotoAuthorization() {
PHPhotoLibrary.requestAuthorization { [weak self] status in
if status == .authorized {
let assets = PHAsset.fetchAssets(with: .image, options: nil)
assets.enumerateObjects { asset, count, unSafePointer in
self?.photoAssets.append(asset)
}
}
}
}
컬렉션뷰에서 데이터를 로드하는 함수는 메인 스레드에서만 사용해야 한다. 따라서 main에서 실행되도록 아래의 함수를 생성하였다.
func reloadCollectionView() {
DispatchQueue.main.async {
self.collectionView.reloadData()
}
}
시간순서대로 photoAssets 폴더에 PHAsset이 저장된다. 최신 사진부터 컬렉션뷰셀에 띄우려고 한다. 배열을 뒤집는 방법도 있겠지만, 그것보다는 배열의 총 길이에서 indexPath.row에서 1을 뺀 값을 cell에 주는 방법이 더 효율적이다.
private var photoAssets: [PHAsset] = []
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
guard let cell = collectionView.dequeueReusableCell...
let asset = photoAssets[photoAssets.count - indexPath.row - 1]
let width = ...
let size = ...
PHImageManager.default().requestImage(for: asset, targetSize: size, contentMode: .aspectFit, options: nil) { image, _ in
cell.photo = image
}
return cell
}
private let selectedPhotoSubject = PublishSubject<UIImage>()
var selectedPhoto: Observable<UIImage> {
return selectedPhotoSubject.asObservable()
}
private let selectedPhotoSubject = PublishSubject<UIImage>()
private var selectedPhoto: Observable<UIImage> {
return selectedPhotoSubject.asObservable()
}
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
let selectedAsset = photoAssets[indexPath.row]
let width = (collectionView.frame.width - ((column - 1) * padding)) / column
let size = CGSize(width: width, height: width)
PHImageManager.default().requestImage(for: selectedAsset, targetSize: size, contentMode: .aspectFit, options: nil) { [weak self] image, info in
guard let image = image, let info = info, let isDegradedImage = info["PHImageResultIsDegradedKey"] as? Bool else { return }
if !isDegradedImage {
self?.selectedPhotoSubject.onNext(image)
self?.navigationController?.popViewController(animated: true)
}
}
}
Subject가 신문이라면 Observable는 관찰할 수 있는 신문기사이다. 그리고 그 기사를 구독하는 구독자가 필요하다.
Subject라는 신문을 만들고, selectedPhoto라는 변수의 이벤트가 있다. 그러나 현재는 구독자가 없다.
여기에서 구독자는 메인뷰이다(코드 참조, 포토뷰에서 이미지를 선택하면 메인 뷰로 다시 이동해 해당 이미지를 띄운다). 포토뷰의 selectedPhoto의 변화 이벤트를 관찰한다.
아래는 메인뷰의 + 버튼이며 이 버튼을 누르면 포토뷰로 이동한다. 포토뷰의 selectedPhoto를 구독하고 포토뷰로 이동한다.
포토뷰에서 selectedPhoto값이 바뀌면, 그 이벤트를 관찰해 자신의 이미지뷰에 띄운다.
@objc func addBtnTapped() {
let photoVC = PhotoCollectionViewController()
photoVC.selectedPhoto.subscribe(onNext: { [weak self] image in
self?.photoImageView.image = image
}).disposed(by: disposeBag)
navigationController?.pushViewController(photoVC, animated: true)
}
처음에 필터버튼을 정의할 때 isHidden을 true로 지정한 후, 위에서 photo를 이미지뷰에 띄운 이후에 필터버튼의 isHidden을 false로 바꾼다.
private let filterBtn: UIButton = {
let btn = UIButton(type: .system)
btn.setTitle("Filter", for: .normal)
...
btn.isHidden = true
return btn
}()
...
@objc func addBtnTapped() {
let photoVC = PhotoCollectionViewController()
photoVC.selectedPhoto.subscribe(onNext: { [weak self] image in
self?.photoImageView.image = image
self?.filterBtn.isHidden = false
}).disposed(by: disposeBag)
navigationController?.pushViewController(photoVC, animated: true)
}
FilterService라는 클래스를 따로 만들어 필터 관련 기능들을 관리한다. 이 클래스에 applyFilter함수를 추가하였다. 이 함수는 이미지를 받아 필터를 적용하는 함수이다.
func applyFilter(to inputImage: UIImage, completion: @escaping ((UIImage) -> ())) {
guard let filter = CIFilter(name: PhotoFilter.shared.halftone) else { return }
filter.setValue(5.0, forKey: kCIInputWidthKey)
let sourceImage = CIImage(image: inputImage)
filter.setValue(sourceImage, forKey: kCIInputImageKey)
guard let outputImage = filter.outputImage else { return }
let formRect = outputImage.extent
guard let cgImage = context.createCGImage(outputImage, from: formRect) else { return }
let processedImage = UIImage(cgImage: cgImage, scale: inputImage.scale, orientation: inputImage.imageOrientation)
completion(processedImage)
}
필터 종류는 아래에서 확인할 수 있다.
기존 코드
수정 코드
// 기존 코드
final class FilterService {
...
func applyFilter(to inputImage: UIImage, filterKey: String, completion: @escaping ((UIImage) -> ())) {
...
completion(processedImage)
}
}
// 수정 코드
import RxSwift
final class FilterService {
...
func applyFilter(to inputImage: UIImage, filterKey: String) -> Observable<UIImage> {
return Observable<UIImage>.create { observer in
self.applyFilterToImage(to: inputImage, filterKey: filterKey) { filteredImage in
observer.onNext(filteredImage)
}
return Disposables.create()
}
}
private func applyFilterToImage(to inputImage: UIImage, filterKey: String, completion: @escaping ((UIImage) -> ())) {
...
completion(processedImage)
}
}
기존 코드
수정 코드
// 기존 코드
func applyFilter() {
guard let selectedImage = selectedImage else { return }
FilterService.shared.applyFilter(to: selectedImage, filterKey: filter.CIKey) { [weak self] filteredImage in
self?.photoImageView.image = filteredImage
}
}
// 수정 코드
func applyFilter() {
guard let selectedImage = selectedImage else { return }
FilterService.shared.applyFilter(to: selectedImage, filterKey: filter.CIKey)
.subscribe(onNext: { [weak self] filteredImage in
self?.photoImageView.image = filteredImage
}).disposed(by: disposeBag)
}
전체 코드는 아래의 깃허브 저장소에서 확인해주세요.