From 692fd99c5d54ad17be8eaf82d74676b9c559ee8e Mon Sep 17 00:00:00 2001 From: Javier Cicchelli Date: Sat, 17 Dec 2022 11:40:45 +0100 Subject: [PATCH 1/5] Renamed the InputAlertView component in the Browse module as InputAlert. --- .../Browse/UI/Components/InputAlert.swift | 134 ++++++++++++++++++ 1 file changed, 134 insertions(+) create mode 100644 Modules/Sources/Browse/UI/Components/InputAlert.swift diff --git a/Modules/Sources/Browse/UI/Components/InputAlert.swift b/Modules/Sources/Browse/UI/Components/InputAlert.swift new file mode 100644 index 0000000..1b5d071 --- /dev/null +++ b/Modules/Sources/Browse/UI/Components/InputAlert.swift @@ -0,0 +1,134 @@ +// +// InputAlert.swift +// Browse +// +// Created by Javier Cicchelli on 16/12/2022. +// Copyright © 2022 Röck+Cöde. All rights reserved. +// + +import DataModels +import SwiftUI +import UIKit + +struct InputAlert: UIViewControllerRepresentable { + + // MARK: Bindings + + @Binding private var isPresenting: Bool + @Binding private var textFieldString: String + + // MARK: Properties + + private let title: String + private let message: String + private let textFieldPlaceholder: String + private let actions: [UIAlertAction] + + // MARK: Initialisers + + init( + isPresenting: Binding, + title: String, + message: String, + textFieldPlaceholder: String, + textFieldString: Binding, + actions: [UIAlertAction] + ) { + self.title = title + self.message = message + self.textFieldPlaceholder = textFieldPlaceholder + self.actions = actions + + self._isPresenting = isPresenting + self._textFieldString = textFieldString + } + + // MARK: Functions + + func makeUIViewController( + context: UIViewControllerRepresentableContext + ) -> UIViewController { + .init() + } + + func updateUIViewController( + _ viewController: UIViewController, + context: UIViewControllerRepresentableContext + ) { + guard + context.coordinator.alert == nil, + isPresenting + else { return } + + let alertController = { + let alert = UIAlertController( + title: title, + message: message, + preferredStyle: .alert + ) + + alert.addTextField { textField in + textField.placeholder = textFieldPlaceholder + textField.text = textFieldString + textField.delegate = context.coordinator + } + + actions.forEach { action in + alert.addAction(action) + } + + return alert + }() + + context.coordinator.alert = alertController + + Task { @MainActor in + viewController.present( + alertController, + animated: true + ) { + isPresenting = false + context.coordinator.alert = nil + } + } + } + + func makeCoordinator() -> Coordinator { + .init(self) + } + +} + +// MARK: - Coordinator + +extension InputAlert { + class Coordinator: NSObject, UITextFieldDelegate { + + // MARK: Properties + + var alert: UIAlertController? + + private let parent: InputAlert + + // MARK: Initialisers + + init(_ parent: InputAlert) { + self.parent = parent + } + + // MARK: UITextFieldDelegate + + func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool { + parent.textFieldString = { + if let text = textField.text as NSString? { + return text.replacingCharacters(in: range, with: string) + } else { + return .empty + } + }() + + return true + } + + } +} From c968982355c85af51edefbf9f8c7ca27de7df93c Mon Sep 17 00:00:00 2001 From: Javier Cicchelli Date: Sat, 17 Dec 2022 11:48:25 +0100 Subject: [PATCH 2/5] Implemented the DocumentPicker component for the Browse module. --- .../Browse/UI/Components/DocumentPicker.swift | 91 +++++++++++++++++++ 1 file changed, 91 insertions(+) create mode 100644 Modules/Sources/Browse/UI/Components/DocumentPicker.swift diff --git a/Modules/Sources/Browse/UI/Components/DocumentPicker.swift b/Modules/Sources/Browse/UI/Components/DocumentPicker.swift new file mode 100644 index 0000000..b7b1188 --- /dev/null +++ b/Modules/Sources/Browse/UI/Components/DocumentPicker.swift @@ -0,0 +1,91 @@ +// +// DocumentPicker.swift +// Browse +// +// Created by Javier Cicchelli on 17/12/2022. +// Copyright © 2022 Röck+Cöde. All rights reserved. +// + +import Foundation +import SwiftUI +import UIKit + +struct DocumentPicker: UIViewControllerRepresentable { + + // MARK: Type aliases + + typealias SelectedClosure = ([URL]) -> Void + typealias CancelledClosure = () -> Void + + // MARK: Properties + + private let selected: SelectedClosure + private let cancelled: CancelledClosure? + + // MARK: Initialisers + + init( + selected: @escaping SelectedClosure, + cancelled: CancelledClosure? = nil + ) { + self.selected = selected + self.cancelled = cancelled + } + + // MARK: Functions + + func makeUIViewController(context: Context) -> UIDocumentPickerViewController { + let controller = UIDocumentPickerViewController( + forOpeningContentTypes: [.item], + asCopy: true + ) + + controller.allowsMultipleSelection = false + controller.shouldShowFileExtensions = false + controller.delegate = context.coordinator + + return controller + } + + func updateUIViewController( + _ uiViewController: UIDocumentPickerViewController, + context: Context + ) { } + + func makeCoordinator() -> Coordinator { + .init(self) + } +} + +// MARK: - Coordinators + +extension DocumentPicker { + class Coordinator: NSObject, UIDocumentPickerDelegate { + + // MARK: Properties + + private var parent: DocumentPicker + + // MARK: Initialisers + + init(_ parent: DocumentPicker) { + self.parent = parent + } + + // MARK: UIDocumentPickerDelegate + + func documentPicker( + _ controller: UIDocumentPickerViewController, + didPickDocumentsAt urls: [URL] + ) { + parent.selected(urls) + } + + func documentPickerWasCancelled(_ controller: UIDocumentPickerViewController) { + controller.dismiss(animated: true) { [weak self] in + self?.parent.cancelled?() + } + } + + } +} From b35544b0142d9b8799407723acc97d8066f8bb5c Mon Sep 17 00:00:00 2001 From: Javier Cicchelli Date: Sat, 17 Dec 2022 11:56:41 +0100 Subject: [PATCH 3/5] Implemented the UploadFileUseCase use case for the Browse module. --- .../Browse/Logic/Adapters/FileAdapter.swift | 41 +++++++++++++++ .../Logic/Use Cases/UploadFileUseCase.swift | 50 +++++++++++++++++++ 2 files changed, 91 insertions(+) create mode 100644 Modules/Sources/Browse/Logic/Adapters/FileAdapter.swift create mode 100644 Modules/Sources/Browse/Logic/Use Cases/UploadFileUseCase.swift diff --git a/Modules/Sources/Browse/Logic/Adapters/FileAdapter.swift b/Modules/Sources/Browse/Logic/Adapters/FileAdapter.swift new file mode 100644 index 0000000..ff768fe --- /dev/null +++ b/Modules/Sources/Browse/Logic/Adapters/FileAdapter.swift @@ -0,0 +1,41 @@ +// +// FileAdapter.swift +// Browse +// +// Created by Javier Cicchelli on 17/12/2022. +// Copyright © 2022 Röck+Cöde. All rights reserved. +// + +import APIService +import Foundation + +struct FileAdapter { + func callAsFunction(url: URL) throws -> File { + guard url.isFileURL else { + throw FileAdapterError.urlIsNotFileURL + } + + let data = try Data(contentsOf: url) + let name = try url + .resourceValues(forKeys: [.nameKey]) + .allValues + .first(where: { $0.key == .nameKey }) + .map(\.value) + + guard let name = name as? String else { + throw FileAdapterError.nameNotCasted + } + + return .init( + name: name, + data: data + ) + } +} + +// MARK: - Errors + +enum FileAdapterError: Error { + case urlIsNotFileURL + case nameNotCasted +} diff --git a/Modules/Sources/Browse/Logic/Use Cases/UploadFileUseCase.swift b/Modules/Sources/Browse/Logic/Use Cases/UploadFileUseCase.swift new file mode 100644 index 0000000..8a82025 --- /dev/null +++ b/Modules/Sources/Browse/Logic/Use Cases/UploadFileUseCase.swift @@ -0,0 +1,50 @@ +// +// UploadFileUseCase.swift +// Browse +// +// Created by Javier Cicchelli on 17/12/2022. +// Copyright © 2022 Röck+Cöde. All rights reserved. +// + +import APIService +import DependencyInjection +import Dependencies +import Foundation + +struct UploadFileUseCase { + + // MARK: Properties + + let apiService: APIService + + private let fileAdapter: FileAdapter = .init() + + // MARK: Functions + + func callAsFunction( + id: String, + url: URL, + username: String, + password: String + ) async throws { + _ = try await apiService.uploadFile( + id: id, + file: fileAdapter(url: url), + credentials: .init( + username: username, + password: password + ) + ) + } + +} + +// MARK: - Initialisers + +extension UploadFileUseCase { + init() { + @Dependency(\.apiService) var apiService + + self.init(apiService: apiService) + } +} From 1c04a671840965e2040ae3f0aa98da2203142b4f Mon Sep 17 00:00:00 2001 From: Javier Cicchelli Date: Sat, 17 Dec 2022 21:06:30 +0100 Subject: [PATCH 4/5] Implemented the UploadView view for the Browse module. --- .../Sources/Browse/UI/Views/UploadView.swift | 78 +++++++++++++++++++ 1 file changed, 78 insertions(+) create mode 100644 Modules/Sources/Browse/UI/Views/UploadView.swift diff --git a/Modules/Sources/Browse/UI/Views/UploadView.swift b/Modules/Sources/Browse/UI/Views/UploadView.swift new file mode 100644 index 0000000..495a43d --- /dev/null +++ b/Modules/Sources/Browse/UI/Views/UploadView.swift @@ -0,0 +1,78 @@ +// +// UploadView.swift +// Browse +// +// Created by Javier Cicchelli on 17/12/2022. +// Copyright © 2022 Röck+Cöde. All rights reserved. +// + +import DataModels +import Foundation +import KeychainStorage +import SwiftUI + + +struct UploadView: View { + + // MARK: Storages + + @KeychainStorage(key: .KeychainStorage.account) private var account: Account? + + // MARK: States + + @State private var urls: [URL] = [] + + // MARK: Properties + + let id: String + let uploaded: ActionClosure + + private let uploadFile: UploadFileUseCase = .init() + + // MARK: Body + + var body: some View { + DocumentPicker { urls in + Task { await addFile(from: urls) } + } + } + +} + +// MARK: - Helpers + +private extension UploadView { + func addFile(from urls: [URL]) async { + guard let url = urls.first else { + // TODO: Handle this error case. + return + } + guard let account else { + // TODO: Handle this error case. + return + } + + do { + try await uploadFile( + id: id, + url: url, + username: account.username, + password: account.password + ) + + uploaded() + } catch { + // TODO: Handle this error case. + } + } +} + +// MARK: - Previews + +struct UploadView_Previews: PreviewProvider { + static var previews: some View { + UploadView(id: "1234567890") { + // uploaded closure. + } + } +} From 5482d8fcbbbf15b54541b746345cab69b55018fd Mon Sep 17 00:00:00 2001 From: Javier Cicchelli Date: Sat, 17 Dec 2022 21:07:07 +0100 Subject: [PATCH 5/5] Integrated the UploadView view into the BrowseView view for the Browse module. --- BeReal/UI/Views/ContentView.swift | 1 - .../Sources/Browse/UI/Views/BrowseView.swift | 20 +++++++++++-------- 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/BeReal/UI/Views/ContentView.swift b/BeReal/UI/Views/ContentView.swift index a3d61da..4af869e 100644 --- a/BeReal/UI/Views/ContentView.swift +++ b/BeReal/UI/Views/ContentView.swift @@ -85,7 +85,6 @@ private extension ContentView { id: user.rootFolder.id, name: user.rootFolder.name ), - uploadFile: uploadFile, showProfile: showProfile, login: login ) diff --git a/Modules/Sources/Browse/UI/Views/BrowseView.swift b/Modules/Sources/Browse/UI/Views/BrowseView.swift index 4e13ccd..c493659 100644 --- a/Modules/Sources/Browse/UI/Views/BrowseView.swift +++ b/Modules/Sources/Browse/UI/Views/BrowseView.swift @@ -23,11 +23,11 @@ public struct BrowseView: View { @State private var stack: Stack? @State private var itemToDelete: (any FileSystemItem)? @State private var showCreateFolder: Bool = false + @State private var showUploadFile: Bool = false // MARK: Properties private let folder: Folder - private let uploadFile: ActionClosure private let showProfile: ActionClosure private let login: ActionClosure @@ -37,12 +37,10 @@ public struct BrowseView: View { public init( folder: Folder, - uploadFile: @escaping ActionClosure, showProfile: @escaping ActionClosure, login: @escaping ActionClosure ) { self.folder = folder - self.uploadFile = uploadFile self.showProfile = showProfile self.login = login } @@ -57,10 +55,17 @@ public struct BrowseView: View { createFolder: { showCreateFolder = true }, - uploadFile: uploadFile, + uploadFile: { + showUploadFile = true + }, showProfile: showProfile ) } + .sheet(isPresented: $showUploadFile) { + UploadView(id: folder.id) { + Task { await loadItems() } + } + } .createFolder( isPresenting: $showCreateFolder, id: folder.id @@ -112,7 +117,9 @@ private extension BrowseView { case .empty: MessageView( type: .empty, - action: uploadFile + action: { + showUploadFile = true + } ) case .error: MessageView(type: .error) { @@ -137,7 +144,6 @@ private extension BrowseView { .navigate( to: BrowseView( folder: folder, - uploadFile: uploadFile, showProfile: showProfile, login: login ), @@ -214,8 +220,6 @@ struct BrowseView_Previews: PreviewProvider { id: UUID().uuidString, name: "Some folder name" )) { - // upload file closure. - } showProfile: { // show profile closure. } login: { // login closure.