Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added loading indicator while downloading an image on home #95

Open
wants to merge 1 commit into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions Papr.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -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 */; };
Expand Down Expand Up @@ -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 = "<group>"; };
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 = "<group>"; };
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 = "<group>"; };
AE73E39F235B0F12002C79D9 /* PHPhotoLibrary+Rx.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "PHPhotoLibrary+Rx.swift"; sourceTree = "<group>"; };
AE73E3A4235B2FCC002C79D9 /* LoadingButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadingButton.swift; sourceTree = "<group>"; };
AE73E3A6235B3023002C79D9 /* LoadingButton+Rx.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "LoadingButton+Rx.swift"; sourceTree = "<group>"; };
C006934D2189DB4500AC6736 /* CollectionCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = CollectionCell.xib; sourceTree = "<group>"; };
C00739562151547500F51C91 /* LoadingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadingView.swift; sourceTree = "<group>"; };
C09D7654216CC91E0035F54D /* UserProfileViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserProfileViewController.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -331,6 +337,15 @@
name = Frameworks;
sourceTree = "<group>";
};
AE73E3A3235B2FC3002C79D9 /* Views */ = {
isa = PBXGroup;
children = (
AE73E3A4235B2FCC002C79D9 /* LoadingButton.swift */,
AE73E3A6235B3023002C79D9 /* LoadingButton+Rx.swift */,
);
path = Views;
sourceTree = "<group>";
};
C00739552151546100F51C91 /* LoadingView */ = {
isa = PBXGroup;
children = (
Expand Down Expand Up @@ -688,6 +703,7 @@
DCBB1B511FA8B7BA004E95F0 /* Utils */ = {
isa = PBXGroup;
children = (
AE73E3A3235B2FC3002C79D9 /* Views */,
C0D52CAF2181DA5700FB0517 /* Cache */,
DC5AF7531FAD2EE400ADA2BB /* Enums */,
DC7CF5851FC468C6006E0A39 /* Authentication */,
Expand Down Expand Up @@ -804,6 +820,7 @@
DC487F3F2104E99F0042DBBC /* UIScrollView+Rx.swift */,
C0F79DEE2167A5080051232D /* RxPinterestLayoutDelegateProxy.swift */,
C0F79DF02167A58C0051232D /* PinterestLayout+Rx.swift */,
AE73E39F235B0F12002C79D9 /* PHPhotoLibrary+Rx.swift */,
);
path = Rx;
sourceTree = "<group>";
Expand Down Expand Up @@ -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 */,
Expand Down Expand Up @@ -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 */,
Expand Down
2 changes: 2 additions & 0 deletions Papr/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,8 @@
</array>
<key>UNSPLASH_CLIENT_ID</key>
<string>$(UNSPLASH_CLIENT_ID)</string>
<key>NSPhotoLibraryAddUsageDescription</key>
<string>Saving images on a device disk</string>
<key>UNSPLASH_CLIENT_SECRET</key>
<string>$(UNSPLASH_CLIENT_SECRET)</string>
</dict>
Expand Down
9 changes: 7 additions & 2 deletions Papr/Scenes/Home/Cell/Footer/HomeViewCellFooter.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
67 changes: 39 additions & 28 deletions Papr/Scenes/Home/Cell/Footer/HomeViewCellFooterModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -114,17 +114,14 @@ class HomeViewCellFooterModel: HomeViewCellFooterModelInput,
}()

lazy var writeImageToPhotosAlbumAction: Action<UIImage, Void> = {
Action<UIImage, Void> { 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<UIImage, Void> { [unowned self] image in
self.photoLibrary.rx.authorizationStatus()
.flatMap { status -> Observable<Void> in
guard status == .authorized else {
return .error(PhotoError.unauthorized)
}
return self.creationRequestForAsset(from: image)
}
return .empty()
}
}()

Expand All @@ -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
Expand Down Expand Up @@ -182,25 +180,38 @@ class HomeViewCellFooterModel: HomeViewCellFooterModelInput,
.unwrap()
}

private func creationRequestForAsset(from image: UIImage) {
photoLibrary.performChanges({
private func creationRequestForAsset(from image: UIImage) -> Observable<Void> {
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
}
}
48 changes: 48 additions & 0 deletions Papr/Utils/Extentions/Rx/PHPhotoLibrary+Rx.swift
Original file line number Diff line number Diff line change
@@ -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<Void> {
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<PHAuthorizationStatus> {
return Observable<PHAuthorizationStatus>.create { observer in
PHPhotoLibrary.requestAuthorization { status in
observer.onNext(status)
observer.onCompleted()
}

return Disposables.create()
}
}
}
18 changes: 18 additions & 0 deletions Papr/Utils/Views/LoadingButton+Rx.swift
Original file line number Diff line number Diff line change
@@ -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<Bool> {
return Binder(base) { view, isLoading in
view.isLoading = isLoading
}
}
}
48 changes: 48 additions & 0 deletions Papr/Utils/Views/LoadingButton.swift
Original file line number Diff line number Diff line change
@@ -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
}()
}