class ProjectsViewController: UIViewController { //MARK: - Types enum Section: CaseIterable { case normal } //MARK: - Properties let projectsController = ProjectsController() var collectionView: UICollectionView! var dataSource: UICollectionViewDiffableDataSource<Section, Project>! var lastScrollPosition: CGFloat = 0 var isSearching = false let searchController = UISearchController() //MARK: - ViewController Methods override func viewDidLoad() { super.viewDidLoad() configureViewController() configureSearchController() configureCollectionView() createDataSource() updateData(on: projectsController.filteredProjects()) } override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) if isSearching { isSearching.toggle() searchController.searchBar.text = "" searchController.resignFirstResponder() } } override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) searchController.searchBar.searchTextField.attributedPlaceholder = NSAttributedString(string: "Title or details text ...", attributes: [NSAttributedString.Key.foregroundColor: UIColor.secondaryLabel]) } override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { super.traitCollectionDidChange(previousTraitCollection) collectionView.collectionViewLayout = UICollectionView.createFlexibleFlowLayout(in: view) } //MARK: - DataSource func createDataSource() { dataSource = UICollectionViewDiffableDataSource<Section, Project>(collectionView: collectionView) { (collectionView, indexPath, project) in return self.configure(NormalProjectCell.self, with: project, for: indexPath) } } func updateData(on projects: [Project]) { var snapshot = NSDiffableDataSourceSnapshot<Section, Project>() snapshot.appendSections([Section.normal]) snapshot.appendItems(projects) //apply() is safe to call from a background queue! self.dataSource.apply(snapshot, animatingDifferences: true) } ///Configure any type of cell that conforms to selfConfiguringProjectCell! func configure<T: SelfConfiguringProjectCell>(_ cellType: T.Type, with project: Project, for indexPath: IndexPath) -> T { guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: cellType.reuseIdentifier, for: indexPath) as? T else { fatalError("Unable to dequeue \(cellType)") } cell.configure(with: project) return cell } //MARK: - Actions @objc func addButtonTapped() { let project = Project() let viewController = ProjectDetailsViewController(withProject: project) viewController.delegate = self navigationController?.pushViewController(viewController, animated: true) } @objc private func tapAndHoldCell(recognizer: UILongPressGestureRecognizer) { if recognizer.state == .ended { guard let indexPath = collectionView.indexPathForItem(at: recognizer.location(in: self.collectionView)), let project = dataSource?.itemIdentifier(for: indexPath) else { return } let viewController = ProjectDetailsViewController(withProject: project) viewController.delegate = self navigationController?.pushViewController(viewController, animated: true) } } @objc private func swipeFromRightOnCell(recognizer: UISwipeGestureRecognizer) { if recognizer.state == .ended { guard let indexPath = collectionView.indexPathForItem(at: recognizer.location(in: self.collectionView)), let cell = collectionView.cellForItem(at: indexPath), let project = dataSource?.itemIdentifier(for: indexPath) else { return } let overlay = ProjectCellDeletionOverlay(frame: CGRect(x: cell.bounds.width, y: 0, width: 0, height: cell.bounds.height)) cell.addSubview(overlay) UIView.animate(withDuration: 0.70, animations: { overlay.backgroundColor = UIColor.red.withAlphaComponent(0.60) overlay.frame = CGRect(x: cell.bounds.width / 2, y: 0, width: cell.bounds.width / 2, height: cell.bounds.height) }) { _ in self.presentProjectAlertOnMainThread(withTitle: "Delete this Project?", andMessage: "Are you sure?\nThis cannot be undone!\nAll associated notes will also be deleted!", andDismissButtonTitle: "Cancel", andConfirmButtonTitle: "Delete!", completion: { success in if success { UIView.animate(withDuration: 1.40, animations: { overlay.frame = CGRect(x: 0, y: 0, width: cell.bounds.width, height: cell.bounds.height) cell.alpha = 0 }) { _ in self.delete(project) overlay.removeFromSuperview() } } else { UIView.animate(withDuration: 1.5, animations: { overlay.frame = CGRect(x: cell.bounds.width, y: 0, width: 0, height: cell.bounds.height) overlay.alpha = 0 }) { _ in overlay.removeFromSuperview() } } }) } } } ///Will show an overlay view with help text on the app @objc private func showHelpView() { let helpViewController = AppHelpViewController(with: HelpViewDisplayTextFor.projects) helpViewController.modalTransitionStyle = .flipHorizontal helpViewController.modalPresentationStyle = .fullScreen present(helpViewController, animated: true) } ///Will show a menu with several options @objc private func showMenu() { } //MARK: - UI & Layout private func configureViewController() { view.backgroundColor = .systemPurple title = "Projects" navigationController?.navigationBar.prefersLargeTitles = false let menu = UIBarButtonItem(image: ProjectImages.BarButton.menu, style: .plain, target: self, action: #selector(showMenu)) let add = UIBarButtonItem(barButtonSystemItem: .add, target: self, action: #selector(addButtonTapped)) navigationItem.leftBarButtonItems = [menu, add] let questionMark = UIBarButtonItem(image: ProjectImages.BarButton.questionmark, style: .plain, target: self, action: #selector(showHelpView)) navigationItem.rightBarButtonItem = questionMark } private func configureCollectionView() { collectionView = UICollectionView(frame: view.bounds, collectionViewLayout: UICollectionView.createFlexibleFlowLayout(in: view)) collectionView.delegate = self collectionView.autoresizingMask = [.flexibleWidth, .flexibleHeight] collectionView.backgroundColor = .clear view.addSubview(collectionView) collectionView.register(NormalProjectCell.self, forCellWithReuseIdentifier: NormalProjectCell.reuseIdentifier) let tapAndHold = UILongPressGestureRecognizer(target: self, action: #selector(tapAndHoldCell)) tapAndHold.minimumPressDuration = 0.3 collectionView.addGestureRecognizer(tapAndHold) let swipeFromRight = UISwipeGestureRecognizer(target: self, action: #selector(swipeFromRightOnCell) ) swipeFromRight.direction = UISwipeGestureRecognizer.Direction.left collectionView.addGestureRecognizer(swipeFromRight) } private func configureSearchController() { searchController.searchResultsUpdater = self searchController.obscuresBackgroundDuringPresentation = false navigationItem.searchController = searchController //CollectionView under searchbar fix ??? searchController.extendedLayoutIncludesOpaqueBars = true // searchController.edgesForExtendedLayout = .top } } //MARK: - Ext CollectionView Delegate extension ProjectsViewController: UICollectionViewDelegate { func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { guard let project = dataSource?.itemIdentifier(for: indexPath) else { return } ProjectsController.activeProject = project let loadingView = showLoadingView(for: project) let viewController = SplitOrFlipContainerController() UIView.animate(withDuration: 1.5, animations: { loadingView.alpha = 1 }) { (complete) in self.dismiss(animated: false) { self.present(viewController, animated: false) } } } } //MARK: - Ext Search Results & Bar extension ProjectsViewController: UISearchResultsUpdating { func updateSearchResults(for searchController: UISearchController) { guard let filter = searchController.searchBar.text, filter.isNotEmpty else { isSearching = false updateData(on: projectsController.filteredProjects()) return } isSearching = true updateData(on: projectsController.filteredProjects(with: filter.lowercased())) } func scrollViewWillBeginDragging(_ scrollView: UIScrollView) { lastScrollPosition = scrollView.contentOffset.y } func scrollViewDidScroll(_ scrollView: UIScrollView) { if lastScrollPosition < scrollView.contentOffset.y { navigationItem.hidesSearchBarWhenScrolling = true } else if lastScrollPosition > scrollView.contentOffset.y { navigationItem.hidesSearchBarWhenScrolling = false } } } //MARK: - ProjectHandler extension ProjectsViewController: ProjectHandler { internal func save(_ project: Project, withImage image: UIImage?) { //call save and update the snapshot projectsController.save(project, withImage: image) updateData(on: projectsController.filteredProjects()) collectionView.reloadData() } internal func delete(_ project: Project) { //call delete and update the snapshot projectsController.delete(project) updateData(on: projectsController.filteredProjects()) } }