From 377bf6aecc70cc827ee1b06491389b5d39644d8d Mon Sep 17 00:00:00 2001 From: Justin Mazzocchi <2831158+jzzocc@users.noreply.github.com> Date: Sat, 9 Jan 2021 23:08:45 -0800 Subject: [PATCH] Browse files for attachments --- Localizations/Localizable.strings | 3 + .../NewStatusViewController.swift | 35 +++++++++ .../ViewModels/NewStatusViewModel.swift | 5 ++ Views/CompositionInputAccessoryView.swift | 72 ++++++++++--------- 4 files changed, 80 insertions(+), 35 deletions(-) diff --git a/Localizations/Localizable.strings b/Localizations/Localizable.strings index e2830b9..a01a10c 100644 --- a/Localizations/Localizable.strings +++ b/Localizations/Localizable.strings @@ -46,6 +46,9 @@ "compose.attachment.uploading" = "Uploading"; "compose.prompt" = "What's on your mind?"; "compose.mark-media-sensitive" = "Mark media as sensitive"; +"compose.photo-library" = "Photo Library"; +"compose.take-photo-or-video" = "Take Photo or Video"; +"compose.browse" = "Browse"; "error" = "Error"; "favorites" = "Favorites"; "registration.review-terms-of-use-and-privacy-policy-%@" = "Please review %@'s Terms of Use and Privacy Policy to continue"; diff --git a/View Controllers/NewStatusViewController.swift b/View Controllers/NewStatusViewController.swift index 8fee7c2..ac59527 100644 --- a/View Controllers/NewStatusViewController.swift +++ b/View Controllers/NewStatusViewController.swift @@ -20,6 +20,7 @@ final class NewStatusViewController: UIViewController { action: nil) private let mediaSelections = PassthroughSubject<[PHPickerResult], Never>() private let imagePickerResults = PassthroughSubject<[UIImagePickerController.InfoKey: Any]?, Never>() + private let documentPickerResuls = PassthroughSubject<[URL]?, Never>() private var cancellables = Set() init(viewModel: NewStatusViewModel) { @@ -110,6 +111,16 @@ extension NewStatusViewController: UIImagePickerControllerDelegate { } } +extension NewStatusViewController: UIDocumentPickerDelegate { + func documentPicker(_ controller: UIDocumentPickerViewController, didPickDocumentsAt urls: [URL]) { + documentPickerResuls.send(urls) + } + + func documentPickerWasCancelled(_ controller: UIDocumentPickerViewController) { + documentPickerResuls.send(nil) + } +} + // Required by UIImagePickerController extension NewStatusViewController: UINavigationControllerDelegate {} @@ -122,6 +133,8 @@ private extension NewStatusViewController { #if !IS_SHARE_EXTENSION presentCamera(compositionViewModel: compositionViewModel) #endif + case let .presentDocumentPicker(compositionViewModel): + presentDocumentPicker(compositionViewModel: compositionViewModel) case let .editAttachment(attachmentViewModel, compositionViewModel): presentAttachmentEditor( attachmentViewModel: attachmentViewModel, @@ -301,6 +314,28 @@ private extension NewStatusViewController { } #endif + func presentDocumentPicker(compositionViewModel: CompositionViewModel) { + documentPickerResuls.first().sink { [weak self] in + guard let self = self, + let result = $0?.first, + result.startAccessingSecurityScopedResource(), + let itemProvider = NSItemProvider(contentsOf: result) + else { return } + + self.viewModel.attach(itemProvider: itemProvider, to: compositionViewModel) + result.stopAccessingSecurityScopedResource() + } + .store(in: &cancellables) + + let documentPickerController = UIDocumentPickerViewController(forOpeningContentTypes: [.image, .movie, .audio]) + + documentPickerController.delegate = self + documentPickerController.allowsMultipleSelection = false + documentPickerController.modalPresentationStyle = .overFullScreen + + present(documentPickerController, animated: true) + } + func presentAttachmentEditor(attachmentViewModel: AttachmentViewModel, compositionViewModel: CompositionViewModel) { let editAttachmentsView = EditAttachmentView { (attachmentViewModel, compositionViewModel) } let editAttachmentViewController = UIHostingController(rootView: editAttachmentsView) diff --git a/ViewModels/Sources/ViewModels/NewStatusViewModel.swift b/ViewModels/Sources/ViewModels/NewStatusViewModel.swift index af6b1b3..9b526ae 100644 --- a/ViewModels/Sources/ViewModels/NewStatusViewModel.swift +++ b/ViewModels/Sources/ViewModels/NewStatusViewModel.swift @@ -53,6 +53,7 @@ public extension NewStatusViewModel { enum Event { case presentMediaPicker(CompositionViewModel) case presentCamera(CompositionViewModel) + case presentDocumentPicker(CompositionViewModel) case editAttachment(AttachmentViewModel, CompositionViewModel) } @@ -89,6 +90,10 @@ public extension NewStatusViewModel { eventsSubject.send(.presentCamera(viewModel)) } + func presentDocumentPicker(viewModel: CompositionViewModel) { + eventsSubject.send(.presentDocumentPicker(viewModel)) + } + func remove(viewModel: CompositionViewModel) { compositionViewModels.removeAll { $0 === viewModel } } diff --git a/Views/CompositionInputAccessoryView.swift b/Views/CompositionInputAccessoryView.swift index 3dba81a..89cce80 100644 --- a/Views/CompositionInputAccessoryView.swift +++ b/Views/CompositionInputAccessoryView.swift @@ -45,40 +45,44 @@ private extension CompositionInputAccessoryView { stackView.translatesAutoresizingMaskIntoConstraints = false stackView.spacing = .defaultSpacing - let mediaButton = UIButton() - - stackView.addArrangedSubview(mediaButton) - mediaButton.setImage( - UIImage( - systemName: "photo", - withConfiguration: UIImage.SymbolConfiguration(scale: .medium)), - for: .normal) - mediaButton.addAction(UIAction { [weak self] _ in - guard let self = self else { return } - - self.parentViewModel.presentMediaPicker(viewModel: self.viewModel) - }, - for: .touchUpInside) - - let cameraButton = UIButton() - - #if !IS_SHARE_EXTENSION - if AVCaptureDevice.authorizationStatus(for: .video) != .restricted { - stackView.addArrangedSubview(cameraButton) - cameraButton.setImage( - UIImage( - systemName: "camera", - withConfiguration: UIImage.SymbolConfiguration(scale: .medium)), - for: .normal) - cameraButton.addAction(UIAction { [weak self] _ in + var attachmentActions = [ + UIAction( + title: NSLocalizedString("compose.browse", comment: ""), + image: UIImage(systemName: "ellipsis")) { [weak self] _ in guard let self = self else { return } - self.parentViewModel.presentCamera(viewModel: self.viewModel) + self.parentViewModel.presentDocumentPicker(viewModel: self.viewModel) }, - for: .touchUpInside) - } + UIAction( + title: NSLocalizedString("compose.photo-library", comment: ""), + image: UIImage(systemName: "rectangle.on.rectangle")) { [weak self] _ in + guard let self = self else { return } + + self.parentViewModel.presentMediaPicker(viewModel: self.viewModel) + } + ] + + #if !IS_SHARE_EXTENSION + attachmentActions.insert(UIAction( + title: NSLocalizedString("compose.take-photo-or-video", comment: ""), + image: UIImage(systemName: "camera.fill")) { [weak self] _ in + guard let self = self else { return } + + self.parentViewModel.presentCamera(viewModel: self.viewModel) + }, + at: 1) #endif + let attachmentButton = UIButton() + stackView.addArrangedSubview(attachmentButton) + attachmentButton.setImage( + UIImage( + systemName: "paperclip", + withConfiguration: UIImage.SymbolConfiguration(scale: .medium)), + for: .normal) + attachmentButton.showsMenuAsPrimaryAction = true + attachmentButton.menu = UIMenu(children: attachmentActions) + let pollButton = UIButton() stackView.addArrangedSubview(pollButton) @@ -126,11 +130,9 @@ private extension CompositionInputAccessoryView { self.parentViewModel.insert(after: self.viewModel) }, for: .touchUpInside) - viewModel.$canAddAttachment.sink { - mediaButton.isEnabled = $0 - cameraButton.isEnabled = $0 - } - .store(in: &cancellables) + viewModel.$canAddAttachment + .sink { attachmentButton.isEnabled = $0 } + .store(in: &cancellables) viewModel.$remainingCharacters.sink { charactersLabel.text = String($0) @@ -148,7 +150,7 @@ private extension CompositionInputAccessoryView { } .store(in: &cancellables) - for button in [mediaButton, pollButton, visibilityButton, contentWarningButton, addButton] { + for button in [attachmentButton, pollButton, visibilityButton, contentWarningButton, addButton] { button.heightAnchor.constraint(greaterThanOrEqualToConstant: .minimumButtonDimension).isActive = true button.widthAnchor.constraint(greaterThanOrEqualToConstant: .minimumButtonDimension).isActive = true }