301 lines
13 KiB
Swift
301 lines
13 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: ScrollingViewController, UICollectionViewDataSource, UICollectionViewDelegate, UICollectionViewDelegateFlowLayout {
|
|
public var collectionView: UICollectionView?
|
|
|
|
// The three main views
|
|
private var topView: UIView?
|
|
private var bottomView: UIView?
|
|
private var headerView: ContainerCollectionReusableView?
|
|
private var footerView: ContainerCollectionReusableView?
|
|
var useMargins: Bool = true
|
|
private let headerID = "header"
|
|
private let footerID = "footer"
|
|
|
|
//MARK: - MFViewController
|
|
open override func updateViews() {
|
|
super.updateViews()
|
|
let width = view.bounds.width
|
|
if let topView = topView as? MVMCoreViewProtocol {
|
|
topView.updateView(width)
|
|
// showHeader(width)
|
|
}
|
|
if let bottomView = bottomView as? MVMCoreViewProtocol {
|
|
bottomView.updateView(width)
|
|
//showFooter(width)
|
|
}
|
|
self.collectionView?.collectionViewLayout.invalidateLayout()
|
|
}
|
|
|
|
open override func handleNewData() {
|
|
super.handleNewData()
|
|
createViewForTableHeader()
|
|
createViewForTableFooter()
|
|
collectionView?.reloadData()
|
|
}
|
|
|
|
override open func viewDidLoad() {
|
|
let collection = createCollectionView()
|
|
collectionView = collection
|
|
view.addSubview(collection)
|
|
NSLayoutConstraint.constraintPinSubview(toSuperview: collection)
|
|
scrollView = collectionView
|
|
|
|
registerCells()
|
|
|
|
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 table sections, nil to fill. 0 default
|
|
open func spaceBelowTopView() -> CGFloat? {
|
|
return 0
|
|
}
|
|
|
|
/// Space between the bottom view and the table sections, 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
|
|
}
|
|
|
|
open override func updateViewConstraints() {
|
|
super.updateViewConstraints()
|
|
|
|
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, 0.1) {
|
|
if fillTop && fillBottom {
|
|
// space both
|
|
let half = newSpace / 2
|
|
headerView?.bottomConstraint?.constant = half
|
|
footerView?.topConstraint?.constant = half
|
|
collectionView?.invalidateIntrinsicContentSize()
|
|
} 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?.invalidateIntrinsicContentSize()
|
|
} else if fillBottom {
|
|
// Only bottom is spaced.
|
|
print("newSpace \(newSpace)")
|
|
footerView?.topConstraint?.constant = newSpace
|
|
collectionView?.invalidateIntrinsicContentSize()
|
|
}
|
|
}
|
|
}
|
|
|
|
//MARK: - Header Footer
|
|
/// Gets the top view and adds it to a spacing view, headerView, and then calls showHeader.
|
|
open func createViewForTableHeader() {
|
|
guard let topView = viewForTop() else {
|
|
self.topView = nil
|
|
self.headerView = nil
|
|
return
|
|
}
|
|
self.topView = topView
|
|
}
|
|
|
|
/// Gets the bottom view and adds it to a spacing view, footerView, and then calls showFooter.
|
|
open func createViewForTableFooter() {
|
|
guard let bottomView = viewForBottom() else {
|
|
self.bottomView = nil
|
|
self.footerView = nil
|
|
return
|
|
}
|
|
self.bottomView = bottomView
|
|
}
|
|
|
|
// /// Takes the current headerView and adds it to the tableHeaderView
|
|
// func showHeader(_ sizingWidth: CGFloat?) {
|
|
// headerView?.removeFromSuperview()
|
|
// tableView?.tableHeaderView = nil
|
|
// guard let headerView = headerView else {
|
|
// return
|
|
// }
|
|
//
|
|
// // This extra view is needed because of the wonkiness of apple's table header. Things breaks if using autolayout.
|
|
// headerView.setNeedsLayout()
|
|
// headerView.layoutIfNeeded()
|
|
// MVMCoreUIUtility.sizeView(toFit: headerView)
|
|
// let tableHeaderView = UIView(frame: CGRect(x: 0, y: 0, width: MVMCoreUIUtility.getWidth(), height: headerView.frame.height))
|
|
// tableHeaderView.addSubview(headerView)
|
|
// NSLayoutConstraint.constraintPinSubview(toSuperview: headerView)
|
|
// tableView?.tableHeaderView = tableHeaderView
|
|
// }
|
|
//
|
|
// /// Takes the current footerView and adds it to the tableFooterView
|
|
// func showFooter(_ sizingWidth: CGFloat?) {
|
|
// footerView?.removeFromSuperview()
|
|
// safeAreaView?.removeFromSuperview()
|
|
// guard let footerView = footerView, let tableView = tableView else {
|
|
// return
|
|
// }
|
|
//
|
|
// if bottomViewOutsideOfScrollArea {
|
|
// // put bottom view outside of scrolling area.
|
|
// bottomConstraint?.isActive = false
|
|
// view.addSubview(footerView)
|
|
// footerView.topAnchor.constraint(equalTo: tableView.bottomAnchor).isActive = true
|
|
// footerView.leftAnchor.constraint(equalTo: view.leftAnchor).isActive = true
|
|
// view.rightAnchor.constraint(equalTo: footerView.rightAnchor).isActive = true
|
|
// view.safeAreaLayoutGuide.bottomAnchor.constraint(equalTo: footerView.bottomAnchor).isActive = true
|
|
// safeAreaView = MVMCoreUICommonViewsUtility.getAndSetupSafeAreaView(on: view)
|
|
// safeAreaView?.backgroundColor = bottomView?.backgroundColor
|
|
// } else {
|
|
// bottomConstraint?.isActive = true
|
|
// var y: CGFloat?
|
|
// if let tableFooterView = tableView.tableFooterView {
|
|
// // if footer already exists, use the same y location to avoid strange moving animation
|
|
// y = tableFooterView.frame.minY
|
|
// }
|
|
//
|
|
// // This extra view is needed because of the wonkiness of apple's table footer. Things breaks if using autolayout.
|
|
// MVMCoreUIUtility.sizeView(toFit: footerView)
|
|
// let tableFooterView = UIView(frame: CGRect(x: 0, y: y ?? 0, width: MVMCoreUIUtility.getWidth(), height: footerView.frame.height))
|
|
// tableFooterView.addSubview(footerView)
|
|
// NSLayoutConstraint.constraintPinSubview(toSuperview: footerView)
|
|
// tableView.tableFooterView = tableFooterView
|
|
// }
|
|
// }
|
|
|
|
//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
|
|
}
|
|
|
|
open func createCollectionViewLayout() -> UICollectionViewLayout {
|
|
let layout = UICollectionViewFlowLayout()
|
|
layout.scrollDirection = .vertical
|
|
layout.minimumLineSpacing = 0
|
|
layout.minimumInteritemSpacing = 0
|
|
layout.estimatedItemSize = UICollectionViewFlowLayout.automaticSize
|
|
return layout
|
|
}
|
|
|
|
open func createCollectionView() -> UICollectionView {
|
|
let collection = UICollectionView(frame: .zero, collectionViewLayout: createCollectionViewLayout())
|
|
collection.translatesAutoresizingMaskIntoConstraints = false
|
|
collection.dataSource = self
|
|
collection.delegate = self
|
|
collection.showsHorizontalScrollIndicator = false
|
|
collection.backgroundColor = .white
|
|
collection.isAccessibilityElement = false
|
|
collection.contentInsetAdjustmentBehavior = .always
|
|
return collection
|
|
}
|
|
|
|
deinit {
|
|
collectionView?.delegate = nil
|
|
collectionView?.dataSource = nil
|
|
}
|
|
|
|
//MARK: - Collection
|
|
|
|
open func registerCells() {
|
|
collectionView?.register(ContainerCollectionReusableView.self, forSupplementaryViewOfKind: UICollectionView.elementKindSectionHeader, withReuseIdentifier: headerID)
|
|
collectionView?.register(ContainerCollectionReusableView.self, forSupplementaryViewOfKind: UICollectionView.elementKindSectionFooter, withReuseIdentifier: footerID)
|
|
}
|
|
|
|
open func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
|
|
return 0
|
|
}
|
|
|
|
open func numberOfSections(in collectionView: UICollectionView) -> Int {
|
|
return 1
|
|
}
|
|
|
|
open func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
|
|
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "collectionItem", for: indexPath) as! MoleculeCollectionViewCell
|
|
let labelModel = LabelModel(text: "hello")
|
|
let model = MoleculeCollectionItemModel(with: labelModel)
|
|
cell.set(with: model, delegateObjectIVar, nil)
|
|
return cell
|
|
}
|
|
|
|
open func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, referenceSizeForHeaderInSection section: Int) -> CGSize {
|
|
guard let view = headerView ?? topView,
|
|
section == 0 else { return .zero }
|
|
|
|
// Use this view to calculate the optimal size based on the collection view's width
|
|
return view.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 view = footerView ?? bottomView,
|
|
section == numberOfSections(in: collectionView) - 1 else { return .zero }
|
|
|
|
// Use this view to calculate the optimal size based on the collection view's width
|
|
let size = view.systemLayoutSizeFitting(CGSize(width: collectionView.frame.width, height: UIView.layoutFittingExpandedSize.height),
|
|
withHorizontalFittingPriority: .required, // Width is fixed
|
|
verticalFittingPriority: .fittingSizeLevel) // Height can be as large as needed
|
|
print("SIZEEE \(size.height) \(String(describing: footerView?.topConstraint?.constant))")
|
|
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()
|
|
}
|
|
}
|