00ce232420
* 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
249 lines
9.5 KiB
Swift
249 lines
9.5 KiB
Swift
// Copyright 2018-Present Shin Yamamoto. All rights reserved. MIT license.
|
|
|
|
import UIKit
|
|
import FloatingPanel
|
|
|
|
final class TabBarViewController: UITabBarController {}
|
|
|
|
final class TabBarContentViewController: UIViewController {
|
|
enum Tab3Mode {
|
|
case changeOffset
|
|
case changeAutoLayout
|
|
var label: String {
|
|
switch self {
|
|
case .changeAutoLayout: return "Use AutoLayout(OK)"
|
|
case .changeOffset: return "Use ContentOffset(NG)"
|
|
}
|
|
}
|
|
}
|
|
lazy var fpc = FloatingPanelController()
|
|
var consoleVC: DebugTextViewController!
|
|
|
|
var threeLayout: ThreeTabBarPanelLayout!
|
|
var tab3Mode: Tab3Mode = .changeAutoLayout
|
|
var switcherLabel: UILabel!
|
|
|
|
override func viewDidLoad() {
|
|
fpc.delegate = self
|
|
|
|
let appearance = SurfaceAppearance()
|
|
appearance.cornerRadius = 6.0
|
|
fpc.surfaceView.appearance = appearance
|
|
|
|
// Set a content view controller and track the scroll view
|
|
let consoleVC = storyboard?.instantiateViewController(withIdentifier: "ConsoleViewController") as! DebugTextViewController
|
|
fpc.set(contentViewController: consoleVC)
|
|
consoleVC.textView.delegate = self // MUST call it before fpc.track(scrollView:)
|
|
fpc.track(scrollView: consoleVC.textView)
|
|
self.consoleVC = consoleVC
|
|
|
|
// Add FloatingPanel to self.view
|
|
fpc.addPanel(toParent: self)
|
|
|
|
|
|
switch tabBarItem.tag {
|
|
case 1:
|
|
fpc.behavior = TwoTabBarPanelBehavior()
|
|
case 2:
|
|
let switcher = UISwitch()
|
|
fpc.view.addSubview(switcher)
|
|
switcher.translatesAutoresizingMaskIntoConstraints = false
|
|
NSLayoutConstraint.activate([
|
|
switcher.bottomAnchor.constraint(equalTo: fpc.surfaceView.topAnchor, constant: -16.0),
|
|
switcher.rightAnchor.constraint(equalTo: fpc.surfaceView.rightAnchor, constant: -16.0),
|
|
])
|
|
switcher.isOn = true
|
|
switcher.tintColor = .white
|
|
switcher.backgroundColor = .white
|
|
switcher.layer.cornerRadius = 16.0
|
|
switcher.addTarget(self,
|
|
action: #selector(changeTab3Mode(_:)),
|
|
for: .valueChanged)
|
|
let label = UILabel()
|
|
label.text = tab3Mode.label
|
|
fpc.view.addSubview(label)
|
|
switcherLabel = label
|
|
label.translatesAutoresizingMaskIntoConstraints = false
|
|
NSLayoutConstraint.activate([
|
|
label.centerYAnchor.constraint(equalTo: switcher.centerYAnchor, constant: 0.0),
|
|
label.rightAnchor.constraint(equalTo: switcher.leftAnchor, constant: -16.0),
|
|
])
|
|
|
|
// Turn off the mask instead of content inset change
|
|
consoleVC.textView.clipsToBounds = false
|
|
default:
|
|
break
|
|
}
|
|
}
|
|
|
|
override func viewDidAppear(_ animated: Bool) {
|
|
super.viewDidAppear(animated)
|
|
fpc.invalidateLayout()
|
|
}
|
|
|
|
// MARK: - Action
|
|
|
|
@IBAction func close(sender: UIButton) {
|
|
dismiss(animated: true, completion: nil)
|
|
}
|
|
|
|
// MARK: - Private
|
|
|
|
@objc
|
|
private func changeTab3Mode(_ sender: UISwitch) {
|
|
if sender.isOn {
|
|
tab3Mode = .changeAutoLayout
|
|
} else {
|
|
tab3Mode = .changeOffset
|
|
}
|
|
switcherLabel.text = tab3Mode.label
|
|
}
|
|
}
|
|
|
|
extension TabBarContentViewController: UITextViewDelegate {
|
|
func scrollViewDidScroll(_ scrollView: UIScrollView) {
|
|
guard self.tabBarItem.tag == 2 else { return }
|
|
// Reset an invalid content offset by a user after updating the layout
|
|
// of `consoleVC.textView`.
|
|
// NOTE: FloatingPanel doesn't implicitly reset the offset(i.e.
|
|
// Using KVO of `scrollView.contentOffset`). Because it can lead to an
|
|
// infinite loop if a user also resets a content offset as below and,
|
|
// in the situation, a user has to modify the library.
|
|
if fpc.state != .full, fpc.surfaceLocation.y > fpc.surfaceLocation(for: .full).y {
|
|
scrollView.contentOffset = .zero
|
|
}
|
|
}
|
|
}
|
|
|
|
extension TabBarContentViewController: FloatingPanelControllerDelegate {
|
|
// MARK: - FloatingPanel
|
|
|
|
func floatingPanel(_ vc: FloatingPanelController, layoutFor newCollection: UITraitCollection) -> FloatingPanelLayout {
|
|
switch self.tabBarItem.tag {
|
|
case 0:
|
|
return OneTabBarPanelLayout()
|
|
case 1:
|
|
return TwoTabBarPanelLayout()
|
|
case 2:
|
|
threeLayout = ThreeTabBarPanelLayout(parent: self)
|
|
return threeLayout
|
|
default:
|
|
return FloatingPanelBottomLayout()
|
|
}
|
|
}
|
|
|
|
func floatingPanelDidMove(_ vc: FloatingPanelController) {
|
|
guard self.tabBarItem.tag == 2 else { return }
|
|
switch tab3Mode {
|
|
case .changeAutoLayout:
|
|
/* Good solution: Manipulate top constraint */
|
|
assert(consoleVC.textViewTopConstraint != nil)
|
|
let safeAreaTop = vc.layoutInsets.top
|
|
if vc.surfaceLocation.y + threeLayout.topPadding < safeAreaTop {
|
|
consoleVC.textViewTopConstraint?.constant = min(safeAreaTop - vc.surfaceLocation.y,
|
|
safeAreaTop)
|
|
} else {
|
|
consoleVC.textViewTopConstraint?.constant = threeLayout.topPadding
|
|
}
|
|
case .changeOffset:
|
|
/*
|
|
Bad solution: Manipulate scroll content inset
|
|
|
|
FloatingPanelController keeps a content offset in moving a panel
|
|
so that changing content inset or offset causes a buggy behavior.
|
|
*/
|
|
guard let scrollView = consoleVC.textView else { return }
|
|
var insets = vc.adjustedContentInsets
|
|
if vc.surfaceView.frame.minY < vc.layoutInsets.top {
|
|
insets.top = vc.layoutInsets.top - vc.surfaceView.frame.minY
|
|
} else {
|
|
insets.top = 0.0
|
|
}
|
|
scrollView.contentInset = insets
|
|
|
|
if vc.surfaceView.frame.minY > 0 {
|
|
scrollView.contentOffset = CGPoint(x: 0.0,
|
|
y: 0.0 - scrollView.contentInset.top)
|
|
}
|
|
}
|
|
|
|
if vc.surfaceLocation.y > vc.surfaceLocation(for: .half).y {
|
|
let progress = (vc.surfaceLocation.y - vc.surfaceLocation(for: .half).y)
|
|
/ (vc.surfaceLocation(for: .tip).y - vc.surfaceLocation(for: .half).y)
|
|
threeLayout.leftConstraint.constant = max(min(progress, 1.0), 0.0) * threeLayout.sideMargin
|
|
threeLayout.rightConstraint.constant = -max(min(progress, 1.0), 0.0) * threeLayout.sideMargin
|
|
} else {
|
|
threeLayout.leftConstraint.constant = 0.0
|
|
threeLayout.rightConstraint.constant = 0.0
|
|
}
|
|
}
|
|
}
|
|
|
|
class OneTabBarPanelLayout: FloatingPanelLayout {
|
|
var initialState: FloatingPanelState { .tip }
|
|
var position: FloatingPanelPosition { .bottom }
|
|
var anchors: [FloatingPanelState : FloatingPanelLayoutAnchoring] {
|
|
return [
|
|
.full: FloatingPanelLayoutAnchor(absoluteInset: 16.0, edge: .top, referenceGuide: .safeArea),
|
|
.tip: FloatingPanelLayoutAnchor(absoluteInset: 22.0, edge: .bottom, referenceGuide: .safeArea)
|
|
]
|
|
}
|
|
}
|
|
|
|
class TwoTabBarPanelLayout: FloatingPanelLayout {
|
|
var initialState: FloatingPanelState { .half }
|
|
var position: FloatingPanelPosition { .bottom }
|
|
var anchors: [FloatingPanelState : FloatingPanelLayoutAnchoring] {
|
|
return [
|
|
.full: FloatingPanelLayoutAnchor(absoluteInset: 100.0, edge: .top, referenceGuide: .safeArea),
|
|
.half: FloatingPanelLayoutAnchor(absoluteInset: 261.0, edge: .bottom, referenceGuide: .safeArea)
|
|
]
|
|
}
|
|
}
|
|
|
|
class TwoTabBarPanelBehavior: FloatingPanelBehavior {
|
|
func allowsRubberBanding(for edges: UIRectEdge) -> Bool {
|
|
return [UIRectEdge.top, UIRectEdge.bottom].contains(edges)
|
|
}
|
|
}
|
|
|
|
|
|
class ThreeTabBarPanelLayout: FloatingPanelLayout {
|
|
weak var parentVC: UIViewController!
|
|
|
|
var leftConstraint: NSLayoutConstraint!
|
|
var rightConstraint: NSLayoutConstraint!
|
|
|
|
let topPadding: CGFloat = 17.0
|
|
let sideMargin: CGFloat = 16.0
|
|
|
|
init(parent: UIViewController) {
|
|
parentVC = parent
|
|
}
|
|
|
|
var initialState: FloatingPanelState { .half }
|
|
var position: FloatingPanelPosition { .bottom }
|
|
var anchors: [FloatingPanelState : FloatingPanelLayoutAnchoring] {
|
|
return [
|
|
.full: FloatingPanelLayoutAnchor(absoluteInset: 0.0, edge: .top, referenceGuide: .superview),
|
|
.half: FloatingPanelLayoutAnchor(absoluteInset: 261.0 + parentVC.layoutInsets.bottom, edge: .bottom, referenceGuide: .superview),
|
|
.tip: FloatingPanelLayoutAnchor(absoluteInset: 88.0 + parentVC.layoutInsets.bottom, edge: .bottom, referenceGuide: .superview),
|
|
]
|
|
}
|
|
|
|
func backdropAlpha(for state: FloatingPanelState) -> CGFloat {
|
|
return 0.3
|
|
}
|
|
|
|
func prepareLayout(surfaceView: UIView, in view: UIView) -> [NSLayoutConstraint] {
|
|
if #available(iOS 11.0, *) {
|
|
leftConstraint = surfaceView.leftAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leftAnchor, constant: 0.0)
|
|
rightConstraint = surfaceView.rightAnchor.constraint(equalTo: view.safeAreaLayoutGuide.rightAnchor, constant: 0.0)
|
|
} else {
|
|
leftConstraint = surfaceView.leftAnchor.constraint(equalTo: view.leftAnchor, constant: 0.0)
|
|
rightConstraint = surfaceView.rightAnchor.constraint(equalTo: view.rightAnchor, constant: 0.0)
|
|
}
|
|
return [ leftConstraint, rightConstraint ]
|
|
}
|
|
}
|