// Copyright © 2021 Metabolist. All rights reserved. import Combine import UIKit import ViewModels final class ExploreViewController: UICollectionViewController { private let webfingerIndicatorView = WebfingerIndicatorView() private let viewModel: ExploreViewModel private let rootViewModel: RootViewModel private var cancellables = Set() private lazy var dataSource: ExploreDataSource = { .init(collectionView: collectionView, viewModel: viewModel) }() init(viewModel: ExploreViewModel, rootViewModel: RootViewModel) { self.viewModel = viewModel self.rootViewModel = rootViewModel super.init(collectionViewLayout: Self.layout()) tabBarItem = UITabBarItem( title: NSLocalizedString("main-navigation.explore", comment: ""), image: UIImage(systemName: "magnifyingglass"), selectedImage: nil) } @available(*, unavailable) required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } // swiftlint:disable:next function_body_length override func viewDidLoad() { super.viewDidLoad() collectionView.dataSource = dataSource collectionView.backgroundColor = .systemBackground collectionView.contentInset.bottom = Self.bottomInset collectionView.isAccessibilityElement = false collectionView.shouldGroupAccessibilityChildren = true clearsSelectionOnViewWillAppear = true collectionView.refreshControl = UIRefreshControl() collectionView.refreshControl?.addAction( UIAction { [weak self] _ in self?.viewModel.refresh() }, for: .valueChanged) navigationItem.title = NSLocalizedString("main-navigation.explore", comment: "") let searchResultsController = TableViewController( viewModel: viewModel.searchViewModel, rootViewModel: rootViewModel, insetBottom: false, parentNavigationController: navigationController) let searchController = UISearchController(searchResultsController: searchResultsController) searchController.searchResultsUpdater = self searchController.searchBar.keyboardType = .twitter navigationItem.searchController = searchController view.addSubview(webfingerIndicatorView) webfingerIndicatorView.translatesAutoresizingMaskIntoConstraints = false viewModel.identityContext.$appPreferences.sink { appPreferences in searchController.searchBar.scopeButtonTitles = SearchScope.allCases.map { $0.title(statusWord: appPreferences.statusWord) } } .store(in: &cancellables) NSLayoutConstraint.activate([ webfingerIndicatorView.centerXAnchor.constraint(equalTo: view.safeAreaLayoutGuide.centerXAnchor), webfingerIndicatorView.centerYAnchor.constraint(equalTo: view.safeAreaLayoutGuide.centerYAnchor) ]) viewModel.events.sink { [weak self] in self?.handle(event: $0) }.store(in: &cancellables) viewModel.$loading.sink { [weak self] in guard let self = self else { return } let refreshControlVisibile = self.collectionView.refreshControl?.isRefreshing ?? false if !$0, refreshControlVisibile { self.collectionView.refreshControl?.endRefreshing() } } .store(in: &cancellables) viewModel.searchViewModel.searchScopeChanges.sink { [weak self] in searchController.searchBar.selectedScopeButtonIndex = $0.rawValue self?.updateSearchResults(for: searchController) } .store(in: &cancellables) } override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) viewModel.refresh() } override func collectionView(_ collectionView: UICollectionView, shouldHighlightItemAt indexPath: IndexPath) -> Bool { dataSource.itemIdentifier(for: indexPath) != .instance } override func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { guard let item = dataSource.itemIdentifier(for: indexPath) else { return } viewModel.select(item: item) } } extension ExploreViewController: UISearchResultsUpdating { func updateSearchResults(for searchController: UISearchController) { if let scope = SearchScope(rawValue: searchController.searchBar.selectedScopeButtonIndex) { if scope != viewModel.searchViewModel.scope, let scrollView = searchController.searchResultsController?.view as? UIScrollView { scrollView.setContentOffset(.init(x: 0, y: -scrollView.safeAreaInsets.top), animated: false) } viewModel.searchViewModel.scope = scope } viewModel.searchViewModel.query = searchController.searchBar.text ?? "" } } extension ExploreViewController: ScrollableToTop { func scrollToTop(animated: Bool) { collectionView.scrollToTop(animated: animated) } } extension ExploreViewController: NavigationHandling { func handle(navigation: Navigation) { switch navigation { case let .collection(collectionService): let vc = TableViewController( viewModel: CollectionItemsViewModel( collectionService: collectionService, identityContext: viewModel.identityContext), rootViewModel: rootViewModel, parentNavigationController: nil) show(vc, sender: self) webfingerIndicatorView.stopAnimating() case let .profile(profileService): let vc = ProfileViewController( viewModel: ProfileViewModel( profileService: profileService, identityContext: viewModel.identityContext), rootViewModel: rootViewModel, identityContext: viewModel.identityContext, parentNavigationController: nil) show(vc, sender: self) webfingerIndicatorView.stopAnimating() case let .url(url): open(url: url, identityContext: viewModel.identityContext) webfingerIndicatorView.stopAnimating() case .webfingerStart: webfingerIndicatorView.startAnimating() case .webfingerEnd: webfingerIndicatorView.stopAnimating() default: break } } } private extension ExploreViewController { static let bottomInset: CGFloat = .newStatusButtonDimension + .defaultSpacing * 4 static func layout() -> UICollectionViewLayout { var listConfiguration = UICollectionLayoutListConfiguration(appearance: .plain) listConfiguration.headerMode = .supplementary return UICollectionViewCompositionalLayout( sectionProvider: { let section = NSCollectionLayoutSection.list(using: listConfiguration, layoutEnvironment: $1) if UIDevice.current.userInterfaceIdiom == .pad { section.contentInsetsReference = .readableContent } return section }) } func handle(event: ExploreViewModel.Event) { switch event { case let .navigation(navigation): handle(navigation: navigation) } } }