Tusker/Tusker/Screens/Utilities/SegmentedPageViewController...

221 lines
8.1 KiB
Swift

//
// 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<Page: SegmentedPageViewControllerPage>: 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<Page>()
init(pages: [Page], initialPage: Page? = nil, pageProvider: @escaping (Page) -> UIViewController) {
precondition(!pages.isEmpty)
self.pageProvider = pageProvider
self.initialPage = initialPage ?? pages.first!
currentPage = self.initialPage
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(self.initialPage, 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
}
}
// Extension point for subclasses
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
}
}