From e63b9ba9b5e479aa89812f058303ef1ffba00378 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Mro=CC=81z?= Date: Sat, 23 Jan 2021 16:19:05 +0100 Subject: [PATCH] Add a bit rough progress indicator at tree cell --- Tree Tracker/Models/ListSection.swift | 18 +++++- .../Trees/TreeCollectionViewCell.swift | 27 +++++++- .../Screens/Trees/TreesListItem.swift | 4 +- .../Screens/Trees/TreesViewController.swift | 4 +- .../Screens/Trees/TreesViewModel.swift | 1 + .../UploadList/UploadListViewController.swift | 4 +- .../UploadList/UploadListViewModel.swift | 64 ++++++++++++------- Tree Tracker/Services/Api.swift | 33 ++++++---- .../UI/CollectionViewDataSource.swift | 3 +- Tree Tracker/UI/TableViewDataSource.swift | 3 +- 10 files changed, 115 insertions(+), 46 deletions(-) diff --git a/Tree Tracker/Models/ListSection.swift b/Tree Tracker/Models/ListSection.swift index fb06756..0c3f5b7 100644 --- a/Tree Tracker/Models/ListSection.swift +++ b/Tree Tracker/Models/ListSection.swift @@ -1,6 +1,6 @@ import Foundation -enum ListSection: Hashable, Identifiable { +enum ListSection: Hashable, Identifiable { case titled(String, [ListItem]) case untitled(id: String = "untitled", [ListItem]) @@ -28,4 +28,20 @@ enum ListSection: Hashable, Identifiable { return .untitled(id: id, items) } } + + func section(replacing item: ListItem) -> ListSection { + var newItems = items + guard let index = newItems.firstIndex(where: { $0.id == item.id }) else { + return self + } + + newItems[index] = item + + switch self { + case let .titled(title, _): + return .titled(title, newItems) + case let .untitled(id, _): + return .untitled(id: id, newItems) + } + } } diff --git a/Tree Tracker/Screens/Trees/TreeCollectionViewCell.swift b/Tree Tracker/Screens/Trees/TreeCollectionViewCell.swift index dfab274..53f3d37 100644 --- a/Tree Tracker/Screens/Trees/TreeCollectionViewCell.swift +++ b/Tree Tracker/Screens/Trees/TreeCollectionViewCell.swift @@ -20,6 +20,16 @@ final class TreeCollectionViewCell: UICollectionViewCell, Reusable { return view }() + private var progress: UIView = { + let view = UIView() + view.translatesAutoresizingMaskIntoConstraints = false + view.backgroundColor = .green + view.layer.cornerRadius = 4.0 + view.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner] + + return view + }() + private let infoOverlay: UIView = { let view = UIView() view.translatesAutoresizingMaskIntoConstraints = false @@ -40,6 +50,8 @@ final class TreeCollectionViewCell: UICollectionViewCell, Reusable { return label }() + private lazy var progressWidthConstraint = progress.widthAnchor.constraint(equalToConstant: 0.0) + private var imageLoader: AnyImageLoader? private var tapAction: Action? @@ -57,13 +69,18 @@ final class TreeCollectionViewCell: UICollectionViewCell, Reusable { private func setup() { contentView.addSubview(wrapper) - wrapper.add(subviews: imageView, infoOverlay) + wrapper.add(subviews: imageView, progress, infoOverlay) infoOverlay.addSubview(infoLabel) wrapper.pin(to: contentView) imageView.pin(to: wrapper) NSLayoutConstraint.activate([ + progress.leadingAnchor.constraint(equalTo: wrapper.leadingAnchor), + progress.topAnchor.constraint(equalTo: wrapper.topAnchor), + progress.heightAnchor.constraint(equalToConstant: 5.0), + progressWidthConstraint, + infoLabel.leadingAnchor.constraint(equalTo: infoOverlay.leadingAnchor, constant: 8.0), infoLabel.trailingAnchor.constraint(equalTo: infoOverlay.trailingAnchor, constant: -8.0), infoLabel.topAnchor.constraint(equalTo: infoOverlay.topAnchor, constant: 8.0), @@ -75,7 +92,7 @@ final class TreeCollectionViewCell: UICollectionViewCell, Reusable { ]) } - func set(imageLoader: AnyImageLoader?, info: String, detail: String?, tapAction: Action?) { + func set(imageLoader: AnyImageLoader?, progress: Double, info: String, detail: String?, tapAction: Action?) { self.imageLoader = imageLoader self.tapAction = tapAction @@ -88,6 +105,12 @@ final class TreeCollectionViewCell: UICollectionViewCell, Reusable { self.imageView.image = image } + + progressWidthConstraint.constant = CGFloat(progress) * imageView.bounds.width + + UIView.animate(withDuration: 0.1) { + self.progress.layoutIfNeeded() + } } override func touchesEnded(_ touches: Set, with event: UIEvent?) { diff --git a/Tree Tracker/Screens/Trees/TreesListItem.swift b/Tree Tracker/Screens/Trees/TreesListItem.swift index d8638d6..c6f0d01 100644 --- a/Tree Tracker/Screens/Trees/TreesListItem.swift +++ b/Tree Tracker/Screens/Trees/TreesListItem.swift @@ -1,11 +1,11 @@ import UIKit enum TreesListItem: Identifiable, Hashable { - case tree(id: String, imageLoader: AnyImageLoader?, info: String, detail: String?, tapAction: Action?) + case tree(id: String, imageLoader: AnyImageLoader?, progress: Double, info: String, detail: String?, tapAction: Action?) var id: String { switch self { - case let .tree(id, _, _, _, _): return id + case let .tree(id, _, _, _, _, _): return id } } } diff --git a/Tree Tracker/Screens/Trees/TreesViewController.swift b/Tree Tracker/Screens/Trees/TreesViewController.swift index 4522834..d455da4 100644 --- a/Tree Tracker/Screens/Trees/TreesViewController.swift +++ b/Tree Tracker/Screens/Trees/TreesViewController.swift @@ -106,9 +106,9 @@ final class TreesViewController: UIViewController { private func buildDataSource() -> CollectionViewDataSource { return CollectionViewDataSource(collectionView: collectionView, cellTypes: [TreeCollectionViewCell.self]) { collectionView, indexPath, model -> UICollectionViewCell? in switch model { - case let .tree(_, imageLoader, info, detail, tapAction): + case let .tree(_, imageLoader, progress, info, detail, tapAction): let cell = collectionView.dequeue(cell: TreeCollectionViewCell.self, indexPath: indexPath) - cell.set(imageLoader: imageLoader, info: info, detail: detail, tapAction: tapAction) + cell.set(imageLoader: imageLoader, progress: progress, info: info, detail: detail, tapAction: tapAction) return cell } diff --git a/Tree Tracker/Screens/Trees/TreesViewModel.swift b/Tree Tracker/Screens/Trees/TreesViewModel.swift index aa71c80..8365acc 100644 --- a/Tree Tracker/Screens/Trees/TreesViewModel.swift +++ b/Tree Tracker/Screens/Trees/TreesViewModel.swift @@ -55,6 +55,7 @@ final class TreesViewModel { let imageLoader = (tree.thumbnailUrl ?? tree.imageUrl).map { AnyImageLoader(imageLoader: URLImageLoader(url: $0)) } return .tree(id: "\(tree.id)", imageLoader: imageLoader, + progress: 0, info: tree.species, detail: tree.supervisor, tapAction: Action(id: "tree_action_\(tree.id)") { diff --git a/Tree Tracker/Screens/UploadList/UploadListViewController.swift b/Tree Tracker/Screens/UploadList/UploadListViewController.swift index 97c3bac..0b2121c 100644 --- a/Tree Tracker/Screens/UploadList/UploadListViewController.swift +++ b/Tree Tracker/Screens/UploadList/UploadListViewController.swift @@ -111,9 +111,9 @@ final class UploadListViewController: UIViewController { private func buildDataSource() -> CollectionViewDataSource { return CollectionViewDataSource(collectionView: collectionView, cellTypes: [TreeCollectionViewCell.self]) { collectionView, indexPath, model -> UICollectionViewCell? in switch model { - case let .tree(_, imageLoader, info, detail, tapAction): + case let .tree(_, imageLoader, progress, info, detail, tapAction): let cell = collectionView.dequeue(cell: TreeCollectionViewCell.self, indexPath: indexPath) - cell.set(imageLoader: imageLoader, info: info, detail: detail, tapAction: tapAction) + cell.set(imageLoader: imageLoader, progress: progress, info: info, detail: detail, tapAction: tapAction) return cell } diff --git a/Tree Tracker/Screens/UploadList/UploadListViewModel.swift b/Tree Tracker/Screens/UploadList/UploadListViewModel.swift index 270b3c9..a76fa93 100644 --- a/Tree Tracker/Screens/UploadList/UploadListViewModel.swift +++ b/Tree Tracker/Screens/UploadList/UploadListViewModel.swift @@ -72,36 +72,56 @@ final class UploadListViewModel { guard let tree = trees.first else { return } print("Now uploading tree: \(tree)") - self?.currentUpload = self?.api.upload(tree: tree, completion: { result in - switch result { - case let .success(airtableTree): - print("Successfully uploaded tree.") - self?.database.save([airtableTree]) - self?.database.remove(tree: tree) { - self?.presentTreesFromDatabase() - self?.uploadLocalTreesRecursively() + self?.currentUpload = self?.api.upload( + tree: tree, + progress: { progress in + NSLog("progress: \(progress)") + self?.update(uploadProgress: progress, for: tree) + }, + completion: { result in + switch result { + case let .success(airtableTree): + print("Successfully uploaded tree.") + self?.database.save([airtableTree]) + self?.database.remove(tree: tree) { + self?.presentTreesFromDatabase() + self?.uploadLocalTreesRecursively() + } + case let .failure(error): + print("Error when uploading a local tree: \(error)") } - case let .failure(error): - print("Error when uploading a local tree: \(error)") } - }) + ) } } + private func update(uploadProgress: Double, for tree: LocalTree) { + guard let section = data.first else { return } + + let newItem = buildItem(tree: tree, progress: uploadProgress) + let newSection = section.section(replacing: newItem) + data = [newSection] + } + private func presentTreesFromDatabase() { database.fetchLocalTrees { [weak self] trees in - self?.data = [.untitled(id: "trees", trees.map { tree in - let imageLoader = AnyImageLoader(imageLoader: PHImageLoader(phImageId: tree.phImageId)) - return .tree(id: tree.phImageId, - imageLoader: imageLoader, - info: tree.species, - detail: tree.supervisor, - tapAction: Action(id: "tree_action_\(tree.phImageId)") { - self?.navigation?.triggerEditDetailsFlow(tree: tree) { - self?.loadData() - } - }) + self?.data = [.untitled(id: "trees", trees.compactMap { tree in + return self?.buildItem(tree: tree, progress: 0.0) })] } } + + private func buildItem(tree: LocalTree, progress: Double) -> TreesListItem { + let imageLoader = AnyImageLoader(imageLoader: PHImageLoader(phImageId: tree.phImageId)) + return .tree(id: tree.phImageId, + imageLoader: imageLoader, + progress: progress, + info: tree.species, + detail: tree.supervisor, + tapAction: Action(id: "tree_action_\(tree.phImageId)") { [weak self] in + self?.navigation?.triggerEditDetailsFlow(tree: tree) { + self?.loadData() + } + }) + } } diff --git a/Tree Tracker/Services/Api.swift b/Tree Tracker/Services/Api.swift index 0f641a8..df40d81 100644 --- a/Tree Tracker/Services/Api.swift +++ b/Tree Tracker/Services/Api.swift @@ -29,9 +29,9 @@ final class Api { } } - func upload(tree: LocalTree, completion: @escaping (Result) -> Void) -> Cancellable { + func upload(tree: LocalTree, progress: @escaping (Double) -> Void = { _ in }, completion: @escaping (Result) -> Void) -> Cancellable { let upload = ImageUpload(tree: tree) - upload.upload(tree: tree, session: session, completion: completion) + upload.upload(tree: tree, progress: progress, session: session, completion: completion) return upload } @@ -54,6 +54,7 @@ final class ImageUpload: Cancellable { private let imageLoader: PHImageLoader private var request: Request? + private var progress: ((Double) -> Void)? private var isCancelled = false init(tree: LocalTree) { @@ -68,7 +69,9 @@ final class ImageUpload: Cancellable { request?.cancel() } - func upload(tree: LocalTree, session: Session, completion: @escaping (Result) -> Void) { + func upload(tree: LocalTree, progress: @escaping (Double) -> Void = { _ in }, session: Session, completion: @escaping (Result) -> Void) { + self.progress = progress + imageLoader.loadHighQualityImage { [weak self] image in guard self?.isCancelled != true else { completion(.failure(AFError.explicitlyCancelled)) @@ -80,6 +83,8 @@ final class ImageUpload: Cancellable { return } + self?.progress?(0.2) + self?.request = self?.upload(image: image, session: session) { result in switch result { case let .success((url, md5)): @@ -102,13 +107,17 @@ final class ImageUpload: Cancellable { } let md5 = data.md5() ?? "" - let request = session.upload( - multipartFormData: { formData in - formData.append(data, withName: "file", fileName: "image.jpg", mimeType: "image/jpg") - formData.append(Constants.Cloudinary.uploadPresetName.data(using: .utf8)!, withName: "upload_preset") - }, - to: Api.Config.Cloudinary.uploadUrl, - method: .post) + let request = session + .upload( + multipartFormData: { formData in + formData.append(data, withName: "file", fileName: "image.jpg", mimeType: "image/jpg") + formData.append(Constants.Cloudinary.uploadPresetName.data(using: .utf8)!, withName: "upload_preset") + }, + to: Api.Config.Cloudinary.uploadUrl, + method: .post + ).uploadProgress { progress in + self.progress?(0.2 + 0.75 * progress.fractionCompleted) + } return request.validate().responseJSON { response in switch response.result { @@ -134,7 +143,9 @@ final class ImageUpload: Cancellable { let airtableTree = tree.toAirtableTree(imageUrl: imageUrl) let request = session.request(Api.Config.treesUrl, method: .post, parameters: airtableTree, encoder: JSONParameterEncoder(encoder: ._iso8601ms), headers: Api.Config.headers, interceptor: nil, requestModifier: nil) - return request.validate().responseDecodable(decoder: JSONDecoder._iso8601ms) { (response: DataResponse) in + return request.validate().responseDecodable(decoder: JSONDecoder._iso8601ms) { [weak self] (response: DataResponse) in + self?.progress?(1.0) + switch response.result { case let .success(tree): print("Tree uploaded!") diff --git a/Tree Tracker/UI/CollectionViewDataSource.swift b/Tree Tracker/UI/CollectionViewDataSource.swift index 9b9e30c..223f998 100644 --- a/Tree Tracker/UI/CollectionViewDataSource.swift +++ b/Tree Tracker/UI/CollectionViewDataSource.swift @@ -1,7 +1,6 @@ import UIKit -final class CollectionViewDataSource: UICollectionViewDiffableDataSource, ListItem> { - +final class CollectionViewDataSource: UICollectionViewDiffableDataSource, ListItem> { private var data = [ListSection]() private var currentItems: [ListSection] { return snapshot().sectionIdentifiers.map { $0.section(with: snapshot().itemIdentifiers(inSection: $0)) } diff --git a/Tree Tracker/UI/TableViewDataSource.swift b/Tree Tracker/UI/TableViewDataSource.swift index 1a8e783..9008fdf 100644 --- a/Tree Tracker/UI/TableViewDataSource.swift +++ b/Tree Tracker/UI/TableViewDataSource.swift @@ -1,7 +1,6 @@ import UIKit -final class TableViewDataSource: UITableViewDiffableDataSource, ListItem>, UITableViewDelegate { - +final class TableViewDataSource: UITableViewDiffableDataSource, ListItem>, UITableViewDelegate { private var data = [ListSection]() private var currentItems: [ListSection] { return snapshot().sectionIdentifiers.map { $0.section(with: snapshot().itemIdentifiers(inSection: $0)) }