0

Can anybody tell me how to implement a stretchy UITableView footer.

I want my image inside my footer to stretch/increase size when the user 'overscrolls'. (If the user are scrolling down when the UITableView is already at the bottom)

Current code:

private let tableFooterHeight: CGFloat = 300
private var footerCustomView = UIImageView()

    func setupFooterView(){
         // Footer view
         self.footerCustomView = UIImageView(frame: CGRect.zero)
         self.footerCustomView.backgroundColor = UIColor.brown
         self.footerCustomView.image = UIImage(named: "image")

         self.view.addSubview(self.footerCustomView)
//        self.view.bringSubview(toFront: self.tableView)

         self.tableView.contentInset = UIEdgeInsets(top: 0, left: 0, bottom: self.tableFooterHeight, right: 0)

         self.footerCustomView.translatesAutoresizingMaskIntoConstraints = false

         let leadingConstraint = NSLayoutConstraint(item: self.footerCustomView,
                                               attribute: .leading,
                                               relatedBy: .equal,
                                               toItem: self.tableView,
                                               attribute: .leading,
                                               multiplier: 1,
                                               constant: 0)

         let trailingConstraint = NSLayoutConstraint(item: self.footerCustomView,
                                           attribute: .trailing,
                                           relatedBy: .equal,
                                           toItem: self.tableView,
                                           attribute: .trailing,
                                           multiplier: 1,
                                           constant: 0)

        let bottomConstraint = NSLayoutConstraint(item: self.footerCustomView,
                                           attribute: .bottom,
                                           relatedBy: .equal,
                                           toItem: self.tableView,
                                           attribute: .bottom,
                                           multiplier: 1,
                                           constant: 0)

        let heightConstraint = NSLayoutConstraint(item: self.footerCustomView,
                                              attribute: .height,
                                              relatedBy: .equal,
                                              toItem: nil,
                                              attribute: .height,
                                              multiplier: 1,
                                              constant: self.tableFooterHeight)

        self.view.addConstraints([bottomConstraint, trailingConstraint, heightConstraint, leadingConstraint])
}

func scrollViewDidScroll(_ scrollView: UIScrollView) {

    self.updateFooterView()
}

func updateFooterView() {        
    if self.tableView.bounds.origin.y > 1559 { // 1559 is end of tableView
        let diff = self.tableView.contentOffset.y - 1559
        self.footerCustomView.frame.size.height = self.tableFooterHeight + diff
    }
}

Thank you!

Normal: Normal


Stretch mode: Stretch mode

PAK
  • 431
  • 4
  • 17
  • You should add the bottom contentInset to table view and add an image view at the bottom of table view's super view. Make sure it is added to behind the table view. And in the scroll view delegate, scrollViewDidScroll, adjust the image view frame with overscroll amount. – HMHero Apr 06 '17 at 16:56
  • Thank you for your input! I updated my code in the question above. The tableFooterView is growing downwards. I tried to add a bottom constraint between the tableviews bottom and the footerCustomView, but without luck. Also when I call self.view.bringSubview(toFront: self.tableView) my customView isn't visible, and covered by my last cells backgroundColor. Do you have an idea, and did I implement the code as you described? – PAK Apr 06 '17 at 18:43

2 Answers2

0

A UITableView already has a property tableFooterView

We can subclass UIView with a UIImageView, a UIScrollView, and another UIView to serve as a container for the UIImageView. Let's call it StretchyFooterView.

I like to use a library called PureLayout for adding constraints.

class YourView: UIView, UITableViewDelegate {

fileprivate let tableView = UITableView(frame: .zero, style: .plain)

fileprivate let image = UIImage(named: "yourImage")!

// ----------------------------------------------------------------------------
// MARK: Init
// ----------------------------------------------------------------------------

override init(frame: CGRect) {
    super.init(frame: frame)

    setupViews()
}

required init?(coder aDecoder: NSCoder) {
    fatalError("init(coder:) has not been implemented")
}

// ----------------------------------------------------------------------------
// MARK: UIView
// ----------------------------------------------------------------------------

override func updateConstraints() {
    tableView.autoPinEdgesToSuperviewEdges()

    super.updateConstraints()
}

override func layoutSubviews() {
    super.layoutSubviews()

    // do this after the view lays out so we know the frame width and height
    let imageAspect = image.size.width / image.size.height
    let footerWidth = frame.size.width
    let footerHeight = footerWidth / imageAspect
    let footerSize = CGSize(width: footerWidth, height: footerHeight)
    let footerFrame = CGRect(origin: .zero, size: footerSize)

    tableView.tableFooterView = StretchyFooterView(frame: footerFrame, image: image)
}

// ----------------------------------------------------------------------------
// MARK: Internal
// ----------------------------------------------------------------------------

func scrollViewDidScroll(_ scrollView: UIScrollView) {
    let verticalScrollDistance = scrollView.contentOffset.y
    let bottomOfView = frame.height

    if let stretchyFooterView = tableView.tableFooterView as? StretchyFooterView {
        stretchyFooterView.stretchBy(verticalScrollDistance, bottomBound: bottomOfView)
    }
}

// ----------------------------------------------------------------------------
// MARK: Private
// ----------------------------------------------------------------------------

fileprivate func setupViews() {
    tableView.delegate = self

    addSubview(tableView)
}
}

class StretchyFooterView: UIView {

fileprivate let containerView = UIView()
fileprivate let scrollView = UIScrollView()
fileprivate let imageView = UIImageView()

fileprivate let image: UIImage

// ----------------------------------------------------------------------------
// MARK: Init
// ----------------------------------------------------------------------------

init(frame: CGRect, image: UIImage) {
    self.image = image

    super.init(frame: frame)

    setupViews()
}

required init?(coder aDecoder: NSCoder) {
    fatalError("init(coder:) has not been implemented")
}

// ----------------------------------------------------------------------------
// MARK: UIView
// ----------------------------------------------------------------------------

override func updateConstraints() {
    containerView.autoCenterInSuperview()
    containerView.autoMatch(.height, to: .height, of: scrollView)

    imageView.autoPinEdgesToSuperviewEdges()
    imageView.autoMatch(.width, to: .height, of: scrollView, withMultiplier: image.aspect)

    super.updateConstraints()
}

// ----------------------------------------------------------------------------
// MARK: Internal
// ----------------------------------------------------------------------------

func stretchBy(_ distanceScrolled: CGFloat, bottomBound: CGFloat) {
    let distanceFromTop = bottomBound + distanceScrolled // the footer could be offscreen, so we need to add the distance scrolled to the bottom of the view
    let distanceFromBottom = frame.maxY - distanceFromTop // this is how much we need to scroll

    if distanceFromBottom < 0 { // once we hit zero and below, we need to stretch the height
        let origin = CGPoint.zero // since we are scrolling, the origin needs to stay at zero
        let newSize = CGSize(width: frame.width, height: frame.height - distanceFromBottom) // subtract distanceFromBottom because it's negative

        scrollView.frame = CGRect(origin: origin, size: newSize)
    }
}

// ----------------------------------------------------------------------------
// MARK: Private
// ----------------------------------------------------------------------------

fileprivate func setupViews() {
    imageView.image = image

    containerView.addSubview(imageView)

    scrollView.frame = bounds
    scrollView.addSubview(containerView)

    addSubview(scrollView)
}
}
Eric Armstrong
  • 646
  • 6
  • 17
0

Simple variant if you use Storyboard:

  1. Add empty view as tableView footer.
  2. Connect an outlet to tableView delegate.
  3. Implement scrollViewDidScroll(_ scrollView: UIScrollView):

    func scrollViewDidScroll(_ scrollView: UIScrollView) { guard scrollView == mainTableView else { return } updateTableFooterFrame() }

    func updateTableFooterFrame() { let distanceFromTop = mainTableView.frame.height + mainTableView.contentOffset.y var distanceScrolled = distanceFromTop - mainTableView.contentSize.height distanceScrolled = distanceScrolled > 0 ? distanceScrolled : 0 let newFooterFrame = CGRect( x: 0, y: mainTableView.contentSize.height, width: mainTableView.frame.width, height: distanceScrolled ) tableFooterView.frame = newFooterFrame }

  • When possible, please make an effort to provide additional explanation instead of just code. Such answers tend to be more useful as they help members of the community and especially new developers better understand the reasoning of the solution, and can help prevent the need to address follow-up questions. – Rajan May 12 '20 at 10:52