// // SegmentedPageViewController.swift // Tusker // // Created by Shadowfacts on 9/13/19. // Copyright © 2019 Shadowfacts. All rights reserved. // import UIKit protocol SegmentedPageViewControllerPage: Hashable { var segmentedControlTitle: String { get } } class SegmentedPageViewController: UIViewController, UIPageViewControllerDelegate, TabbedPageViewController { private(set) var pages: [Page]! private let pageProvider: (Page) -> UIViewController private var pageControllers = [Page: UIViewController]() private var initialPage: Page private(set) var currentPage: Page var currentIndex: Int! { pages.firstIndex(of: currentPage) } var currentViewController: UIViewController! let segmentedControl = ScrollingSegmentedControl() init(pages: [Page], pageProvider: @escaping (Page) -> UIViewController) { precondition(!pages.isEmpty) self.pageProvider = pageProvider initialPage = pages.first! currentPage = pages.first! super.init(nibName: nil, bundle: nil) setPages(pages, animated: false) segmentedControl.didSelectOption = { [unowned self] option in if let option { self.selectPage(option, animated: true) } } // TODO: the custom segmented control isn't treated as a group and I have no idea how to change that // the segemented control itself is only focusable when VoiceOver is in Group navigation mode, // so make it clear that to switch tabs the user needs to enter the group segmentedControl.accessibilityHint = "Enter group to select timeline" segmentedControl.setSelectedOption(segmentedControl.options.first!.value, animated: false) navigationItem.titleView = segmentedControl } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } func setPages(_ pages: [Page], animated: Bool) { precondition(!pages.isEmpty) self.pages = pages if !pages.contains(currentPage) { selectPage(pages.first!, animated: animated) } for key in pageControllers.keys where !pages.contains(key) { pageControllers.removeValue(forKey: key) } // this needs to happen in init because EnhancedNavigationViewController expects to be able to look at the titleView // before the view has necessarily loaded segmentedControl.options = pages.map { .init(value: $0, name: $0.segmentedControlTitle) } } override func viewDidLoad() { super.viewDidLoad() view.backgroundColor = .appBackground selectPage(initialPage, animated: false) addKeyCommand(MenuController.prevSubTabCommand) addKeyCommand(MenuController.nextSubTabCommand) // disable the transparent nav bar because it gets messy with multiple pages at different scroll positions if let nav = navigationController { let appearance = UINavigationBarAppearance() appearance.configureWithDefaultBackground() nav.navigationBar.scrollEdgeAppearance = appearance } } func configureViewController(_ viewController: UIViewController) { } func selectPage(_ page: Page, animated: Bool) { guard pages.contains(page) else { fatalError("invalid page \(page) that is not in SegmentedPageViewController.pages") } guard isViewLoaded else { initialPage = page return } let direction: AnimationMode if let prevIndex = currentIndex { let index = pages.firstIndex(of: page)! direction = index - prevIndex > 0 ? .forward : .reverse } else { direction = .none } currentPage = page let newController: UIViewController if let existing = pageControllers[page] { newController = existing } else { newController = pageProvider(page) configureViewController(newController) pageControllers[page] = newController } setViewController(newController, animated: animated ? direction : .none) navigationItem.title = newController.title segmentedControl.setSelectedOption(page, animated: animated) } private func setViewController(_ newViewController: UIViewController, animated: AnimationMode) { guard let currentViewController, animated != .none else { currentViewController?.removeViewAndController() newViewController.view.translatesAutoresizingMaskIntoConstraints = false // don't use embedChild here because it triggers an appearance transition, even though this vc hasn't appeared yet addChild(newViewController) view.addSubview(newViewController.view) NSLayoutConstraint.activate([ newViewController.view.leadingAnchor.constraint(equalTo: view.leadingAnchor), newViewController.view.trailingAnchor.constraint(equalTo: view.trailingAnchor), newViewController.view.topAnchor.constraint(equalTo: view.topAnchor), newViewController.view.bottomAnchor.constraint(equalTo: view.bottomAnchor), ]) newViewController.didMove(toParent: self) self.currentViewController = newViewController return } guard currentViewController !== newViewController else { return } self.currentViewController = newViewController newViewController.view.translatesAutoresizingMaskIntoConstraints = false embedChild(newViewController) let direction: CGFloat = animated == .forward ? 1 : -1 newViewController.view.transform = CGAffineTransform(translationX: direction * view.bounds.width, y: 0) let animator = UIViewPropertyAnimator(duration: 0.5, timingParameters: UISpringTimingParameters(dampingRatio: 1, initialVelocity: .zero)) animator.addAnimations { newViewController.view.transform = .identity currentViewController.view.transform = CGAffineTransform(translationX: -1 * direction * self.view.bounds.width, y: 0) } animator.addCompletion { _ in currentViewController.removeViewAndController() } animator.startAnimation() } // MARK: TabbedPageViewController func selectNextPage() { guard currentIndex < pages.count - 1 else { return } selectPage(pages[currentIndex + 1], animated: true) } func selectPrevPage() { guard currentIndex > 0 else { return } selectPage(pages[currentIndex - 1], animated: true) } } extension SegmentedPageViewController { enum AnimationMode: Equatable { case none case forward case reverse } } extension SegmentedPageViewController: TabBarScrollableViewController { func tabBarScrollToTop() { if let scrollableVC = currentViewController as? TabBarScrollableViewController { scrollableVC.tabBarScrollToTop() } } } extension SegmentedPageViewController: BackgroundableViewController { func sceneDidEnterBackground() { if let current = currentViewController as? BackgroundableViewController { current.sceneDidEnterBackground() } } } extension SegmentedPageViewController: StatusBarTappableViewController { func handleStatusBarTapped(xPosition: CGFloat) -> StatusBarTapActionResult { if let current = currentViewController as? StatusBarTappableViewController { return current.handleStatusBarTapped(xPosition: xPosition) } return .continue } } extension SegmentedPageViewController: NestedResponderProvider { var innerResponder: UIResponder? { currentViewController } }