mvm_core_ui/MVMCoreUI/BaseControllers/ThreeLayerCollectionViewController.swift
2020-04-08 16:14:40 -04:00

214 lines
9.1 KiB
Swift

//
// ThreeLayerCollectionViewController.swift
// MVMCoreUI
//
// Created by Scott Pfeil on 4/6/20.
// Copyright © 2020 Verizon Wireless. All rights reserved.
//
import Foundation
@objc open class ThreeLayerCollectionViewController: ProgrammaticCollectionViewController, UICollectionViewDelegateFlowLayout {
// The three main views
private var topView: UIView?
private var bottomView: UIView?
private var headerView: ContainerCollectionReusableView?
private var footerView: ContainerCollectionReusableView?
private let headerID = "header"
private let footerID = "footer"
/// Updates the padding for flexible space (header or footer)
private func updateFlexibleSpace() {
guard let tableView = collectionView else { return }
let minimumSpace: CGFloat = minimumFillSpace()
var currentSpace: CGFloat = 0
var totalMinimumSpace: CGFloat = 0
var fillTop = false
if spaceBelowTopView() == nil, headerView != nil {
fillTop = true
currentSpace += headerView?.bottomConstraint?.constant ?? 0
totalMinimumSpace += minimumSpace
}
var fillBottom = false
if spaceAboveBottomView() == nil, footerView != nil {
fillBottom = true
currentSpace += footerView?.topConstraint?.constant ?? 0
totalMinimumSpace += minimumSpace
}
guard fillTop || fillBottom else { return }
let newSpace = MVMCoreUIUtility.getVariableConstraintHeight(currentSpace, in: tableView, minimumHeight: totalMinimumSpace)
// If the bottom view is outside of the scroll, then only the top view constraint is being used, so we have to double it to account for the bottom constraint not being there when we compare to the new value.
var currentSpaceForCompare: CGFloat = currentSpace
if fillTop {
currentSpaceForCompare = currentSpace * 2;
}
if !MVMCoreGetterUtility.cgfequalwiththreshold(newSpace, currentSpaceForCompare, 2) {
if fillTop && fillBottom {
// space both
let half = newSpace / 2
headerView?.bottomConstraint?.constant = half
footerView?.topConstraint?.constant = half
collectionView?.collectionViewLayout.invalidateLayout()
} else if fillTop {
// Only top is spaced (half the size if the bottom view is out of the scroll because it needs to be sized as if there are two spacers but there is only one.
headerView?.bottomConstraint?.constant = newSpace
collectionView?.collectionViewLayout.invalidateLayout()
} else if fillBottom {
// Only bottom is spaced.
footerView?.topConstraint?.constant = newSpace
collectionView?.collectionViewLayout.invalidateLayout()
}
}
}
//MARK: - MFViewController
open override func updateViews() {
super.updateViews()
let width = view.bounds.width
if let topView = topView as? MVMCoreViewProtocol {
topView.updateView(width)
}
if let bottomView = bottomView as? MVMCoreViewProtocol {
bottomView.updateView(width)
}
invalidateCollectionLayout()
}
open override func handleNewData() {
super.handleNewData()
createViewForHeader()
createViewForFooter()
reloadCollectionData()
}
override open func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
}
//MARK: - Spacing
// If both are subclassed to return a value, then the buttons will not be pinned towards the bottom because neither spacing would try to fill the screen.
/// Space between the top view and the collection rows, nil to fill. 0 default
open func spaceBelowTopView() -> CGFloat? {
return 0
}
/// Space between the bottom view and the collection rows, nil to fill. nil default
open func spaceAboveBottomView() -> CGFloat? {
return nil
}
/// can override to return a minimum fill space.
open func minimumFillSpace() -> CGFloat {
return 0
}
//MARK: - Header Footer
/// Creates the top view.
open func createViewForHeader() {
guard let topView = viewForTop() else {
self.topView = nil
self.headerView = nil
return
}
self.topView = topView
}
/// Creates the footer
open func createViewForFooter() {
guard let bottomView = viewForBottom() else {
self.bottomView = nil
self.footerView = nil
return
}
self.bottomView = bottomView
}
//MARK: - Functions to subclass
/// Subclass for a top view.
open func viewForTop() -> UIView? {
return nil
}
/// Subclass for a bottom view.
open func viewForBottom() -> UIView? {
return nil
}
//MARK: - Collection
/// Should be used to refresh the layout of the collection view. Updates flexible padding.
open func invalidateCollectionLayout() {
self.collectionView?.collectionViewLayout.invalidateLayout()
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.1, execute: {
self.updateFlexibleSpace()
})
}
/// Should be used to reload the data of the collection view. Updates flexible padding.
open func reloadCollectionData() {
collectionView?.reloadData()
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.1, execute: {
self.updateFlexibleSpace()
})
}
open override func registerCells() {
super.registerCells()
collectionView?.register(ContainerCollectionReusableView.self, forSupplementaryViewOfKind: UICollectionView.elementKindSectionHeader, withReuseIdentifier: headerID)
collectionView?.register(ContainerCollectionReusableView.self, forSupplementaryViewOfKind: UICollectionView.elementKindSectionFooter, withReuseIdentifier: footerID)
}
open override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return 0
}
open func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, referenceSizeForHeaderInSection section: Int) -> CGSize {
guard let _ = topView,
section == 0 else { return .zero }
let header = headerView ?? self.collectionView(collectionView, viewForSupplementaryElementOfKind: UICollectionView.elementKindSectionHeader, at: IndexPath(row: 0, section: section))
// Use this view to calculate the optimal size based on the collection view's width
return header.systemLayoutSizeFitting(CGSize(width: collectionView.frame.width, height: UIView.layoutFittingExpandedSize.height),
withHorizontalFittingPriority: .required, // Width is fixed
verticalFittingPriority: .fittingSizeLevel) // Height can be as large as needed
}
open func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, referenceSizeForFooterInSection section: Int) -> CGSize {
guard let _ = bottomView,
section == numberOfSections(in: collectionView) - 1 else { return .zero }
let footer = footerView ?? self.collectionView(collectionView, viewForSupplementaryElementOfKind: UICollectionView.elementKindSectionFooter, at: IndexPath(row: 0, section: section))
// Use this view to calculate the optimal size based on the collection view's width
let size = footer.systemLayoutSizeFitting(CGSize(width: collectionView.frame.width, height: UIView.layoutFittingExpandedSize.height),
withHorizontalFittingPriority: .required, // Width is fixed
verticalFittingPriority: .fittingSizeLevel) // Height can be as large as needed
return size
}
open func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView {
if kind == UICollectionView.elementKindSectionFooter {
let footerView = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: footerID, for: indexPath) as! ContainerCollectionReusableView
footerView.addAndContain(view: bottomView!)
footerView.topConstraint?.constant = spaceAboveBottomView() ?? 0
self.footerView = footerView
return footerView
} else if kind == UICollectionView.elementKindSectionHeader {
let headerView = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: headerID, for: indexPath) as! ContainerCollectionReusableView
headerView.addAndContain(view: topView!)
headerView.bottomConstraint?.constant = spaceBelowTopView() ?? 0
self.headerView = headerView
return headerView
}
fatalError()
}
}