diff --git a/Papr.xcodeproj/project.pbxproj b/Papr.xcodeproj/project.pbxproj index 81f7dab..59c596c 100644 --- a/Papr.xcodeproj/project.pbxproj +++ b/Papr.xcodeproj/project.pbxproj @@ -9,6 +9,9 @@ /* Begin PBXBuildFile section */ 1090D75275C09BAE87BE25BD /* Pods_Papr.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D778F2E1EFA76CEB59A377CB /* Pods_Papr.framework */; }; 63CC2B27C8EE9499B1D6C702 /* Pods_PaprTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DD3D8A956FB1C8A8D3B456A7 /* Pods_PaprTests.framework */; }; + AE73E3A0235B0F12002C79D9 /* PHPhotoLibrary+Rx.swift in Sources */ = {isa = PBXBuildFile; fileRef = AE73E39F235B0F12002C79D9 /* PHPhotoLibrary+Rx.swift */; }; + AE73E3A5235B2FCC002C79D9 /* LoadingButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = AE73E3A4235B2FCC002C79D9 /* LoadingButton.swift */; }; + AE73E3A7235B3023002C79D9 /* LoadingButton+Rx.swift in Sources */ = {isa = PBXBuildFile; fileRef = AE73E3A6235B3023002C79D9 /* LoadingButton+Rx.swift */; }; C006934F2189DB4500AC6736 /* CollectionCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = C006934D2189DB4500AC6736 /* CollectionCell.xib */; }; C00739572151547500F51C91 /* LoadingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C00739562151547500F51C91 /* LoadingView.swift */; }; C09D7656216CC91E0035F54D /* UserProfileViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C09D7654216CC91E0035F54D /* UserProfileViewController.swift */; }; @@ -163,6 +166,9 @@ 54A367A7275D753D93ACC1AB /* Pods-Papr.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Papr.debug.xcconfig"; path = "Pods/Target Support Files/Pods-Papr/Pods-Papr.debug.xcconfig"; sourceTree = ""; }; 73C64AFFEE5BB0DA9AE1AB4D /* Pods-Papr.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Papr.release.xcconfig"; path = "Pods/Target Support Files/Pods-Papr/Pods-Papr.release.xcconfig"; sourceTree = ""; }; 7ADA4A068C8B3E3A9F491B6C /* Pods-PaprUITests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-PaprUITests.release.xcconfig"; path = "Pods/Target Support Files/Pods-PaprUITests/Pods-PaprUITests.release.xcconfig"; sourceTree = ""; }; + AE73E39F235B0F12002C79D9 /* PHPhotoLibrary+Rx.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "PHPhotoLibrary+Rx.swift"; sourceTree = ""; }; + AE73E3A4235B2FCC002C79D9 /* LoadingButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadingButton.swift; sourceTree = ""; }; + AE73E3A6235B3023002C79D9 /* LoadingButton+Rx.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "LoadingButton+Rx.swift"; sourceTree = ""; }; C006934D2189DB4500AC6736 /* CollectionCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = CollectionCell.xib; sourceTree = ""; }; C00739562151547500F51C91 /* LoadingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadingView.swift; sourceTree = ""; }; C09D7654216CC91E0035F54D /* UserProfileViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserProfileViewController.swift; sourceTree = ""; }; @@ -331,6 +337,15 @@ name = Frameworks; sourceTree = ""; }; + AE73E3A3235B2FC3002C79D9 /* Views */ = { + isa = PBXGroup; + children = ( + AE73E3A4235B2FCC002C79D9 /* LoadingButton.swift */, + AE73E3A6235B3023002C79D9 /* LoadingButton+Rx.swift */, + ); + path = Views; + sourceTree = ""; + }; C00739552151546100F51C91 /* LoadingView */ = { isa = PBXGroup; children = ( @@ -688,6 +703,7 @@ DCBB1B511FA8B7BA004E95F0 /* Utils */ = { isa = PBXGroup; children = ( + AE73E3A3235B2FC3002C79D9 /* Views */, C0D52CAF2181DA5700FB0517 /* Cache */, DC5AF7531FAD2EE400ADA2BB /* Enums */, DC7CF5851FC468C6006E0A39 /* Authentication */, @@ -804,6 +820,7 @@ DC487F3F2104E99F0042DBBC /* UIScrollView+Rx.swift */, C0F79DEE2167A5080051232D /* RxPinterestLayoutDelegateProxy.swift */, C0F79DF02167A58C0051232D /* PinterestLayout+Rx.swift */, + AE73E39F235B0F12002C79D9 /* PHPhotoLibrary+Rx.swift */, ); path = Rx; sourceTree = ""; @@ -1077,6 +1094,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + AE73E3A5235B2FCC002C79D9 /* LoadingButton.swift in Sources */, DCBB1B241FA7B1BD004E95F0 /* ViewController.swift in Sources */, DCBE008A202A1A980065BEF3 /* Date+Extensions.swift in Sources */, DCAF6BBF204D465E002B7F00 /* Int+Extensions.swift in Sources */, @@ -1141,10 +1159,12 @@ DC922309203F95C300C9D14F /* UIBarButtonItem+Rx.swift in Sources */, DC99FB8E2003A7CC001195FD /* PhotoServiceType.swift in Sources */, DCC8CA132002CC2700F6C540 /* HomeViewCell.swift in Sources */, + AE73E3A0235B0F12002C79D9 /* PHPhotoLibrary+Rx.swift in Sources */, C0CD2446213EB2E40053A802 /* CollectionsViewModel.swift in Sources */, C00739572151547500F51C91 /* LoadingView.swift in Sources */, C0CD244B213FC23B0053A802 /* CollectionCellViewModel.swift in Sources */, DC71C30F1FBDE00A00C8E41A /* UnsplashAuthError.swift in Sources */, + AE73E3A7235B3023002C79D9 /* LoadingButton+Rx.swift in Sources */, DC71C3DB1FBF635C00C8E41A /* UnsplashAccessToken.swift in Sources */, DC66E66120A4D003005E5E31 /* SearchServiceType.swift in Sources */, DC2E76F8203B6DB90023A173 /* LikeUnlike.swift in Sources */, diff --git a/Papr/Info.plist b/Papr/Info.plist index 2b29e97..49a3d01 100644 --- a/Papr/Info.plist +++ b/Papr/Info.plist @@ -64,6 +64,8 @@ UNSPLASH_CLIENT_ID $(UNSPLASH_CLIENT_ID) + NSPhotoLibraryAddUsageDescription + Saving images on a device disk UNSPLASH_CLIENT_SECRET $(UNSPLASH_CLIENT_SECRET) diff --git a/Papr/Scenes/Home/Cell/Footer/HomeViewCellFooter.swift b/Papr/Scenes/Home/Cell/Footer/HomeViewCellFooter.swift index 400e528..134ca81 100644 --- a/Papr/Scenes/Home/Cell/Footer/HomeViewCellFooter.swift +++ b/Papr/Scenes/Home/Cell/Footer/HomeViewCellFooter.swift @@ -82,8 +82,8 @@ class HomeViewCellFooter: UIView, BindableType { return button }() - private lazy var downloadButton: UIButton = { - let button = UIButton() + private lazy var downloadButton: LoadingButton = { + let button = LoadingButton() button.tintColor = .black button.setImage(Constants.Appearance.Icon.squareAndArrowDownMedium, for: .normal) return button @@ -110,6 +110,11 @@ class HomeViewCellFooter: UIView, BindableType { } } .disposed(by: disposeBag) + + inputs.writeImageToPhotosAlbumAction.executing + .skip(1) + .bind(to: downloadButton.rx.isLoading) + .disposed(by: disposeBag) Observable.combineLatest(outputs.isLikedByUser, outputs.photo) .bind { [weak self] in diff --git a/Papr/Scenes/Home/Cell/Footer/HomeViewCellFooterModel.swift b/Papr/Scenes/Home/Cell/Footer/HomeViewCellFooterModel.swift index b777eaa..4276eaf 100644 --- a/Papr/Scenes/Home/Cell/Footer/HomeViewCellFooterModel.swift +++ b/Papr/Scenes/Home/Cell/Footer/HomeViewCellFooterModel.swift @@ -114,17 +114,14 @@ class HomeViewCellFooterModel: HomeViewCellFooterModelInput, }() lazy var writeImageToPhotosAlbumAction: Action = { - Action { image in - PHPhotoLibrary.requestAuthorization { [unowned self] authorizationStatus in - if authorizationStatus == .authorized { - self.creationRequestForAsset(from: image) - } else if authorizationStatus == .denied { - self.alertAction.execute(( - title: "Upsss...", - message: "Photo can't be saved! Photo Libray access is denied ⚠️")) - } + Action { [unowned self] image in + self.photoLibrary.rx.authorizationStatus() + .flatMap { status -> Observable in + guard status == .authorized else { + return .error(PhotoError.unauthorized) + } + return self.creationRequestForAsset(from: image) } - return .empty() } }() @@ -139,6 +136,7 @@ class HomeViewCellFooterModel: HomeViewCellFooterModelInput, private let photoService: PhotoServiceType private let photoLibrary: PHPhotoLibrary private let sceneCoordinator: SceneCoordinatorType + private let bag = DisposeBag() private lazy var alertAction: Action<(title: String, message: String), Void> = { Action<(title: String, message: String), Void> { [unowned self] (title, message) in @@ -182,25 +180,38 @@ class HomeViewCellFooterModel: HomeViewCellFooterModelInput, .unwrap() } - private func creationRequestForAsset(from image: UIImage) { - photoLibrary.performChanges({ + private func creationRequestForAsset(from image: UIImage) -> Observable { + let change = photoLibrary.rx.performChanges { PHAssetChangeRequest.creationRequestForAsset(from: image) - }, completionHandler: { [unowned self] success, error in - if success { - self.alertAction.execute(( - title: "Saved to Photos 🎉", - message: "" )) - } - else if let error = error { - self.alertAction.execute(( - title: "Upsss...", - message: error.localizedDescription + "😕")) + }.share() + + // error case + change.materialize() + .map { event -> Error? in + switch event { + case .error(let error): return error + default: return nil } - else { - self.alertAction.execute(( - title: "Upsss...", - message: "Unknown error 😱")) - } - }) + }.unwrap() + .map { error -> (String, String) in + switch error { + case PhotoError.unauthorized: + return (title: "Upsss...", + message: "Photo can't be saved! Photo Libray access is denied ⚠️") + default: + return (title: "Upsss...", + message: error.localizedDescription + "😕") + } + }.bind(to: alertAction.inputs) + .disposed(by: bag) + + // success case + change.map { + (title: "Saved to Photos 🎉", + message: "" ) + }.bind(to: alertAction.inputs) + .disposed(by: bag) + + return change } } diff --git a/Papr/Utils/Extentions/Rx/PHPhotoLibrary+Rx.swift b/Papr/Utils/Extentions/Rx/PHPhotoLibrary+Rx.swift new file mode 100644 index 0000000..29afa2e --- /dev/null +++ b/Papr/Utils/Extentions/Rx/PHPhotoLibrary+Rx.swift @@ -0,0 +1,48 @@ +// +// PHPhotoLibrary+Rx.swift +// Papr +// +// Created by Piotr on 19/10/2019. +// Copyright © 2019 Joan Disho. All rights reserved. +// + +import Photos +import RxSwift + +enum PhotoError: Error { + case unknown + case unauthorized +} + +extension Reactive where Base: PHPhotoLibrary { + func performChanges(_ changeBlock: @escaping () -> Void) -> Observable { + return Observable.create { observer in + self.base.performChanges(changeBlock) { success, error in + guard success else { + if let error = error { + observer.onError(error) + } else { + observer.onError(PhotoError.unknown) + } + return + } + + observer.onNext(()) + observer.onCompleted() + } + + return Disposables.create() + }.observeOn(MainScheduler.instance) + } + + func authorizationStatus() -> Observable { + return Observable.create { observer in + PHPhotoLibrary.requestAuthorization { status in + observer.onNext(status) + observer.onCompleted() + } + + return Disposables.create() + } + } +} diff --git a/Papr/Utils/Views/LoadingButton+Rx.swift b/Papr/Utils/Views/LoadingButton+Rx.swift new file mode 100644 index 0000000..6ae8a3a --- /dev/null +++ b/Papr/Utils/Views/LoadingButton+Rx.swift @@ -0,0 +1,18 @@ +// +// LoadingButton+Rx.swift +// Papr +// +// Created by Piotr on 19/10/2019. +// Copyright © 2019 Joan Disho. All rights reserved. +// + +import RxSwift +import RxCocoa + +extension Reactive where Base: LoadingButton { + var isLoading: Binder { + return Binder(base) { view, isLoading in + view.isLoading = isLoading + } + } +} diff --git a/Papr/Utils/Views/LoadingButton.swift b/Papr/Utils/Views/LoadingButton.swift new file mode 100644 index 0000000..5a15c6b --- /dev/null +++ b/Papr/Utils/Views/LoadingButton.swift @@ -0,0 +1,48 @@ +// +// LoadingButton.swift +// Papr +// +// Created by Piotr on 19/10/2019. +// Copyright © 2019 Joan Disho. All rights reserved. +// + +import UIKit + +class LoadingButton: UIButton { + private var originalImage: UIImage? + + var isLoading: Bool = false { + didSet { + isEnabled = !isLoading + if isLoading { + originalImage = image(for: .normal) + setImage(nil, for: .normal) + activityIndicator.startAnimating() + } else { + setImage(originalImage, for: .normal) + activityIndicator.stopAnimating() + } + } + } + + override func setImage(_ image: UIImage?, for state: UIControl.State) { + super.setImage(image, for: state) + if let image = image { + originalImage = image + } + } + + private lazy var activityIndicator: UIActivityIndicatorView = { + let activityIndicator = UIActivityIndicatorView() + activityIndicator.translatesAutoresizingMaskIntoConstraints = false + activityIndicator.color = .black + addSubview(activityIndicator) + + NSLayoutConstraint.activate([ + activityIndicator.centerYAnchor.constraint(equalTo: centerYAnchor), + activityIndicator.centerXAnchor.constraint(equalTo: centerXAnchor) + ]) + + return activityIndicator + }() +}