import UIKit import VisionKit final class DocumentsTableViewController: UITableViewController { struct Constants { static let reuseIdentifier = String(describing: DocumentsTableViewController.self) static let storyboardName = "Documents" static let title = Bundle.appName static let rowHeight: CGFloat = 96 static let cellReuseIdentifier = String(describing: DocumentsTableViewCell.self) static let topBottom: CGFloat = 120 static let margin: CGFloat = 64 } static func buildViewController() -> DocumentsTableViewController { let controller = UIStoryboard(name: Constants.storyboardName, bundle: .main).instantiateViewController(withIdentifier: Constants.reuseIdentifier) return controller as! DocumentsTableViewController } private var viewModels: [File] = [] private var localFileManager: LocalFileManager? private var sortType: SortyFileType = .date private let noDocumentsimageView = UIImageView(image: UIImage(named: "box")) private var renameAlertController: UIAlertController? private var renameFileName: String? override func viewDidLoad() { super.viewDidLoad() navigationItem.title = Constants.title tableView.tableFooterView = UIView() localFileManager = LocalFileManager() tabBarController?.delegate = UIApplication.shared.delegate as? UITabBarControllerDelegate NotificationCenter.default.addObserver(self, selector: #selector(middleButtonTapped(_:)), name: NSNotification.Name.MiddleButtonTapped, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(fetchViewModels), name: NSNotification.Name.ReloadViewModels, object: nil) if let view = navigationController?.view { noDocumentsimageView.translatesAutoresizingMaskIntoConstraints = false noDocumentsimageView.contentMode = .scaleAspectFit view.addSubview(noDocumentsimageView) NSLayoutConstraint.activate([ noDocumentsimageView.topAnchor.constraint(equalTo: view.topAnchor, constant: Constants.topBottom), noDocumentsimageView.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: -Constants.topBottom), noDocumentsimageView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: Constants.margin), noDocumentsimageView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -Constants.margin) ]) } if UserDefaults.standard.startsAppWithCamera { openCamera() } } override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) fetchViewModels() } deinit { NotificationCenter.default.removeObserver(self) } @objc private func fetchViewModels() { guard let directoryURL = localFileManager?.scannerURL, let viewModels = localFileManager?.filesForDirectory(directoryURL, sortType: sortType) else { return } self.viewModels = viewModels tableView.reloadData() if viewModels.isEmpty { noDocumentsimageView.isHidden = false } else { noDocumentsimageView.isHidden = true } } // MARK: - Table view data source override func numberOfSections(in tableView: UITableView) -> Int { // #warning Incomplete implementation, return the number of sections return 1 } override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { // #warning Incomplete implementation, return the number of rows return viewModels.count } override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { guard let cell = tableView.dequeueReusableCell(withIdentifier: Constants.cellReuseIdentifier, for: indexPath) as? DocumentsTableViewCell else { return UITableViewCell() } // Configure the cell... let viewModel = viewModels[indexPath.row] cell.configure(with: viewModel, image: localFileManager?.getThumbnail(for: viewModel.fileURL)) return cell } override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { return Constants.rowHeight } override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { let viewModel = viewModels[indexPath.row] let controller = DocumentPreviewViewController.buildViewController() controller.file = viewModel controller.hidesBottomBarWhenPushed = true navigationController?.pushViewController(controller, animated: true) } override func tableView(_ tableView: UITableView, editingStyleForRowAt indexPath: IndexPath) -> UITableViewCell.EditingStyle { return .none } override func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? { // Trash action let trash = UIContextualAction(style: .destructive, title: "Delete".localized) { [weak self] (_, _, completionHandler) in self?.deleteFile(at: indexPath) completionHandler(true) } trash.backgroundColor = .systemRed trash.image = UIImage(systemName: "trash")?.tint(with: .white) // Rename action let rename = UIContextualAction(style: .normal, title: "Rename".localized) { [weak self] (_, _, completionHandler) in self?.renameFile(at: indexPath) completionHandler(true) } rename.backgroundColor = UIColor.black.withAlphaComponent(0.5) rename.image = UIImage(systemName: "square.and.pencil")?.tint(with: .white) let configuration = UISwipeActionsConfiguration(actions: [trash, rename]) configuration.performsFirstActionWithFullSwipe = true return configuration } @objc private func middleButtonTapped(_ notification: NSNotification) { let alertController = UIAlertController(title: nil, message: nil, preferredStyle: .actionSheet) let cameraAction = UIAlertAction(title: "Camera".localized, style: .default, handler: { action in self.openCamera() }) let galleryAction = UIAlertAction(title: "Gallery".localized, style: .default, handler: { action in self.openGallery() }) alertController.addAction(cameraAction) cameraAction.setValue(UIImage(systemName: "camera"), forKey: "image") cameraAction.setValue(CATextLayerAlignmentMode.left, forKey: "titleTextAlignment") alertController.addAction(galleryAction) galleryAction.setValue(UIImage(systemName: "photo"), forKey: "image") galleryAction.setValue(CATextLayerAlignmentMode.left, forKey: "titleTextAlignment") alertController.addAction(UIAlertAction(title: "Cancel".localized, style: .cancel, handler: nil)) alertController.popoverPresentationController?.sourceView = notification.object as? UIButton present(alertController, animated: true) } private func openCamera() { let scannerOptions = ImageScannerOptions(scanMultipleItems: true, allowAutoScan: false, allowTapToFocus: false, defaultColorRenderOption:.color) let scannerViewController = ImageScannerController(options: scannerOptions) scannerViewController.imageScannerDelegate = self present(scannerViewController, animated: true) } private func openGallery() { guard UIImagePickerController.isSourceTypeAvailable(.savedPhotosAlbum) else { return } let imagePickerController = UIImagePickerController() imagePickerController.delegate = self imagePickerController.sourceType = UIImagePickerController.SourceType.savedPhotosAlbum imagePickerController.allowsEditing = UserDefaults.standard.isPhotoEditigOn present(imagePickerController, animated: true, completion: nil) } private func renameFile(at indexPath: IndexPath) { let viewModel = viewModels[indexPath.row] renameAlertController = UIAlertController(title: "Rename document".localized, message: nil, preferredStyle: .alert) renameAlertController!.addTextField { [weak self] textField in guard let self = self else { return } textField.placeholder = viewModel.displayName textField.text = viewModel.displayName textField.clearButtonMode = .always self.renameFileName = viewModel.displayName textField.addTarget(self, action: #selector(self.textFieldDidChange(_:)), for: .editingChanged) } let continueAction = UIAlertAction(title: "Rename".localized, style: .default) { [weak self] _ in guard let self = self else { return } guard let textFields = self.renameAlertController?.textFields else { return } if let newName = textFields.first?.text, newName != viewModel.displayName { do { try self.localFileManager?.renameFile(at: viewModel.fileURL, withName: newName) self.fetchViewModels() } catch { return } } } renameAlertController!.addAction(continueAction) renameAlertController!.addAction(UIAlertAction(title: "Cancel".localized, style: .cancel, handler: nil)) renameAlertController?.actions.first?.isEnabled = false present(renameAlertController!, animated: true) } private func deleteFile(at indexPath: IndexPath) { let viewModel = viewModels[indexPath.row] if UserDefaults.standard.askOnSwipeDelete { let alertController = UIAlertController(title: Bundle.appName, message: "Are you sure you want to delete this document?".localized, preferredStyle: .alert) let okAction = UIAlertAction(title: "Yes, delete!".localized, style: .destructive, handler: { [weak self] action in guard let self = self else { return } self.delete(file: viewModel.fileURL, at: indexPath) }) alertController.addAction(okAction) alertController.addAction(UIAlertAction(title: "Cancel".localized, style: .cancel, handler: nil)) alertController.popoverPresentationController?.sourceView = tableView.cellForRow(at: indexPath) present(alertController, animated: true) } else { delete(file: viewModel.fileURL, at: indexPath) } } private func delete(file fileURL: URL, at indexPath: IndexPath) { do { try self.localFileManager?.filesManager.removeItem(at: fileURL) try self.localFileManager?.removeThumbnail(for: fileURL) self.viewModels.remove(at: indexPath.row) self.tableView.deleteRows(at: [indexPath], with: .fade) } catch { return } if self.viewModels.isEmpty { self.noDocumentsimageView.isHidden = false } } } /// Tells the delegate that the user picked a still image. extension DocumentsTableViewController: UINavigationControllerDelegate, UIImagePickerControllerDelegate { /// Your delegate object’s implementation of this method should pass the specified media on to any custom code that needs it, and should then dismiss the picker view. /// When editing is enabled, the image picker view presents the user with a preview of the currently selected image or movie along with controls for modifying it. /// (This behavior is managed by the picker view prior to calling this method.) If the user modifies the image or movie, the editing information is available in the info parameter. /// The original image is also returned in the info parameter. func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) { picker.dismiss(animated: true) { [weak self] in guard let self = self else { return } guard let image = info[UserDefaults.standard.isPhotoEditigOn ? UIImagePickerController.InfoKey.editedImage : UIImagePickerController.InfoKey.originalImage] as? UIImage else { return } PDFManager.createPDFDocument(from: image, localFileManager: self.localFileManager) self.fetchViewModels() } } } extension DocumentsTableViewController: ImageScannerControllerDelegate { func imageScannerController(_ scanner: ImageScannerController, didFinishWithSession session: MultiPageScanSession) { var images = [URL]() for index in 0..<session.scannedItems.count { if let url = session.scannedItems[index].renderedImage { images.append(url) } } print("images: \(images)") PDFManager.createMultiPDFPage(from: images, localFileManager: localFileManager) { scanner.dismiss(animated: true) { self.fetchViewModels() } } } func imageScannerControllerDidCancel(_ scanner: ImageScannerController) { scanner.dismiss(animated: true) } func imageScannerController(_ scanner: ImageScannerController, didFailWithError error: Error) { scanner.dismiss(animated: true) } @objc func textFieldDidChange(_ textField: UITextField) { guard let text = textField.text, let renameFileName = renameFileName, renameFileName != text, !text.trimmingCharacters(in: .whitespaces).isEmpty else { renameAlertController?.actions.first?.isEnabled = false return } renameAlertController?.actions.first?.isEnabled = true } }