// // SidebarViewController.swift // Mastodon // // Created by Cirno MainasuK on 2021-9-22. // import os.log import UIKit import Combine import CoreDataStack import MastodonUI protocol SidebarViewControllerDelegate: AnyObject { func sidebarViewController(_ sidebarViewController: SidebarViewController, didSelectTab tab: MainTabBarController.Tab) func sidebarViewController(_ sidebarViewController: SidebarViewController, didLongPressItem item: SidebarViewModel.Item, sourceView: UIView) } final class SidebarViewController: UIViewController, NeedsDependency { let logger = Logger(subsystem: "SidebarViewController", category: "ViewController") weak var context: AppContext! { willSet { precondition(!isViewLoaded) } } weak var coordinator: SceneCoordinator! { willSet { precondition(!isViewLoaded) } } var disposeBag = Set() var observations = Set() var viewModel: SidebarViewModel! weak var delegate: SidebarViewControllerDelegate? static func createLayout() -> UICollectionViewLayout { let layout = UICollectionViewCompositionalLayout() { (sectionIndex, layoutEnvironment) -> NSCollectionLayoutSection? in var configuration = UICollectionLayoutListConfiguration(appearance: .sidebar) configuration.backgroundColor = .clear configuration.showsSeparators = false let section = NSCollectionLayoutSection.list(using: configuration, layoutEnvironment: layoutEnvironment) switch sectionIndex { case 0: let header = NSCollectionLayoutBoundarySupplementaryItem( layoutSize: NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .estimated(100)), elementKind: UICollectionView.elementKindSectionHeader, alignment: .top ) section.boundarySupplementaryItems = [header] default: break } return section } return layout } let collectionView: UICollectionView = { let layout = SidebarViewController.createLayout() let collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout) collectionView.alwaysBounceVertical = false collectionView.backgroundColor = .clear return collectionView }() static func createSecondaryLayout() -> UICollectionViewLayout { let layout = UICollectionViewCompositionalLayout() { (sectionIndex, layoutEnvironment) -> NSCollectionLayoutSection? in var configuration = UICollectionLayoutListConfiguration(appearance: .sidebar) configuration.backgroundColor = .clear configuration.showsSeparators = false let section = NSCollectionLayoutSection.list(using: configuration, layoutEnvironment: layoutEnvironment) return section } return layout } let secondaryCollectionView: UICollectionView = { let layout = SidebarViewController.createSecondaryLayout() let collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout) collectionView.isScrollEnabled = false collectionView.alwaysBounceVertical = false collectionView.backgroundColor = .clear return collectionView }() var secondaryCollectionViewHeightLayoutConstraint: NSLayoutConstraint! } extension SidebarViewController { override func viewDidLoad() { super.viewDidLoad() navigationController?.setNavigationBarHidden(true, animated: false) setupBackground(theme: ThemeService.shared.currentTheme.value) ThemeService.shared.currentTheme .receive(on: DispatchQueue.main) .sink { [weak self] theme in guard let self = self else { return } self.setupBackground(theme: theme) } .store(in: &disposeBag) collectionView.translatesAutoresizingMaskIntoConstraints = false view.addSubview(collectionView) NSLayoutConstraint.activate([ collectionView.topAnchor.constraint(equalTo: view.topAnchor), collectionView.leadingAnchor.constraint(equalTo: view.leadingAnchor), collectionView.trailingAnchor.constraint(equalTo: view.trailingAnchor), collectionView.bottomAnchor.constraint(equalTo: view.bottomAnchor), ]) secondaryCollectionView.translatesAutoresizingMaskIntoConstraints = false view.addSubview(secondaryCollectionView) secondaryCollectionViewHeightLayoutConstraint = secondaryCollectionView.heightAnchor.constraint(equalToConstant: 44).priority(.required - 1) NSLayoutConstraint.activate([ secondaryCollectionView.leadingAnchor.constraint(equalTo: view.leadingAnchor), secondaryCollectionView.trailingAnchor.constraint(equalTo: view.trailingAnchor), view.bottomAnchor.constraint(equalTo: secondaryCollectionView.bottomAnchor), secondaryCollectionViewHeightLayoutConstraint, ]) collectionView.delegate = self secondaryCollectionView.delegate = self viewModel.setupDiffableDataSource( collectionView: collectionView, secondaryCollectionView: secondaryCollectionView ) secondaryCollectionView.observe(\.contentSize, options: [.initial, .new]) { [weak self] secondaryCollectionView, _ in guard let self = self else { return } let contentHeight = secondaryCollectionView.contentSize.height guard contentHeight > 0 else { return } self.logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): secondaryCollectionView contentSize: \(secondaryCollectionView.contentSize.debugDescription)") let currentFrameHeight = secondaryCollectionView.frame.height guard currentFrameHeight < contentHeight else { return } self.secondaryCollectionViewHeightLayoutConstraint.constant = contentHeight self.collectionView.contentInset.bottom = contentHeight } .store(in: &observations) let sidebarLongPressGestureRecognizer = UILongPressGestureRecognizer() sidebarLongPressGestureRecognizer.addTarget(self, action: #selector(SidebarViewController.sidebarLongPressGestureRecognizerHandler(_:))) collectionView.addGestureRecognizer(sidebarLongPressGestureRecognizer) } private func setupBackground(theme: Theme) { let color: UIColor = theme.sidebarBackgroundColor view.backgroundColor = color } override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) { super.viewWillTransition(to: size, with: coordinator) coordinator.animate { context in self.collectionView.collectionViewLayout.invalidateLayout() } completion: { context in // do nothing } } } extension SidebarViewController { @objc private func sidebarLongPressGestureRecognizerHandler(_ sender: UILongPressGestureRecognizer) { guard sender.state == .began else { return } logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public)") assert(sender.view === collectionView) let position = sender.location(in: collectionView) guard let indexPath = collectionView.indexPathForItem(at: position) else { return } guard let diffableDataSource = viewModel.diffableDataSource else { return } guard let item = diffableDataSource.itemIdentifier(for: indexPath) else { return } guard let cell = collectionView.cellForItem(at: indexPath) else { return } delegate?.sidebarViewController(self, didLongPressItem: item, sourceView: cell) } } // MARK: - UICollectionViewDelegate extension SidebarViewController: UICollectionViewDelegate { func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { switch collectionView { case self.collectionView: guard let diffableDataSource = viewModel.diffableDataSource else { return } guard let item = diffableDataSource.itemIdentifier(for: indexPath) else { return } switch item { case .tab(let tab): delegate?.sidebarViewController(self, didSelectTab: tab) case .setting: guard let setting = context.settingService.currentSetting.value else { return } let settingsViewModel = SettingsViewModel(context: context, setting: setting) coordinator.present(scene: .settings(viewModel: settingsViewModel), from: self, transition: .modal(animated: true, completion: nil)) case .compose: assertionFailure() } case secondaryCollectionView: guard let diffableDataSource = viewModel.secondaryDiffableDataSource else { return } guard let item = diffableDataSource.itemIdentifier(for: indexPath) else { return } guard let authenticationBox = context.authenticationService.activeMastodonAuthenticationBox.value else { return } switch item { case .compose: let composeViewModel = ComposeViewModel( context: context, composeKind: .post, authenticationBox: authenticationBox ) coordinator.present(scene: .compose(viewModel: composeViewModel), from: self, transition: .modal(animated: true, completion: nil)) default: assertionFailure() } default: assertionFailure() } } }