// // ThreeLayerTableViewController.swift // MVMCoreUI // // Created by Scott Pfeil on 4/18/19. // Copyright © 2019 Verizon Wireless. All rights reserved. // import UIKit import MVMAnimationFramework open class ThreeLayerTableViewController: ProgrammaticTableViewController { // The three main views private var topView: UIView? private var bottomView: UIView? private var headerView: UIView? private var footerView: UIView? var useMargins: Bool = true public var bottomViewOutsideOfScrollArea: Bool = false public var topViewOutsideOfScrollArea: Bool = false 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() } override open func viewDidLoad() { super.viewDidLoad() // Do any additional setup after loading the view. setNoSectionHeadersFooters() } //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 = tableView else { 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 { 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 && bottomViewOutsideOfScrollArea { currentSpaceForCompare = currentSpace * 2; } let width = view.bounds.width if !MVMCoreGetterUtility.cgfequalwiththreshold(newSpace, currentSpaceForCompare, 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) } } } //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. if topViewOutsideOfScrollArea { topView = MVMCoreUICommonViewsUtility.commonView() topView.heightAnchor.constraint(equalToConstant: 0.5).isActive = true } 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. if bottomViewOutsideOfScrollArea { bottomView = MVMCoreUICommonViewsUtility.commonView() bottomView.heightAnchor.constraint(equalToConstant: 0.5).isActive = true } 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 topView = topView, let headerView = headerView else { return } if topViewOutsideOfScrollArea { // 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.safeAreaLayoutGuide.leftAnchor).isActive = true view.safeAreaLayoutGuide.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 bottomView = bottomView, let footerView = footerView, let tableView = tableView else { self.tableView?.tableFooterView = nil return } if bottomViewOutsideOfScrollArea { // 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.safeAreaLayoutGuide.leftAnchor).isActive = true view.safeAreaLayoutGuide.rightAnchor.constraint(equalTo: bottomView.rightAnchor).isActive = true view.safeAreaLayoutGuide.bottomAnchor.constraint(equalTo: bottomView.bottomAnchor).isActive = true } 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 { let view = MVMCoreUICommonViewsUtility.commonView() // Small height is needed to stop apple from adding padding for grouped tables when no header. view.heightAnchor.constraint(equalToConstant: 1).isActive = true return view } /// Subclass for a bottom view. open func viewForBottom() -> UIView { // Default spacing is standard when no buttons. let view = MVMCoreUICommonViewsUtility.commonView() view.heightAnchor.constraint(equalToConstant: PaddingDefaultVerticalSpacing).isActive = true return view } deinit { tableView?.delegate = nil } }