mvm_core_ui/MVMCoreUI/BaseControllers/ThreeLayerTableViewController.swift
2022-07-06 11:25:45 -04:00

268 lines
11 KiB
Swift

//
// ThreeLayerTableViewController.swift
// MVMCoreUI
//
// Created by Scott Pfeil on 4/18/19.
// Copyright © 2019 Verizon Wireless. All rights reserved.
//
import UIKit
open class ThreeLayerTableViewController: ProgrammaticTableViewController {
//--------------------------------------------------
// MARK: - Main Views
//--------------------------------------------------
private var topView: UIView?
private var bottomView: UIView?
private var headerView: UIView?
private var footerView: UIView?
//--------------------------------------------------
// MARK: - Properties
//--------------------------------------------------
public var bottomViewOutsideOfScrollArea: Bool = false
public var topViewOutsideOfScrollArea: Bool = false
//--------------------------------------------------
// MARK: - Constraints
//--------------------------------------------------
private var topViewBottomConstraint: NSLayoutConstraint?
private var bottomViewTopConstraint: NSLayoutConstraint?
//--------------------------------------------------
// 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)
}
tableView.reloadData()
}
open override func handleNewData() {
super.handleNewData()
createViewForTableHeader()
createViewForTableFooter()
tableView?.reloadData()
accessibilityElements = [tableView as Any]
}
override open func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
setNoSectionHeadersFooters()
// Ensures the footer and headers are the right size
tableView.frameChangeAction = { [weak self] in
self?.view.setNeedsUpdateConstraints()
}
}
//--------------------------------------------------
// 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? { 0 }
/// Space between the bottom view and the table sections, nil to fill. nil default
open func spaceAboveBottomView() -> CGFloat? { nil }
/// can override to return a minimum fill space.
open func minimumFillSpace() -> CGFloat { 0 }
open override func updateViewConstraints() {
guard let tableView = tableView else {
super.updateViewConstraints()
return
}
let minimumSpace: CGFloat = minimumFillSpace()
var currentSpace: CGFloat = 0
var totalMinimumSpace: CGFloat = 0
var fillTop = false
if spaceBelowTopView() == nil, tableView.tableHeaderView != nil {
fillTop = true
currentSpace += topViewBottomConstraint?.constant ?? 0
totalMinimumSpace += minimumSpace
}
var fillBottom = false
if spaceAboveBottomView() == nil, tableView.tableFooterView != nil {
fillBottom = true
currentSpace += bottomViewTopConstraint?.constant ?? 0
totalMinimumSpace += minimumSpace
}
guard fillTop || fillBottom else {
super.updateViewConstraints()
return
}
let newSpace = MVMCoreUIUtility.getVariableConstraintHeight(currentSpace, in: tableView, minimumHeight: totalMinimumSpace)
let width = view.bounds.width
if !MVMCoreGetterUtility.cgfequalwiththreshold(newSpace, currentSpace, 0.1) {
if fillTop && fillBottom {
// space both
let half = newSpace / 2
topViewBottomConstraint?.constant = half
bottomViewTopConstraint?.constant = half
showHeader(width)
showFooter(width)
} else if fillTop {
topViewBottomConstraint?.constant = newSpace
showHeader(width)
} else if fillBottom {
// Only bottom is spaced.
bottomViewTopConstraint?.constant = newSpace
showFooter(width)
}
refreshTable()
}
super.updateViewConstraints()
}
//--------------------------------------------------
// MARK: - Header Footer
//--------------------------------------------------
/// Gets the top view and adds it to a spacing view, headerView, and then calls showHeader.
open func createViewForTableHeader() {
var topView = viewForTop()
self.topView = topView
// If top view is outside of scroll area, create a dummy view for the header. Small height is needed to stop apple from adding padding for grouped tables when no header.
if topViewOutsideOfScrollArea {
topView = MVMCoreUICommonViewsUtility.getView(with: 0.5)
}
let headerView = MVMCoreUICommonViewsUtility.commonView()
headerView.addSubview(topView)
topView.topAnchor.constraint(equalTo: headerView.topAnchor).isActive = true
topView.leftAnchor.constraint(equalTo: headerView.leftAnchor).isActive = true
headerView.rightAnchor.constraint(equalTo: topView.rightAnchor).isActive = true
topViewBottomConstraint = headerView.bottomAnchor.constraint(equalTo: topView.bottomAnchor, constant: spaceBelowTopView() ?? 0)
topViewBottomConstraint?.isActive = true
self.headerView = headerView
showHeader(nil)
}
/// Gets the bottom view and adds it to a spacing view, footerView, and then calls showFooter.
open func createViewForTableFooter() {
var bottomView = viewForBottom()
self.bottomView = bottomView
// If bottom view is outside of scroll area, create a dummy view for the header. Small height is needed to stop apple from adding padding for grouped tables when no header.
if bottomViewOutsideOfScrollArea {
bottomView = MVMCoreUICommonViewsUtility.getView(with: 0.5)
}
let footerView = MVMCoreUICommonViewsUtility.commonView()
footerView.addSubview(bottomView)
bottomViewTopConstraint = bottomView.topAnchor.constraint(equalTo: footerView.topAnchor, constant: spaceAboveBottomView() ?? 0)
bottomViewTopConstraint?.isActive = true
bottomView.leftAnchor.constraint(equalTo: footerView.leftAnchor).isActive = true
footerView.rightAnchor.constraint(equalTo: bottomView.rightAnchor).isActive = true
footerView.bottomAnchor.constraint(equalTo: bottomView.bottomAnchor).isActive = true
self.footerView = footerView
showFooter(nil)
}
/// 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 }
if topViewOutsideOfScrollArea,
let topView = topView {
// put top view outside of scrolling area.
topConstraint?.isActive = false
view.addSubview(topView)
topView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor).isActive = true
topView.leftAnchor.constraint(equalTo: view.leftAnchor).isActive = true
view.rightAnchor.constraint(equalTo: topView.rightAnchor).isActive = true
tableView.topAnchor.constraint(equalTo: topView.bottomAnchor).isActive = true
} else {
topConstraint?.isActive = true
}
// 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()
guard let footerView = footerView,
let tableView = tableView else {
self.tableView?.tableFooterView = nil
return
}
if bottomViewOutsideOfScrollArea,
let bottomView = bottomView {
// put bottom view outside of scrolling area.
bottomConstraint?.isActive = false
view.addSubview(bottomView)
bottomView.topAnchor.constraint(equalTo: tableView.bottomAnchor).isActive = true
bottomView.leftAnchor.constraint(equalTo: view.leftAnchor).isActive = true
view.rightAnchor.constraint(equalTo: bottomView.rightAnchor).isActive = true
view.safeAreaLayoutGuide.bottomAnchor.constraint(equalTo: bottomView.bottomAnchor).isActive = true
} else {
bottomConstraint?.isActive = true
}
// if footer already exists, use the same y location to avoid strange moving animation
let y = tableView.tableFooterView?.frame.minY ?? 0.0
// 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, 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 {
// Small height is needed to stop apple from adding padding for grouped tables when no header.
MVMCoreUICommonViewsUtility.getView(with: 0.5)
}
/// Subclass for a bottom view.
open func viewForBottom() -> UIView {
// Default spacing is standard when no buttons.
MVMCoreUICommonViewsUtility.getView(with: PaddingDefaultVerticalSpacing)
}
deinit {
tableView?.delegate = nil
}
// Ensures the footer and headers are the right size
func scrollViewDidChangeAdjustedContentInset(_ scrollView: UIScrollView) {
view.setNeedsUpdateConstraints()
}
}