Skip to content

Commit

Permalink
Add a bit rough progress indicator at tree cell
Browse files Browse the repository at this point in the history
  • Loading branch information
sunshinejr committed Jan 23, 2021
1 parent 6ccf202 commit e63b9ba
Show file tree
Hide file tree
Showing 10 changed files with 115 additions and 46 deletions.
18 changes: 17 additions & 1 deletion Tree Tracker/Models/ListSection.swift
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import Foundation

enum ListSection<ListItem: Hashable>: Hashable, Identifiable {
enum ListSection<ListItem: Hashable & Identifiable>: Hashable, Identifiable {
case titled(String, [ListItem])
case untitled(id: String = "untitled", [ListItem])

Expand Down Expand Up @@ -28,4 +28,20 @@ enum ListSection<ListItem: Hashable>: 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)
}
}
}
27 changes: 25 additions & 2 deletions Tree Tracker/Screens/Trees/TreeCollectionViewCell.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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?

Expand All @@ -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),
Expand All @@ -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

Expand All @@ -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<UITouch>, with event: UIEvent?) {
Expand Down
4 changes: 2 additions & 2 deletions Tree Tracker/Screens/Trees/TreesListItem.swift
Original file line number Diff line number Diff line change
@@ -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
}
}
}
4 changes: 2 additions & 2 deletions Tree Tracker/Screens/Trees/TreesViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -106,9 +106,9 @@ final class TreesViewController: UIViewController {
private func buildDataSource() -> CollectionViewDataSource<TreesListItem> {
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
}
Expand Down
1 change: 1 addition & 0 deletions Tree Tracker/Screens/Trees/TreesViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)") {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -111,9 +111,9 @@ final class UploadListViewController: UIViewController {
private func buildDataSource() -> CollectionViewDataSource<TreesListItem> {
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
}
Expand Down
64 changes: 42 additions & 22 deletions Tree Tracker/Screens/UploadList/UploadListViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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()
}
})
}
}
33 changes: 22 additions & 11 deletions Tree Tracker/Services/Api.swift
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,9 @@ final class Api {
}
}

func upload(tree: LocalTree, completion: @escaping (Result<AirtableTree, AFError>) -> Void) -> Cancellable {
func upload(tree: LocalTree, progress: @escaping (Double) -> Void = { _ in }, completion: @escaping (Result<AirtableTree, AFError>) -> 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
}
Expand All @@ -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) {
Expand All @@ -68,7 +69,9 @@ final class ImageUpload: Cancellable {
request?.cancel()
}

func upload(tree: LocalTree, session: Session, completion: @escaping (Result<AirtableTree, AFError>) -> Void) {
func upload(tree: LocalTree, progress: @escaping (Double) -> Void = { _ in }, session: Session, completion: @escaping (Result<AirtableTree, AFError>) -> Void) {
self.progress = progress

imageLoader.loadHighQualityImage { [weak self] image in
guard self?.isCancelled != true else {
completion(.failure(AFError.explicitlyCancelled))
Expand All @@ -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)):
Expand All @@ -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 {
Expand All @@ -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<AirtableTree, AFError>) in
return request.validate().responseDecodable(decoder: JSONDecoder._iso8601ms) { [weak self] (response: DataResponse<AirtableTree, AFError>) in
self?.progress?(1.0)

switch response.result {
case let .success(tree):
print("Tree uploaded!")
Expand Down
3 changes: 1 addition & 2 deletions Tree Tracker/UI/CollectionViewDataSource.swift
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import UIKit

final class CollectionViewDataSource<ListItem: Hashable>: UICollectionViewDiffableDataSource<ListSection<ListItem>, ListItem> {

final class CollectionViewDataSource<ListItem: Hashable & Identifiable>: UICollectionViewDiffableDataSource<ListSection<ListItem>, ListItem> {
private var data = [ListSection<ListItem>]()
private var currentItems: [ListSection<ListItem>] {
return snapshot().sectionIdentifiers.map { $0.section(with: snapshot().itemIdentifiers(inSection: $0)) }
Expand Down
3 changes: 1 addition & 2 deletions Tree Tracker/UI/TableViewDataSource.swift
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import UIKit

final class TableViewDataSource<ListItem: Hashable>: UITableViewDiffableDataSource<ListSection<ListItem>, ListItem>, UITableViewDelegate {

final class TableViewDataSource<ListItem: Hashable & Identifiable>: UITableViewDiffableDataSource<ListSection<ListItem>, ListItem>, UITableViewDelegate {
private var data = [ListSection<ListItem>]()
private var currentItems: [ListSection<ListItem>] {
return snapshot().sectionIdentifiers.map { $0.section(with: snapshot().itemIdentifiers(inSection: $0)) }
Expand Down

0 comments on commit e63b9ba

Please sign in to comment.