195 lines
9.3 KiB
Swift
195 lines
9.3 KiB
Swift
//
|
|
// ThreeLayerCollectionViewController.swift
|
|
// MVMCoreUI
|
|
//
|
|
// Created by Scott Pfeil on 4/6/20.
|
|
// Copyright © 2020 Verizon Wireless. All rights reserved.
|
|
//
|
|
|
|
import Foundation
|
|
|
|
/// A view controller that has three main layers, a header, collection rows, and a footer. The header is added as a supplement header to the first section, and the footer is added as a supplement footer to the last section. This view controller allows for flexible space between the three layers to fit the screeen.
|
|
@objc open class ThreeLayerCollectionViewController: ProgrammaticCollectionViewController, UICollectionViewDelegateFlowLayout {
|
|
|
|
private var topView: UIView?
|
|
private var bottomView: UIView?
|
|
private var headerView: ContainerCollectionReusableView?
|
|
private var footerView: ContainerCollectionReusableView?
|
|
private let headerID = "header"
|
|
private let footerID = "footer"
|
|
public var bottomViewOutsideOfScrollArea: Bool = false
|
|
public var topViewOutsideOfScrollArea: Bool = false
|
|
|
|
/// 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 !MVMCoreGetterUtility.cgfequalwiththreshold(newSpace, currentSpace, 1) {
|
|
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: - ViewController
|
|
open override func updateViews() {
|
|
super.updateViews()
|
|
// Needed due to dispatch in reloadCollectionData.
|
|
DispatchQueue.main.async {
|
|
let width = self.view.bounds.width
|
|
if let topView = self.topView as? MVMCoreViewProtocol {
|
|
topView.updateView(width)
|
|
}
|
|
if let bottomView = self.bottomView as? MVMCoreViewProtocol {
|
|
bottomView.updateView(width)
|
|
}
|
|
if let cells = self.collectionView?.visibleCells {
|
|
for cell in cells {
|
|
self.update(cell: cell, size: width)
|
|
}
|
|
}
|
|
self.invalidateCollectionLayout()
|
|
}
|
|
}
|
|
|
|
open override func handleNewData() {
|
|
super.handleNewData()
|
|
topView = viewForTop()
|
|
bottomView = viewForBottom()
|
|
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. 0 default
|
|
open func minimumFillSpace() -> CGFloat {
|
|
return 0
|
|
}
|
|
|
|
//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()
|
|
// TODO: Improve this workaround (autolayout for cells happens async so contentSize isn't accurate immediately)
|
|
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()
|
|
// TODO: Improve this workaround (autolayout for cells happens async so contentSize isn't accurate immediately)
|
|
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.1, execute: {
|
|
self.updateFlexibleSpace()
|
|
})
|
|
}
|
|
|
|
/// Called in updateView, updates the cell.
|
|
open func update(cell: UICollectionViewCell, size: CGFloat) {
|
|
(cell as? MVMCoreViewProtocol)?.updateView(size)
|
|
}
|
|
|
|
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 section == 0 else { return .zero }
|
|
// Calculate the height of the header since apple doesn't support autolayout. Width is fixed, height is tall as content.
|
|
let header = headerView ?? self.collectionView(collectionView, viewForSupplementaryElementOfKind: UICollectionView.elementKindSectionHeader, at: IndexPath(row: 0, section: section))
|
|
return header.systemLayoutSizeFitting(CGSize(width: collectionView.frame.width, height: UIView.layoutFittingExpandedSize.height), withHorizontalFittingPriority: .required, verticalFittingPriority: .fittingSizeLevel)
|
|
}
|
|
|
|
open func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, referenceSizeForFooterInSection section: Int) -> CGSize {
|
|
guard section == numberOfSections(in: collectionView) - 1 else { return .zero }
|
|
// Calculate the height of the footr since apple doesn't support autolayout. Width is fixed, height is tall as content.
|
|
let footer = footerView ?? self.collectionView(collectionView, viewForSupplementaryElementOfKind: UICollectionView.elementKindSectionFooter, at: IndexPath(row: 0, section: section))
|
|
return footer.systemLayoutSizeFitting(CGSize(width: collectionView.frame.width, height: UIView.layoutFittingExpandedSize.height), withHorizontalFittingPriority: .required, verticalFittingPriority: .fittingSizeLevel)
|
|
}
|
|
|
|
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 {
|
|
let bottomView = self.bottomView ?? MVMCoreUICommonViewsUtility.getView(with: 0)
|
|
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 {
|
|
let topView = self.topView ?? MVMCoreUICommonViewsUtility.getView(with: 0)
|
|
headerView.addAndContain(view: topView)
|
|
headerView.bottomConstraint?.constant = spaceBelowTopView() ?? 0
|
|
self.headerView = headerView
|
|
return headerView
|
|
}
|
|
fatalError()
|
|
}
|
|
}
|