// // ThreeLayerTableViewController.swift // MVMCoreUI // // Created by Scott Pfeil on 4/18/19. // Copyright © 2019 Verizon Wireless. All rights reserved. // import UIKit open class ThreeLayerTableViewController: ProgrammaticTableViewController, RotorViewElementsProtocol { //-------------------------------------------------- // MARK: - Main Views //-------------------------------------------------- private var headerView: UIView? private var footerView: UIView? public var topView: UIView? public var middleView: UIView? { get { tableView } set { } } public var bottomView: 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.visibleCells.forEach { cell in (cell as? MVMCoreViewProtocol)?.updateView(width) } } open override func updateUI(for molecules: [MoleculeModelProtocol]? = nil) { super.updateUI(for: molecules) guard molecules == nil else { return } createViewForTableHeader() createViewForTableFooter() // Reloading the table is handled in updateViews, however, update views is on a separate rendering task than the current thread. The table render needs to be bound and settled to the new model before others put in additional update requests. tableView.reloadData() } 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.backgroundColor = bottomView.backgroundColor 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 //force footerView to redraw footerView.setNeedsLayout() footerView.layoutIfNeeded() // 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() } }