1336 lines
42 KiB
Swift
1336 lines
42 KiB
Swift
//
|
|
// ContainerView.swift
|
|
// PatternsSwift
|
|
//
|
|
// Created by mrustaa on 21/04/2020.
|
|
// Copyright © 2020 mrustaa. All rights reserved.
|
|
//
|
|
|
|
import UIKit
|
|
|
|
open class ContainerController: NSObject {
|
|
|
|
// MARK: Views
|
|
|
|
public var view: ContainerView!
|
|
|
|
public var shadowButton: UIButton!
|
|
|
|
public var controller: UIViewController?
|
|
|
|
public var scrollView: UIScrollView?
|
|
|
|
public var headerView: UIView?
|
|
|
|
public var footerView: UIView?
|
|
|
|
// MARK: Layout
|
|
|
|
public var layout: ContainerLayout = ContainerLayout()
|
|
|
|
// MARK: Delegate
|
|
|
|
public var delegate: ContainerControllerDelegate?
|
|
|
|
// MARK: Current Move Type
|
|
|
|
public var moveType: ContainerMoveType = .hide
|
|
|
|
public var oldMoveType: ContainerMoveType = .hide
|
|
|
|
// MARK: - Properties Scroll
|
|
|
|
private var oldTransform: CGAffineTransform = .identity
|
|
|
|
private var oldPosition: CGFloat = 0.0
|
|
|
|
private var panGesture: UIPanGestureRecognizer?
|
|
|
|
private var panBeginSavePosition: CGFloat = 0.0
|
|
|
|
private var oldOrientation: ContainerDevice.Orientation = ContainerDevice.orientation
|
|
|
|
private var isScrolling: Bool = false
|
|
|
|
private var scrollBordersRunContainer: Bool = false
|
|
|
|
private var scrollOnceBeginDragging: Bool = false
|
|
|
|
private var scrollOnceEnded: Bool = false
|
|
|
|
private var scrollBegin: Bool = false
|
|
|
|
private var scrollStartPosition: CGFloat = 0.0
|
|
|
|
private var scrollTransform = CGAffineTransform.identity
|
|
|
|
// MARK: - Properties Position
|
|
|
|
public var topBarHeight: CGFloat {
|
|
var result: CGFloat = 0.0
|
|
if let vc = controller?.navigationController, !vc.isNavigationBarHidden {
|
|
let statusBarHeight: CGFloat = ContainerDevice.statusBarHeight
|
|
let navBarHeight: CGFloat = vc.navigationBar.frame.height
|
|
result = statusBarHeight + navBarHeight
|
|
}
|
|
return result
|
|
}
|
|
|
|
public var topTranslucent: Bool {
|
|
return controller?.navigationController?.navigationBar.isTranslucent ?? false
|
|
}
|
|
|
|
private var isPortrait: Bool {
|
|
return ContainerDevice.isPortrait
|
|
}
|
|
|
|
private var deviceHeight: CGFloat {
|
|
var height: CGFloat = 0.0
|
|
if isPortrait {
|
|
height = ContainerDevice.screenMax
|
|
} else {
|
|
height = ContainerDevice.screenMin
|
|
}
|
|
height -= topBarHeight
|
|
return height
|
|
}
|
|
|
|
private var deviceWidth: CGFloat {
|
|
var width: CGFloat = 0.0
|
|
if isPortrait {
|
|
width = ContainerDevice.screenMin
|
|
} else {
|
|
width = ContainerDevice.screenMax
|
|
}
|
|
return width
|
|
}
|
|
|
|
// MARK: - Positions Move
|
|
|
|
private var positionTop: CGFloat {
|
|
var top = layout.positions.top
|
|
if !isPortrait {
|
|
if let landscape = layout.landscapePositions {
|
|
top = landscape.top
|
|
}
|
|
}
|
|
return top
|
|
}
|
|
|
|
public var positionMiddle: CGFloat {
|
|
var middle = layout.positions.middle ?? layout.positions.bottom
|
|
if !isPortrait {
|
|
if let landscapeMid = layout.landscapePositions?.middle {
|
|
middle = landscapeMid
|
|
}
|
|
}
|
|
return deviceHeight - middle
|
|
}
|
|
|
|
private var positionBottom: CGFloat {
|
|
var bottom = layout.positions.bottom
|
|
if !isPortrait {
|
|
if let landscape = layout.landscapePositions {
|
|
bottom = landscape.bottom
|
|
}
|
|
}
|
|
return deviceHeight - bottom
|
|
}
|
|
|
|
private var insetsLeft: CGFloat {
|
|
var left: CGFloat = layout.insets.left
|
|
if !isPortrait {
|
|
if let inset = layout.landscapeInsets {
|
|
left = inset.left
|
|
}
|
|
}
|
|
return left
|
|
}
|
|
|
|
private var insetsRight: CGFloat {
|
|
var right: CGFloat = layout.insets.right
|
|
if !isPortrait {
|
|
if let inset = layout.landscapeInsets {
|
|
right = inset.right
|
|
}
|
|
}
|
|
return right
|
|
}
|
|
|
|
private var middleEnable: Bool {
|
|
if isPortrait {
|
|
return layout.positions.middle != nil
|
|
} else {
|
|
if let landscapePositions = layout.landscapePositions {
|
|
return landscapePositions.middle != nil
|
|
} else {
|
|
return layout.positions.middle != nil
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - Init
|
|
|
|
public init(addTo controller: UIViewController, layout: ContainerLayout) {
|
|
super.init()
|
|
|
|
self.controller = controller
|
|
set(layout: layout)
|
|
|
|
NotificationCenter.default.addObserver(self, selector: #selector(rotated), name: UIDevice.orientationDidChangeNotification, object: nil)
|
|
|
|
createShadowButton()
|
|
createContainerView()
|
|
|
|
move(type: layout.startPosition, animation: false)
|
|
}
|
|
|
|
// MARK: - Remove
|
|
|
|
public func remove(completion: (() -> Void)? = nil) {
|
|
|
|
NotificationCenter.default.removeObserver(self)
|
|
|
|
move(type: .hide, completion:
|
|
|
|
{ [weak self] in
|
|
guard let _self = self else { return }
|
|
|
|
_self.scrollView?.removeFromSuperview()
|
|
_self.headerView?.removeFromSuperview()
|
|
_self.footerView?.removeFromSuperview()
|
|
_self.shadowButton.removeFromSuperview()
|
|
_self.view.removeFromSuperview()
|
|
|
|
completion?()
|
|
})
|
|
}
|
|
|
|
// MARK: - Rotated
|
|
|
|
@objc func rotated() {
|
|
|
|
let orint = UIDevice.current.orientation
|
|
if orint == .faceUp || orint == .faceDown { return }
|
|
if ContainerDevice.orientation == oldOrientation { return }
|
|
oldOrientation = ContainerDevice.orientation
|
|
|
|
if isPortrait {
|
|
shadowButton.isHidden = !layout.backgroundShadowShow
|
|
} else {
|
|
if let landscapeShadowShow = layout.landscapeBackgroundShadowShow {
|
|
shadowButton.isHidden = !landscapeShadowShow
|
|
} else {
|
|
shadowButton.isHidden = !layout.backgroundShadowShow
|
|
}
|
|
}
|
|
|
|
delegate?.containerControllerRotation(self)
|
|
|
|
calculationView()
|
|
calculationScrollViewHeight(from: .rotation)
|
|
|
|
move(type: moveType, from: .rotation)
|
|
}
|
|
|
|
// MARK: - Update Layout
|
|
|
|
func set(layout: ContainerLayout) {
|
|
self.layout = layout
|
|
calculationViews()
|
|
}
|
|
|
|
// MARK: Set
|
|
|
|
func set(movingEnabled: Bool) {
|
|
layout.movingEnabled = movingEnabled
|
|
scrollView?.isScrollEnabled = movingEnabled
|
|
panGesture?.isEnabled = movingEnabled
|
|
}
|
|
|
|
func set(trackingPosition: Bool) {
|
|
layout.trackingPosition = trackingPosition
|
|
}
|
|
|
|
func set(footerPadding: CGFloat) {
|
|
layout.footerPadding = footerPadding
|
|
calculationViews()
|
|
}
|
|
|
|
// MARK: Scroll Insets
|
|
|
|
func set(scrollIndicatorTop: CGFloat) {
|
|
layout.scrollIndicatorInsets = UIEdgeInsets(top: scrollIndicatorTop, left: 0, bottom: layout.scrollIndicatorInsets.bottom, right: 0)
|
|
calculationViews()
|
|
}
|
|
|
|
func set(scrollIndicatorBottom: CGFloat) {
|
|
layout.scrollIndicatorInsets = UIEdgeInsets(top: layout.scrollIndicatorInsets.top, left: 0, bottom: scrollIndicatorBottom, right: 0)
|
|
calculationViews()
|
|
}
|
|
|
|
func set(scrollInsetsTop: CGFloat) {
|
|
layout.scrollInsets = UIEdgeInsets(top: scrollInsetsTop, left: 0, bottom: layout.scrollInsets.bottom, right: 0)
|
|
calculationViews()
|
|
}
|
|
|
|
func set(scrollInsetsBottom: CGFloat) {
|
|
layout.scrollInsets = UIEdgeInsets(top: layout.scrollInsets.top, left: 0, bottom: scrollInsetsBottom, right: 0)
|
|
calculationViews()
|
|
}
|
|
|
|
// MARK: Portrait
|
|
|
|
func set(top: CGFloat) {
|
|
layout.positions.top = top
|
|
}
|
|
|
|
func set(middle: CGFloat?) {
|
|
layout.positions.middle = middle
|
|
}
|
|
|
|
func set(bottom: CGFloat) {
|
|
layout.positions.bottom = bottom
|
|
}
|
|
|
|
func set(right: CGFloat) {
|
|
layout.insets.right = right
|
|
if isPortrait { calculationViews() }
|
|
}
|
|
|
|
func set(left: CGFloat) {
|
|
layout.insets.left = left
|
|
if isPortrait { calculationViews() }
|
|
}
|
|
|
|
func set(backgroundShadowShow: Bool) {
|
|
layout.backgroundShadowShow = backgroundShadowShow
|
|
if isPortrait { move(type: moveType) }
|
|
}
|
|
|
|
// MARK: Landscape
|
|
|
|
func updateLandscapeLayout() {
|
|
if layout.landscapePositions == nil {
|
|
layout.landscapePositions = ContainerPosition.zero
|
|
}
|
|
if layout.landscapeInsets == nil {
|
|
layout.landscapeInsets = ContainerInsets.zero
|
|
}
|
|
}
|
|
|
|
func setLandscape(top: CGFloat) {
|
|
updateLandscapeLayout()
|
|
layout.landscapePositions?.top = top
|
|
}
|
|
|
|
func setLandscape(middle: CGFloat?) {
|
|
updateLandscapeLayout()
|
|
layout.landscapePositions?.middle = middle
|
|
}
|
|
|
|
func setLandscape(bottom: CGFloat) {
|
|
updateLandscapeLayout()
|
|
layout.landscapePositions?.bottom = bottom
|
|
}
|
|
|
|
func setLandscape(right: CGFloat) {
|
|
updateLandscapeLayout()
|
|
layout.landscapeInsets?.right = right
|
|
if !isPortrait { calculationViews() }
|
|
}
|
|
|
|
func setLandscape(left: CGFloat) {
|
|
updateLandscapeLayout()
|
|
layout.landscapeInsets?.left = left
|
|
if !isPortrait { calculationViews() }
|
|
}
|
|
|
|
func setLandscape(backgroundShadowShow: Bool) {
|
|
layout.landscapeBackgroundShadowShow = backgroundShadowShow
|
|
if !isPortrait { move(type: moveType) }
|
|
}
|
|
|
|
|
|
// MARK: - Create Shadow-Button
|
|
|
|
private func createShadowButton() {
|
|
shadowButton = UIButton(frame: CGRect(x: 0, y: 0, width: ContainerDevice.screenMax, height: ContainerDevice.screenMax))
|
|
shadowButton.isUserInteractionEnabled = false
|
|
shadowButton.backgroundColor = .black
|
|
shadowButton.alpha = 0.0
|
|
shadowButton.addTarget(self, action: #selector(shadowButtonAction), for: .touchUpInside)
|
|
controller?.view.addSubview(shadowButton)
|
|
}
|
|
|
|
@objc private func shadowButtonAction() {
|
|
delegate?.containerControllerShadowClick(self)
|
|
}
|
|
|
|
// MARK: - Create Container-View
|
|
|
|
private func createContainerView() {
|
|
let frame = CGRect(x: 0, y: 0, width: deviceWidth, height: deviceHeight * 2)
|
|
view = ContainerView(frame: frame)
|
|
view.backgroundColor = .white
|
|
controller?.view.addSubview(view)
|
|
|
|
panGesture = UIPanGestureRecognizer(target: self, action: #selector(handlePan(_:)))
|
|
panGesture?.isEnabled = layout.movingEnabled
|
|
if let panGesture = panGesture {
|
|
view.addGestureRecognizer(panGesture)
|
|
}
|
|
}
|
|
|
|
// MARK: - Add Header
|
|
|
|
public func removeHeaderView() {
|
|
if let headerView = self.headerView {
|
|
headerView.removeFromSuperview()
|
|
}
|
|
headerView = nil
|
|
calculationViews()
|
|
}
|
|
|
|
public func add(headerView: UIView) {
|
|
removeHeaderView()
|
|
self.headerView = headerView
|
|
view.contentView?.addSubview(headerView)
|
|
calculationViews()
|
|
}
|
|
|
|
// MARK: - Add Footer
|
|
|
|
public func removeFooterView() {
|
|
if let footerView = self.footerView {
|
|
footerView.removeFromSuperview()
|
|
}
|
|
footerView = nil
|
|
calculationViews()
|
|
}
|
|
|
|
public func add(footerView: UIView) {
|
|
removeFooterView()
|
|
self.footerView = footerView
|
|
controller?.view.addSubview(footerView)
|
|
calculationViews()
|
|
}
|
|
|
|
// MARK: - Add ScrollView
|
|
|
|
public func removeScrollView() {
|
|
if let scroll = self.scrollView {
|
|
scroll.removeFromSuperview()
|
|
}
|
|
scrollView = nil
|
|
calculationViews()
|
|
}
|
|
|
|
public func add(scrollView: UIScrollView) {
|
|
removeScrollView()
|
|
self.scrollView = scrollView
|
|
|
|
scrollView.isScrollEnabled = layout.movingEnabled
|
|
scrollView.autoresizingMask = [.flexibleLeftMargin,
|
|
.flexibleWidth,
|
|
.flexibleRightMargin,
|
|
.flexibleTopMargin,
|
|
.flexibleHeight,
|
|
.flexibleBottomMargin]
|
|
|
|
scrollView.isScrollEnabled = (moveType == .top)
|
|
|
|
if scrollView.delegate == nil {
|
|
scrollView.delegate = self
|
|
}
|
|
|
|
if let tableAdapterView = scrollView as? TableAdapterView {
|
|
tableAdapterView.delegate = self
|
|
tableAdapterView.dataSource = self
|
|
}
|
|
|
|
if let collectionAdapterView = scrollView as? CollectionAdapterView {
|
|
collectionAdapterView.delegate = self
|
|
collectionAdapterView.dataSource = self
|
|
}
|
|
|
|
view.contentView?.addSubview(scrollView)
|
|
calculationViews()
|
|
}
|
|
|
|
// MARK: - Pan Gesture
|
|
|
|
@objc private func handlePan(_ gesture: UIPanGestureRecognizer) {
|
|
|
|
view.layer.removeAllAnimations()
|
|
scrollView?.layer.removeAllAnimations()
|
|
|
|
switch gesture.state {
|
|
case .began:
|
|
|
|
panBeginSavePosition = view.transform.ty
|
|
|
|
case .changed:
|
|
|
|
var transform = view.transform
|
|
transform.ty = (panBeginSavePosition + gesture.translation(in: view).y)
|
|
|
|
if transform.ty < 0 {
|
|
|
|
transform.ty = (positionTop / 2)
|
|
|
|
} else if transform.ty < positionTop {
|
|
|
|
transform.ty = ((positionTop / 2) + (transform.ty / 2))
|
|
}
|
|
|
|
let position = transform.ty
|
|
let type: ContainerMoveType = moveType
|
|
let from: ContainerFromType = .pan
|
|
let animation = false
|
|
|
|
changeView(transform: transform)
|
|
shadowLevelAlpha(position: position, animation: false)
|
|
changeFooterView(position: position)
|
|
calculationScrollViewHeight(from: from)
|
|
changeMove(position: position, type: type, animation: animation)
|
|
|
|
case .ended:
|
|
|
|
let velocityY = gesture.velocity(in: view).y
|
|
|
|
let type = calculatePositionTypeFrom(velocity: velocityY)
|
|
|
|
move(type: type, animation: true, velocity: velocityY, from: .pan)
|
|
|
|
default: break
|
|
}
|
|
}
|
|
|
|
|
|
// MARK: - Calculation Views Size
|
|
|
|
public func calculationViews() {
|
|
calculationView()
|
|
calculationScrollViewHeight()
|
|
}
|
|
|
|
private func calculationView() {
|
|
guard let view = view else { return }
|
|
|
|
let x: CGFloat = insetsLeft
|
|
let width: CGFloat = (deviceWidth - insetsRight - insetsLeft)
|
|
|
|
view.frame.origin.x = x
|
|
view.frame.size.width = width
|
|
view.frame.size.height = deviceHeight * 2
|
|
|
|
if let headerView = headerView {
|
|
headerView.frame.origin.x = 0.0
|
|
headerView.frame.origin.y = 0.0
|
|
headerView.frame.size.width = width
|
|
}
|
|
|
|
if let footerView = footerView {
|
|
footerView.frame.origin.x = x
|
|
footerView.frame.size.width = width
|
|
changeFooterView()
|
|
}
|
|
}
|
|
|
|
// MARK: - Calculation ScrollView Size
|
|
|
|
private func calculationScrollViewHeight(position: CGFloat = -1.0,
|
|
animation: Bool = false,
|
|
from: ContainerFromType = .custom,
|
|
velocity: CGFloat = 0.0,
|
|
moveType: ContainerMoveType = .custom,
|
|
moveTypeOld: ContainerMoveType = .custom) {
|
|
|
|
guard let scrollView = scrollView else { return }
|
|
scrollView.layer.removeAllAnimations()
|
|
|
|
let headerHeight: CGFloat = headerView?.frame.height ?? 0.0
|
|
|
|
var footerInsets: CGFloat = 0.0
|
|
if let footerView = footerView {
|
|
footerInsets = deviceHeight - footerView.frame.origin.y
|
|
}
|
|
|
|
var scrollInsetsBottom: CGFloat = ContainerDevice.isIphoneXBottom
|
|
if scrollInsetsBottom < footerInsets {
|
|
scrollInsetsBottom = 0.0
|
|
}
|
|
|
|
let top: CGFloat = layout.scrollInsets.top
|
|
let bottom: CGFloat = layout.scrollInsets.bottom + scrollInsetsBottom
|
|
|
|
let indicatorTop: CGFloat = layout.scrollIndicatorInsets.top
|
|
let indicatorBottom: CGFloat = layout.scrollIndicatorInsets.bottom + scrollInsetsBottom
|
|
|
|
var containerViewPositionY: CGFloat = 0.0
|
|
if position == -1.0 {
|
|
containerViewPositionY = view.transform.ty
|
|
} else {
|
|
containerViewPositionY = position
|
|
}
|
|
|
|
let width: CGFloat = (deviceWidth - insetsRight - insetsLeft)
|
|
var height: CGFloat = (deviceHeight - (headerHeight + footerInsets + containerViewPositionY))
|
|
|
|
if height < 0 {
|
|
height = 0
|
|
}
|
|
|
|
if animation ,
|
|
!isScrolling,
|
|
footerView == nil,
|
|
oldPosition < position,
|
|
((moveType == .middle) && (0 < velocity)) || (moveType == .bottom) {
|
|
|
|
height = scrollView.frame.height
|
|
}
|
|
|
|
scrollView.frame = CGRect(x: 0, y: headerHeight, width: width, height: height)
|
|
scrollView.scrollIndicatorInsets = UIEdgeInsets(top: indicatorTop , left: 0, bottom: indicatorBottom, right: 0)
|
|
scrollView.contentInset = UIEdgeInsets(top: top, left: 0, bottom: bottom, right: 0)
|
|
}
|
|
|
|
// MARK: - Position-Type From Velocity
|
|
|
|
private func calculatePositionTypeFrom(velocity: CGFloat) -> ContainerMoveType {
|
|
|
|
var type: ContainerMoveType
|
|
|
|
let position = view.transform.ty
|
|
|
|
if middleEnable {
|
|
|
|
if position < positionTop { /// <<< (70 Top)
|
|
|
|
if 750 < velocity {
|
|
|
|
if 2500 < velocity {
|
|
type = .bottom /// ↓↓↓
|
|
} else {
|
|
type = .middle /// ↓
|
|
}
|
|
|
|
} else {
|
|
type = .top /// Default
|
|
}
|
|
|
|
} else if position > positionBottom { /// (300 Bottom) >>>
|
|
|
|
if velocity < -750 {
|
|
|
|
if velocity < -2000 {
|
|
type = .top /// ↑↑↑
|
|
} else {
|
|
type = .middle /// ↑
|
|
}
|
|
|
|
} else {
|
|
type = .bottom /// Default
|
|
}
|
|
|
|
} else {
|
|
|
|
let centerMiddleTop = (((positionMiddle - positionTop) / 2) + positionTop)
|
|
let centerBottomMiddle = (((positionBottom - positionMiddle) / 2) + positionMiddle)
|
|
|
|
if position < centerMiddleTop { /// ↑↑↑ top ...70
|
|
|
|
if 150 < velocity {
|
|
|
|
if 2500 < velocity {
|
|
type = .bottom /// ↓↓↓
|
|
} else {
|
|
type = .middle /// ↓
|
|
}
|
|
|
|
} else {
|
|
type = .top /// Default
|
|
}
|
|
|
|
} else if position < centerBottomMiddle { /// ---
|
|
|
|
if velocity < 0 {
|
|
|
|
if velocity < -150 {
|
|
type = .top /// ↑↑↑
|
|
} else {
|
|
type = .middle /// ↑
|
|
}
|
|
|
|
} else {
|
|
|
|
if 150 < velocity {
|
|
type = .bottom /// ↓↓↓
|
|
} else {
|
|
type = .middle /// ↓
|
|
}
|
|
}
|
|
|
|
} else { /// ↓↓↓
|
|
|
|
if velocity < -150 {
|
|
|
|
if velocity < -2000 {
|
|
type = .top /// ↑↑↑
|
|
} else {
|
|
type = .middle /// ↑
|
|
}
|
|
|
|
} else {
|
|
type = .bottom /// Default
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
if position < positionTop { /// <<< (70 Top)
|
|
|
|
if 750 < velocity {
|
|
type = .bottom /// ↓↓↓
|
|
} else {
|
|
type = .top /// Default
|
|
}
|
|
|
|
} else if position > positionBottom { /// (300 Bottom) >>>
|
|
|
|
if velocity < -750 {
|
|
type = .top /// ↑↑↑
|
|
} else {
|
|
type = .bottom /// Default
|
|
}
|
|
|
|
} else { /// (pos 150) - Center top...!...bottom
|
|
|
|
/// (((300 - 70 = 230) / 2 = 115) + 70) = 185
|
|
|
|
let centerTopBottom = (((positionBottom - positionTop) / 2) + positionTop)
|
|
|
|
if position < centerTopBottom { /// ↑↑↑
|
|
|
|
if 150 < velocity {
|
|
type = .bottom
|
|
} else {
|
|
type = .top /// Default
|
|
}
|
|
|
|
} else { /// ↓↓↓
|
|
|
|
if velocity < -150 {
|
|
type = .top
|
|
} else {
|
|
type = .bottom /// Default
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return type
|
|
}
|
|
|
|
// MARK: - Move
|
|
|
|
public func move(type: ContainerMoveType,
|
|
animation: Bool = true,
|
|
velocity: CGFloat = 0.0,
|
|
from: ContainerFromType = .custom,
|
|
completion: (() -> Void)? = nil) {
|
|
|
|
let position = positionMoveFrom(type: type)
|
|
|
|
move(position: position,
|
|
animation: animation,
|
|
type: type,
|
|
velocity: velocity,
|
|
from: from,
|
|
completion: completion)
|
|
}
|
|
|
|
// MARK: - Move Position
|
|
|
|
public func positionMoveFrom(type: ContainerMoveType) -> CGFloat {
|
|
|
|
switch type {
|
|
case .top: return positionTop
|
|
case .middle:
|
|
if !middleEnable { return positionBottom }
|
|
else { return positionMiddle }
|
|
case .bottom: return positionBottom
|
|
case .hide: return deviceHeight
|
|
case .custom: return 0.0
|
|
}
|
|
}
|
|
|
|
// MARK: - Move Animtaion
|
|
|
|
private var displayVelocity: CGFloat = 0.0
|
|
|
|
public func move(position: CGFloat,
|
|
animation: Bool,
|
|
type: ContainerMoveType,
|
|
velocity: CGFloat = 0.0,
|
|
from: ContainerFromType,
|
|
completion: (() -> Void)? = nil) {
|
|
|
|
if layout.movingEnabled {
|
|
scrollView?.isScrollEnabled = (type == .top)
|
|
} else {
|
|
scrollView?.isScrollEnabled = false
|
|
}
|
|
|
|
displayVelocity = velocity
|
|
|
|
oldMoveType = moveType
|
|
let oldMove = moveType
|
|
moveType = type
|
|
|
|
shadowLevelAlpha(position: position, animation: true)
|
|
|
|
let transform = CGAffineTransform(translationX: 0, y: position)
|
|
|
|
let animationComp = { [weak self] in
|
|
guard let _self = self else { return }
|
|
|
|
_self.changeView(transform: transform)
|
|
if !_self.layout.trackingPosition {
|
|
_self.changeFooterView(position: position)
|
|
_self.calculationScrollViewHeight(position: position, animation: animation, from: from, velocity: velocity, moveType: type, moveTypeOld: oldMove)
|
|
}
|
|
_self.changeMove(position: position, type: type, animation: true)
|
|
}
|
|
|
|
if animation {
|
|
|
|
animationSpringFrom(force: velocity, type: type, animation: animationComp, completion: completion)
|
|
|
|
} else {
|
|
|
|
changeFooterView(position: position)
|
|
changeView(transform: transform)
|
|
calculationScrollViewHeight(position: position, animation: animation, from: from, velocity: velocity, moveType: type, moveTypeOld: oldMove)
|
|
changeMove(position: position, type: type, animation: false)
|
|
|
|
completion?()
|
|
}
|
|
}
|
|
|
|
// MARK: - Tracking Position
|
|
|
|
@objc func animationDidUpdate(displayLink: CADisplayLink) {
|
|
guard layout.trackingPosition else { return }
|
|
guard let presentationLayer = self.view.layer.presentation() else { return }
|
|
|
|
let position = presentationLayer.frame.origin.y
|
|
changeFooterView(position: position)
|
|
calculationScrollViewHeight(position: position, from: .tracking, velocity: displayVelocity, moveType: moveType, moveTypeOld: oldMoveType)
|
|
}
|
|
|
|
private func changeView(transform: CGAffineTransform) {
|
|
oldPosition = view.frame.origin.y
|
|
oldTransform = view.transform
|
|
view.transform = transform
|
|
}
|
|
|
|
private func changeMove(position: CGFloat,
|
|
type: ContainerMoveType,
|
|
animation: Bool) {
|
|
|
|
delegate?.containerControllerMove(self, position: position, type: type, animation: animation)
|
|
}
|
|
|
|
//MARK: - Shadow Alpha Level
|
|
|
|
func shadowLevelAlpha(position: CGFloat,
|
|
animation: Bool) {
|
|
|
|
if animation {
|
|
animationSpring(duration: 0.45) { [weak self] in
|
|
guard let _self = self else { return }
|
|
_self.shadowLevelAlpha(positionY: position)
|
|
}
|
|
} else {
|
|
shadowLevelAlpha(positionY: position)
|
|
}
|
|
}
|
|
|
|
func animationSpring(duration: CGFloat = 0.45, animations: @escaping () -> Void) {
|
|
UIView.animate(withDuration: TimeInterval(duration),
|
|
delay: 0,
|
|
usingSpringWithDamping: 0.8,
|
|
initialSpringVelocity: 6.0,
|
|
options: [.allowUserInteraction],
|
|
animations: animations,
|
|
completion: nil)
|
|
|
|
}
|
|
|
|
func shadowLevelAlpha(positionY: CGFloat) {
|
|
|
|
if isPortrait {
|
|
shadowButton.isHidden = !layout.backgroundShadowShow
|
|
} else {
|
|
if let landscapeShadowShow = layout.landscapeBackgroundShadowShow {
|
|
shadowButton.isHidden = !landscapeShadowShow
|
|
} else {
|
|
shadowButton.isHidden = !layout.backgroundShadowShow
|
|
}
|
|
}
|
|
|
|
let alphaMax: CGFloat = 0.45
|
|
|
|
if positionY < positionTop {
|
|
|
|
shadowButton.alpha = alphaMax
|
|
|
|
} else if positionY < positionMiddle {
|
|
let m = positionMiddle - positionTop
|
|
let p = positionY - positionTop
|
|
let percent = (1.0 - (p / m))
|
|
let result = percent * alphaMax
|
|
|
|
shadowButton.alpha = result
|
|
} else {
|
|
shadowButton.alpha = 0.0
|
|
}
|
|
|
|
}
|
|
|
|
//MARK: - Change FooterView Position
|
|
|
|
func changeFooterView(position: CGFloat? = nil) {
|
|
guard let footer = footerView else { return }
|
|
|
|
let pos = position ?? positionMoveFrom(type: moveType)
|
|
|
|
let header = headerView?.frame.height ?? 0.0
|
|
let rr = deviceHeight - header - footer.frame.height
|
|
let result = ((rr - pos) - layout.footerPadding)
|
|
|
|
let footerViewPositionDefault = (deviceHeight - footer.frame.height)
|
|
|
|
if result < 0 {
|
|
footer.frame.origin.y = (footerViewPositionDefault + (result * (-1)))
|
|
} else {
|
|
footer.frame.origin.y = footerViewPositionDefault
|
|
}
|
|
|
|
}
|
|
|
|
// MARK: - Animation Spring + Force
|
|
|
|
func animationSpringFrom(force: CGFloat,
|
|
type: ContainerMoveType,
|
|
animation: @escaping (() -> Void),
|
|
completion: (() -> Void)? = nil) {
|
|
|
|
var velocity: CGFloat = 0
|
|
let transformY = view.transform.ty
|
|
|
|
if type == .top {
|
|
|
|
if force < 0 {
|
|
velocity = force * (-1)
|
|
}
|
|
|
|
} else if type == .bottom {
|
|
|
|
velocity = force
|
|
|
|
} else if type == .middle {
|
|
|
|
if force < 0 {
|
|
velocity = force * (-1)
|
|
} else {
|
|
velocity = force
|
|
}
|
|
}
|
|
|
|
velocity = velocity / 10000
|
|
velocity = velocity * 300
|
|
|
|
|
|
if type == .top {
|
|
|
|
if (transformY - positionTop) < 0 {
|
|
velocity = velocity * (-1)
|
|
}
|
|
|
|
} else if type == .bottom {
|
|
|
|
if 0 < (transformY - positionBottom) {
|
|
velocity = velocity * (-1)
|
|
}
|
|
}
|
|
|
|
var positionY: CGFloat = 0
|
|
|
|
if type == .top {
|
|
positionY = (transformY - positionTop)
|
|
} else if type == .bottom {
|
|
positionY = (transformY - positionBottom)
|
|
} else if type == .middle {
|
|
positionY = (transformY - positionMiddle)
|
|
} else if type == .hide {
|
|
positionY = (transformY - deviceHeight)
|
|
}
|
|
|
|
if positionY < 0 {
|
|
positionY = positionY * (-1)
|
|
}
|
|
|
|
var damping: CGFloat = 0.75
|
|
var duration: CGFloat = 0.65
|
|
|
|
let percent = (1.0 - (transformY / deviceHeight))
|
|
|
|
if 350 < positionY { /// 350...
|
|
|
|
velocity = (velocity * percent) / 3.5
|
|
|
|
if 6.5...13.5 ~= velocity {
|
|
if velocity < 9.0 {
|
|
velocity = 6.5
|
|
} else {
|
|
velocity = 13.5
|
|
}
|
|
}
|
|
|
|
damping = 0.8
|
|
duration = 0.45
|
|
|
|
} else if 200 < positionY { /// 200...350
|
|
|
|
velocity = (velocity * percent) / 2.0
|
|
|
|
if 6.5...13.5 ~= velocity {
|
|
if velocity < 9.0 {
|
|
velocity = 6.5
|
|
} else {
|
|
velocity = 13.5
|
|
}
|
|
}
|
|
|
|
damping = 0.8
|
|
duration = 0.45
|
|
|
|
} else if 150 < positionY { /// 150...200
|
|
|
|
damping = 0.7
|
|
duration = 0.55
|
|
|
|
velocity = (velocity * percent) / 2.5
|
|
|
|
if 4.7...8.6 ~= velocity {
|
|
if velocity < 6.5 {
|
|
velocity = 8.6
|
|
} else {
|
|
velocity = 4.7
|
|
}
|
|
}
|
|
|
|
} else if 100 < positionY { /// 100...150
|
|
|
|
velocity = (velocity * percent) / 2.0
|
|
|
|
} else if 50 < positionY { /// 50...100
|
|
|
|
velocity = velocity / 1.5
|
|
|
|
} else if 25 < positionY { /// 25...50
|
|
|
|
// velocity = velocity * 1.5
|
|
|
|
} else if 10 < positionY { /// 10...25
|
|
|
|
velocity = velocity * 1.5
|
|
|
|
} else { /// ...10
|
|
|
|
velocity = velocity * 3.0
|
|
|
|
}
|
|
|
|
if duration == 0.65, damping == 0.75 {
|
|
if 4.3...7.4 ~= velocity {
|
|
if velocity < 5.85 {
|
|
velocity = 7.4
|
|
} else {
|
|
velocity = 4.3
|
|
}
|
|
}
|
|
}
|
|
|
|
var displayLink: CADisplayLink?
|
|
if layout.trackingPosition {
|
|
displayLink = CADisplayLink(target: self, selector: #selector(animationDidUpdate))
|
|
displayLink?.preferredFramesPerSecond = 60
|
|
displayLink?.add(to: .main, forMode: .default)
|
|
}
|
|
|
|
UIView.animate( withDuration: TimeInterval(duration),
|
|
delay: 0.0,
|
|
usingSpringWithDamping: damping,
|
|
initialSpringVelocity: velocity,
|
|
options: [ .allowUserInteraction ],
|
|
animations: animation,
|
|
completion: { _ in
|
|
|
|
if let displayLink = displayLink {
|
|
displayLink.invalidate()
|
|
}
|
|
completion?()
|
|
})
|
|
|
|
}
|
|
}
|
|
|
|
// MARK: - Gesture Delegate
|
|
|
|
extension ContainerController: UIGestureRecognizerDelegate {
|
|
|
|
public func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
|
|
return false
|
|
}
|
|
|
|
}
|
|
|
|
// MARK: - Table Delegate
|
|
|
|
extension ContainerController: UITableViewDelegate {
|
|
|
|
public func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
|
|
if let tableAdapterView = scrollView as? TableAdapterView {
|
|
return tableAdapterView.tableView(tableView, heightForRowAt: indexPath)
|
|
}
|
|
return 0
|
|
}
|
|
|
|
public func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
|
|
if let tableAdapterView = scrollView as? TableAdapterView {
|
|
return tableAdapterView.tableView(tableView, didSelectRowAt: indexPath)
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
// MARK: - Table DataSource
|
|
|
|
extension ContainerController: UITableViewDataSource {
|
|
|
|
public func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
|
|
if let tableAdapterView = scrollView as? TableAdapterView {
|
|
return tableAdapterView.tableView(tableView, numberOfRowsInSection: section)
|
|
}
|
|
return 0
|
|
}
|
|
|
|
public func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
|
|
if let tableAdapterView = scrollView as? TableAdapterView {
|
|
return tableAdapterView.tableView(tableView, cellForRowAt: indexPath)
|
|
}
|
|
return UITableViewCell()
|
|
}
|
|
|
|
}
|
|
|
|
// MARK: - Collection Delegate
|
|
|
|
extension ContainerController: UICollectionViewDelegate {
|
|
|
|
public func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
|
|
if let collectionAdapterView = scrollView as? CollectionAdapterView {
|
|
collectionAdapterView.collectionView(collectionView, didSelectItemAt: indexPath)
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
// MARK: - Collection DataSource
|
|
|
|
extension ContainerController: UICollectionViewDataSource {
|
|
|
|
public func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
|
|
if let collectionAdapterView = scrollView as? CollectionAdapterView {
|
|
return collectionAdapterView.collectionView(collectionView, numberOfItemsInSection: section)
|
|
}
|
|
return 0
|
|
}
|
|
|
|
public func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
|
|
if let collectionAdapterView = scrollView as? CollectionAdapterView {
|
|
return collectionAdapterView.collectionView(collectionView, cellForItemAt: indexPath)
|
|
}
|
|
return UICollectionViewCell()
|
|
}
|
|
}
|
|
|
|
// MARK: - Collection DelegateFlowLayout
|
|
|
|
extension ContainerController: UICollectionViewDelegateFlowLayout {
|
|
|
|
public func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
|
|
if let collectionAdapterView = scrollView as? CollectionAdapterView {
|
|
return collectionAdapterView.collectionView(collectionView, layout: collectionViewLayout, sizeForItemAt: indexPath)
|
|
}
|
|
return CGSize.zero
|
|
}
|
|
}
|
|
|
|
// MARK: - Scroll Delegate
|
|
|
|
extension ContainerController: UIScrollViewDelegate {
|
|
|
|
public func scrollViewDidScroll(_ scrollView: UIScrollView) {
|
|
|
|
if let tableAdapterView = scrollView as? TableAdapterView {
|
|
tableAdapterView.scrollViewDidScroll(tableAdapterView)
|
|
}
|
|
|
|
let gesture: UIPanGestureRecognizer = scrollView.panGestureRecognizer
|
|
|
|
let inViewVelocityY: CGFloat = gesture.velocity(in: controller?.view).y
|
|
let inViewTranslationY: CGFloat = gesture.translation(in: controller?.view).y
|
|
|
|
if gesture.state != .possible, scrollView.contentOffset.y <= 0 {
|
|
scrollView.showsVerticalScrollIndicator = false
|
|
scrollView.contentOffset = CGPoint(x: scrollView.contentOffset.x, y: 0)
|
|
} else {
|
|
scrollView.showsVerticalScrollIndicator = true
|
|
}
|
|
|
|
if scrollView.contentOffset.y == 0, 0 < inViewVelocityY {
|
|
scrollBordersRunContainer = true
|
|
} else {
|
|
scrollBordersRunContainer = false
|
|
}
|
|
|
|
scrollTransform = view.transform
|
|
|
|
let top: CGFloat = positionTop
|
|
|
|
if gesture.state == .ended {
|
|
scrollOnceBeginDragging = false
|
|
}
|
|
|
|
if scrollBordersRunContainer {
|
|
|
|
view.layer.removeAllAnimations()
|
|
|
|
scrollOnceEnded = false
|
|
scrollOnceBeginDragging = false
|
|
|
|
scrollTransform.ty = ((top - scrollStartPosition) + inViewTranslationY)
|
|
|
|
if scrollTransform.ty < top {
|
|
scrollTransform.ty = top
|
|
}
|
|
|
|
if scrollBegin {
|
|
|
|
animationSpring(duration: 0.325) { [weak self] in
|
|
guard let _self = self else { return }
|
|
_self.changeView(transform: _self.scrollTransform)
|
|
}
|
|
|
|
scrollBegin = false
|
|
|
|
} else {
|
|
changeView(transform: scrollTransform)
|
|
}
|
|
|
|
let position = scrollTransform.ty
|
|
let type: ContainerMoveType = .top
|
|
let from: ContainerFromType = .scrollBorder
|
|
let animation = false
|
|
|
|
shadowLevelAlpha(position: position, animation: false)
|
|
changeFooterView(position: position)
|
|
calculationScrollViewHeight(from: from)
|
|
changeMove(position: position, type: type, animation: animation)
|
|
|
|
if gesture.state == .ended {
|
|
move(type: moveType, animation: true, velocity: inViewVelocityY, from: from)
|
|
}
|
|
|
|
} else {
|
|
|
|
if top == scrollTransform.ty, !scrollOnceBeginDragging {
|
|
scrollOnceBeginDragging = true
|
|
}
|
|
|
|
if top < scrollTransform.ty {
|
|
|
|
if inViewVelocityY < 0.0 {
|
|
|
|
if moveType == .top {
|
|
scrollView.contentOffset = CGPoint(x: scrollView.contentOffset.x, y: 0)
|
|
}
|
|
|
|
scrollTransform = view.transform
|
|
scrollTransform.ty = (top - scrollStartPosition) + inViewTranslationY
|
|
|
|
if scrollTransform.ty < top {
|
|
scrollTransform.ty = top
|
|
}
|
|
|
|
let position = scrollTransform.ty
|
|
let type: ContainerMoveType = .top
|
|
let from: ContainerFromType = .scroll
|
|
let animation = false
|
|
|
|
changeView(transform: scrollTransform)
|
|
shadowLevelAlpha(position: position, animation: false)
|
|
changeFooterView(position: position)
|
|
calculationScrollViewHeight(from: from)
|
|
changeMove(position: position, type: type, animation: animation)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - Scroll Begin/End Dragging
|
|
|
|
public func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
|
|
|
|
isScrolling = true
|
|
|
|
scrollStartPosition = scrollView.contentOffset.y
|
|
|
|
scrollBegin = true
|
|
|
|
if scrollStartPosition < 0 {
|
|
scrollStartPosition = 0.0
|
|
}
|
|
}
|
|
|
|
public func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
|
|
isScrolling = false
|
|
}
|
|
|
|
public func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
|
|
|
|
if !decelerate {
|
|
isScrolling = false
|
|
}
|
|
|
|
let gesture: UIPanGestureRecognizer = scrollView.panGestureRecognizer
|
|
|
|
let inViewVelocityY: CGFloat = gesture.velocity(in: controller?.view).y
|
|
|
|
if !scrollOnceEnded {
|
|
scrollOnceEnded = true
|
|
|
|
let type = calculatePositionTypeFrom(velocity: inViewVelocityY)
|
|
|
|
move(type: type, velocity: inViewVelocityY)
|
|
}
|
|
}
|
|
|
|
}
|