* Add UseCaseController * Add Layouts.swift * Add PagePanelController * Append access modifiers into some objects * Split view controllers into each file * Organize the project * Rename currentMenu with currentUseCase * Rename SampleListVC with MainVC * Update UseCase enum * Update DebugTableViewController to test the scroll (un)tracking functions
330 lines
13 KiB
Swift
330 lines
13 KiB
Swift
// Copyright 2018-Present Shin Yamamoto. All rights reserved. MIT license.
|
|
|
|
import UIKit
|
|
import FloatingPanel
|
|
|
|
final class UseCaseController: NSObject {
|
|
unowned let mainVC: MainViewController
|
|
private(set) var useCase: UseCase = .trackingTableView
|
|
|
|
fileprivate var mainPanelVC: FloatingPanelController!
|
|
private var detailPanelVC: FloatingPanelController!
|
|
private var settingsPanelVC: FloatingPanelController!
|
|
|
|
private lazy var pagePanelController = PagePanelController()
|
|
|
|
private var mainPanelObserves: [NSKeyValueObservation] = []
|
|
|
|
init(mainVC: MainViewController) {
|
|
self.mainVC = mainVC
|
|
}
|
|
|
|
func set(useCase: UseCase) {
|
|
self.useCase = useCase
|
|
|
|
let contentVC = useCase.makeContentViewController(with: mainVC.storyboard!)
|
|
|
|
detailPanelVC?.removePanelFromParent(animated: true, completion: nil)
|
|
detailPanelVC = nil
|
|
|
|
switch useCase {
|
|
case .showDetail:
|
|
detailPanelVC?.removePanelFromParent(animated: false)
|
|
|
|
// Initialize FloatingPanelController
|
|
detailPanelVC = FloatingPanelController()
|
|
detailPanelVC.delegate = self
|
|
|
|
let appearance = SurfaceAppearance()
|
|
appearance.cornerRadius = 6.0
|
|
detailPanelVC.surfaceView.appearance = appearance
|
|
|
|
// Set a content view controller
|
|
detailPanelVC.set(contentViewController: contentVC)
|
|
|
|
detailPanelVC.contentMode = .fitToBounds
|
|
(contentVC as? DetailViewController)?.intrinsicHeightConstraint.isActive = false
|
|
|
|
// Add FloatingPanel to self.view
|
|
detailPanelVC.addPanel(toParent: mainVC, animated: true)
|
|
case .showModal, .showTabBar:
|
|
let modalVC = contentVC
|
|
modalVC.modalPresentationStyle = .fullScreen
|
|
mainVC.present(modalVC, animated: true, completion: nil)
|
|
|
|
case .showPageView:
|
|
let pageVC = pagePanelController.makePageViewController(for: mainVC)
|
|
mainVC.present(pageVC, animated: true, completion: nil)
|
|
|
|
case .showPageContentView:
|
|
let pageVC = pagePanelController.makePageViewControllerForContent()
|
|
self.addMainPanel(with: pageVC)
|
|
case .showPanelModal:
|
|
let fpc = FloatingPanelController()
|
|
let contentVC = mainVC.storyboard!.instantiateViewController(withIdentifier: "DetailViewController")
|
|
contentVC.loadViewIfNeeded()
|
|
(contentVC as? DetailViewController)?.modeChangeView.isHidden = true
|
|
fpc.set(contentViewController: contentVC)
|
|
fpc.delegate = self
|
|
|
|
let appearance = SurfaceAppearance()
|
|
appearance.cornerRadius = 38.5
|
|
fpc.surfaceView.appearance = appearance
|
|
fpc.backdropView.dismissalTapGestureRecognizer.isEnabled = true
|
|
|
|
fpc.isRemovalInteractionEnabled = true
|
|
|
|
mainVC.present(fpc, animated: true, completion: nil)
|
|
|
|
case .showMultiPanelModal:
|
|
let fpc = MultiPanelController()
|
|
mainVC.present(fpc, animated: true, completion: nil)
|
|
|
|
case .showPanelInSheetModal:
|
|
let fpc = FloatingPanelController()
|
|
let contentVC = UIViewController()
|
|
fpc.set(contentViewController: contentVC)
|
|
fpc.delegate = self
|
|
|
|
let apprearance = SurfaceAppearance()
|
|
apprearance.cornerRadius = 38.5
|
|
apprearance.shadows = []
|
|
fpc.surfaceView.appearance = apprearance
|
|
fpc.isRemovalInteractionEnabled = true
|
|
|
|
let mvc = UIViewController()
|
|
mvc.view.backgroundColor = UIColor(displayP3Red: 2/255, green: 184/255, blue: 117/255, alpha: 1.0)
|
|
fpc.addPanel(toParent: mvc)
|
|
mainVC.present(mvc, animated: true, completion: nil)
|
|
case .showContentInset:
|
|
let contentViewController = UIViewController()
|
|
contentViewController.view.backgroundColor = .green
|
|
|
|
let fpc = FloatingPanelController()
|
|
fpc.set(contentViewController: contentViewController)
|
|
fpc.surfaceView.contentPadding = .init(top: 20, left: 20, bottom: 20, right: 20)
|
|
|
|
fpc.delegate = self
|
|
fpc.isRemovalInteractionEnabled = true
|
|
mainVC.present(fpc, animated: true, completion: nil)
|
|
|
|
case .showContainerMargins:
|
|
let fpc = FloatingPanelController()
|
|
|
|
let appearance = SurfaceAppearance()
|
|
appearance.cornerRadius = 38.5
|
|
fpc.surfaceView.appearance = appearance
|
|
|
|
fpc.surfaceView.backgroundColor = .red
|
|
fpc.surfaceView.containerMargins = .init(top: 24.0, left: 8.0, bottom: max(mainVC.layoutInsets.bottom, 8.0), right: 8.0)
|
|
#if swift(>=5.1) // Actually Xcode 11 or later
|
|
if #available(iOS 13.0, *) {
|
|
fpc.surfaceView.layer.cornerCurve = .continuous
|
|
}
|
|
#endif
|
|
|
|
fpc.delegate = self
|
|
fpc.isRemovalInteractionEnabled = true
|
|
mainVC.present(fpc, animated: true, completion: nil)
|
|
default:
|
|
self.addMainPanel(with: contentVC)
|
|
}
|
|
}
|
|
|
|
private func addMainPanel(with contentVC: UIViewController) {
|
|
mainPanelObserves.removeAll()
|
|
|
|
let oldMainPanelVC = mainPanelVC
|
|
|
|
mainPanelVC = FloatingPanelController()
|
|
mainPanelVC.delegate = self
|
|
mainPanelVC.contentInsetAdjustmentBehavior = .always
|
|
|
|
let appearance = SurfaceAppearance()
|
|
appearance.cornerRadius = 6.0
|
|
mainPanelVC.surfaceView.appearance = appearance
|
|
|
|
set(contentViewController: contentVC)
|
|
|
|
useCase.setUpInteraction(for: self)
|
|
|
|
// Add FloatingPanel to self.view
|
|
if let oldMainPanelVC = oldMainPanelVC {
|
|
oldMainPanelVC.removePanelFromParent(animated: true, completion: {
|
|
self.mainPanelVC.addPanel(toParent: self.mainVC, animated: true)
|
|
})
|
|
} else {
|
|
mainPanelVC.addPanel(toParent: mainVC, animated: true)
|
|
}
|
|
}
|
|
|
|
private func set(contentViewController contentVC: UIViewController) {
|
|
mainPanelVC.set(contentViewController: contentVC)
|
|
// Track a scroll view
|
|
switch contentVC {
|
|
case let consoleVC as DebugTextViewController:
|
|
mainPanelVC.track(scrollView: consoleVC.textView)
|
|
|
|
case let contentVC as DebugTableViewController:
|
|
let ob = contentVC.tableView.observe(\.isEditing) { (tableView, _) in
|
|
self.mainPanelVC.panGestureRecognizer.isEnabled = !tableView.isEditing
|
|
}
|
|
mainPanelObserves.append(ob)
|
|
mainPanelVC.track(scrollView: contentVC.tableView)
|
|
case let contentVC as NestedScrollViewController:
|
|
mainPanelVC.track(scrollView: contentVC.scrollView)
|
|
case let navVC as UINavigationController:
|
|
if let rootVC = (navVC.topViewController as? MainViewController) {
|
|
rootVC.loadViewIfNeeded()
|
|
mainPanelVC.track(scrollView: rootVC.tableView)
|
|
}
|
|
case let contentVC as ImageViewController:
|
|
if #available(iOS 11.0, *) {
|
|
let mode: ImageViewController.Mode = (useCase == .showAdaptivePanelWithCustomGuide) ? .withHeaderFooter : .onlyImage
|
|
let layoutGuide = contentVC.layoutGuideFor(mode: mode)
|
|
mainPanelVC.layout = ImageViewController.PanelLayout(targetGuide: layoutGuide)
|
|
} else {
|
|
mainPanelVC.layout = ImageViewController.PanelLayout(targetGuide: nil)
|
|
}
|
|
mainPanelVC.delegate = nil
|
|
mainPanelVC.isRemovalInteractionEnabled = true
|
|
mainPanelVC.track(scrollView: contentVC.scrollView)
|
|
default:
|
|
break
|
|
}
|
|
}
|
|
|
|
@objc
|
|
fileprivate func handleSurface(tapGesture: UITapGestureRecognizer) {
|
|
switch mainPanelVC.state {
|
|
case .full:
|
|
mainPanelVC.move(to: .half, animated: true)
|
|
default:
|
|
mainPanelVC.move(to: .full, animated: true)
|
|
}
|
|
}
|
|
|
|
func setUpSettingsPanel(for mainVC: MainViewController) {
|
|
guard settingsPanelVC == nil else { return }
|
|
// Initialize FloatingPanelController
|
|
settingsPanelVC = FloatingPanelController()
|
|
|
|
let appearance = SurfaceAppearance()
|
|
appearance.cornerRadius = 6.0
|
|
settingsPanelVC.surfaceView.appearance = appearance
|
|
|
|
settingsPanelVC.isRemovalInteractionEnabled = true
|
|
settingsPanelVC.backdropView.dismissalTapGestureRecognizer.isEnabled = true
|
|
settingsPanelVC.delegate = self
|
|
|
|
let contentVC = mainVC.storyboard?.instantiateViewController(withIdentifier: "SettingsViewController")
|
|
|
|
// Set a content view controller
|
|
settingsPanelVC.set(contentViewController: contentVC)
|
|
|
|
// Add FloatingPanel to self.view
|
|
settingsPanelVC.addPanel(toParent: mainVC, animated: true)
|
|
}
|
|
}
|
|
|
|
extension UseCaseController: FloatingPanelControllerDelegate {
|
|
func floatingPanel(_ vc: FloatingPanelController, contentOffsetForPinning trackingScrollView: UIScrollView) -> CGPoint {
|
|
if useCase == .showNavigationController, #available(iOS 11.0, *) {
|
|
// 148.0 is the SafeArea's top value for a navigation bar with a large title.
|
|
return CGPoint(x: 0.0, y: 0.0 - trackingScrollView.contentInset.top - 148.0)
|
|
}
|
|
return CGPoint(x: 0.0, y: 0.0 - trackingScrollView.contentInset.top)
|
|
}
|
|
|
|
func floatingPanel(_ vc: FloatingPanelController, layoutFor newCollection: UITraitCollection) -> FloatingPanelLayout {
|
|
if vc == settingsPanelVC {
|
|
return IntrinsicPanelLayout()
|
|
}
|
|
|
|
switch useCase {
|
|
case .showTopPositionedPanel:
|
|
return TopPositionedPanelLayout()
|
|
case .showRemovablePanel:
|
|
return newCollection.verticalSizeClass == .compact ? RemovablePanelLandscapeLayout() : RemovablePanelLayout()
|
|
case .showIntrinsicView:
|
|
return IntrinsicPanelLayout()
|
|
case .showPanelModal:
|
|
if vc != mainPanelVC && vc != detailPanelVC {
|
|
return ModalPanelLayout()
|
|
}
|
|
fallthrough
|
|
case .showContentInset:
|
|
return FloatingPanelBottomLayout()
|
|
case .showCustomStatePanel:
|
|
return FloatingPanelLayoutWithCustomState()
|
|
default:
|
|
return (newCollection.verticalSizeClass == .compact) ? FloatingPanelBottomLayout() : mainVC
|
|
}
|
|
}
|
|
|
|
func floatingPanelDidRemove(_ vc: FloatingPanelController) {
|
|
switch vc {
|
|
case settingsPanelVC:
|
|
settingsPanelVC = nil
|
|
default:
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
extension UseCaseController: UIGestureRecognizerDelegate {
|
|
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
|
|
switch useCase {
|
|
case .showNestedScrollView:
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRequireFailureOf otherGestureRecognizer: UIGestureRecognizer) -> Bool {
|
|
return false
|
|
}
|
|
}
|
|
|
|
extension UseCase {
|
|
func makeContentViewController(with storyboard: UIStoryboard) -> UIViewController {
|
|
guard let storyboardID = self.storyboardID else { return DebugTableViewController() }
|
|
return storyboard.instantiateViewController(withIdentifier: storyboardID)
|
|
}
|
|
|
|
func setUpInteraction(for useCaseController: UseCaseController) {
|
|
let mainVC = useCaseController.mainVC
|
|
let mainPanelVC = useCaseController.mainPanelVC!
|
|
|
|
// Enable tap-to-hide and removal interaction
|
|
switch self {
|
|
case .trackingTableView:
|
|
let tapGesture = UITapGestureRecognizer(target: useCaseController, action: #selector(UseCaseController.handleSurface(tapGesture:)))
|
|
tapGesture.cancelsTouchesInView = false
|
|
tapGesture.numberOfTapsRequired = 2
|
|
// Prevents a delay to response a tap in menus of DebugTableViewController.
|
|
tapGesture.delaysTouchesEnded = false
|
|
mainPanelVC.surfaceView.addGestureRecognizer(tapGesture)
|
|
case .showNestedScrollView:
|
|
mainPanelVC.panGestureRecognizer.delegateProxy = useCaseController
|
|
case .showPageContentView:
|
|
if let page = (mainPanelVC.contentViewController as? UIPageViewController)?.viewControllers?.first {
|
|
mainPanelVC.track(scrollView: (page as! DebugTableViewController).tableView)
|
|
}
|
|
case .showRemovablePanel, .showIntrinsicView:
|
|
mainPanelVC.isRemovalInteractionEnabled = true
|
|
mainPanelVC.backdropView.dismissalTapGestureRecognizer.isEnabled = true
|
|
case .showNavigationController:
|
|
mainPanelVC.contentInsetAdjustmentBehavior = .never
|
|
case .showTopPositionedPanel: // For debug
|
|
let contentVC = UIViewController()
|
|
contentVC.view.backgroundColor = .red
|
|
mainPanelVC.set(contentViewController: contentVC)
|
|
mainPanelVC.addPanel(toParent: mainVC, animated: true)
|
|
return
|
|
default:
|
|
break
|
|
}
|
|
}
|
|
}
|