Last active
September 7, 2020 21:45
-
-
Save zackdotcomputer/d365bfa5cd7e4a38d45c078b09459da3 to your computer and use it in GitHub Desktop.
A table view cell that shrinks to avoid the top of the table
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// | |
// ShrinkingTableCell.swift | |
// ShrinkingTableCell | |
// | |
// Created by Zack Sheppard on 9/2/20. | |
// Copyright © 2020 Zack Sheppard. All rights reserved. | |
// Available under the MIT License | |
// Available at https://gist.github.com/zackdotcomputer/d365bfa5cd7e4a38d45c078b09459da3 | |
// | |
import UIKit | |
/// The behavior this cell will have on the screen | |
struct ShrinkingTableCellBehavior { | |
let cellHeightAsRatioOfWidth: ClosedRange<Double>? | |
let cellHeightAsPointValues: ClosedRange<Double>? | |
init(confinedToRatioOfWidth: ClosedRange<Double>) { | |
cellHeightAsRatioOfWidth = confinedToRatioOfWidth | |
cellHeightAsPointValues = nil | |
} | |
init(confinedToPointSizes: ClosedRange<Double>) { | |
cellHeightAsRatioOfWidth = nil | |
cellHeightAsPointValues = confinedToPointSizes | |
} | |
static var defaultBehavior: ShrinkingTableCellBehavior { | |
return self.init(confinedToRatioOfWidth: 0.25...1) | |
} | |
} | |
/// A UITableViewCell that will shrink away from the top of the screen as you scroll. | |
/// - Note: Usage Tips: | |
/// + Subclass this class and add your cell's UI to the `shrinkingContentView` view. | |
/// + It is recommended you use Autolayout to constrain your layout based on that view's size. | |
/// + If you use Autolayout priorities higher than .defaultHigh, they will overpower the shrinking behavior. | |
/// + You should feel free to set a custom behavior in your init method or afterwards, to define the | |
/// bounds for growing and shrinking. | |
/// + When you're using your cell, you need to listen to your `UITableView`'s scrollViewDidScroll(_:) | |
/// delegate method and forward calls from there to this class's tableViewDidScroll(_:) function so | |
/// that the cell can update its layout in response to the scroll event. | |
class ShrinkingTableCell: UITableViewCell { | |
/// This wrapper view is used to ensure the cell's height doesn't change from the table view's perspective | |
let fixedHeightContainer: UIView = UIView() | |
/// Add your UI to this content view, which will automatically adjust its height as you've requested | |
public let shrinkingContentView: UIView = UIView() | |
private var shrinkingContentViewVerticalConstraint: NSLayoutConstraint! | |
public private(set) var effectiveTopInset: CGFloat = 0 { | |
didSet { | |
shrinkingContentViewVerticalConstraint.constant = effectiveTopInset | |
} | |
} | |
public var behavior: ShrinkingTableCellBehavior = ShrinkingTableCellBehavior.defaultBehavior { | |
didSet { | |
makeAllConstraints() | |
} | |
} | |
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { | |
super.init(style: style, reuseIdentifier: reuseIdentifier) | |
setupShrinkingViews() | |
} | |
required init?(coder: NSCoder) { | |
super.init(coder: coder) | |
setupShrinkingViews() | |
} | |
override func prepareForReuse() { | |
super.prepareForReuse() | |
effectiveTopInset = 0 | |
} | |
private func setupShrinkingViews() { | |
contentView.addSubview(fixedHeightContainer) | |
fixedHeightContainer.addSubview(shrinkingContentView) | |
makeAllConstraints() | |
} | |
private func makeAllConstraints() { | |
fixedHeightContainer.removeConstraints(fixedHeightContainer.constraints) | |
shrinkingContentView.removeConstraints(shrinkingContentView.constraints) | |
fixedHeightContainer.translatesAutoresizingMaskIntoConstraints = false | |
shrinkingContentView.translatesAutoresizingMaskIntoConstraints = false | |
let fhEdgeConstraints = [ | |
fixedHeightContainer.leftAnchor.constraint(equalTo: contentView.leftAnchor), | |
fixedHeightContainer.rightAnchor.constraint(equalTo: contentView.rightAnchor), | |
fixedHeightContainer.topAnchor.constraint(equalTo: contentView.topAnchor), | |
fixedHeightContainer.bottomAnchor.constraint(equalTo: contentView.bottomAnchor) | |
] | |
let scvEdgeConstraints = [ | |
shrinkingContentView.leftAnchor.constraint(equalTo: fixedHeightContainer.leftAnchor), | |
shrinkingContentView.rightAnchor.constraint(equalTo: fixedHeightContainer.rightAnchor), | |
shrinkingContentView.topAnchor.constraint(greaterThanOrEqualTo: fixedHeightContainer.topAnchor), | |
shrinkingContentView.bottomAnchor.constraint(equalTo: fixedHeightContainer.bottomAnchor) | |
] | |
let heightBoundsConstraints: [NSLayoutConstraint] | |
if let asRatio = behavior.cellHeightAsRatioOfWidth { | |
heightBoundsConstraints = [ | |
fixedHeightContainer.heightAnchor.constraint( | |
equalTo: fixedHeightContainer.widthAnchor, | |
multiplier: CGFloat(asRatio.upperBound) | |
), | |
shrinkingContentView.heightAnchor.constraint( | |
greaterThanOrEqualTo: fixedHeightContainer.widthAnchor, | |
multiplier: CGFloat(asRatio.lowerBound) | |
) | |
] | |
} else if let asPoints = behavior.cellHeightAsPointValues { | |
heightBoundsConstraints = [ | |
fixedHeightContainer.heightAnchor.constraint(equalToConstant: CGFloat(asPoints.upperBound)), | |
shrinkingContentView.heightAnchor.constraint( | |
greaterThanOrEqualToConstant: CGFloat(asPoints.lowerBound) | |
) | |
] | |
} else { | |
fatalError("ShrinkingTableViewCell needs to be given size bounds in its behavior") | |
} | |
let desiredTopPaddingConstraint = shrinkingContentView.topAnchor.constraint( | |
equalTo: fixedHeightContainer.topAnchor, constant: 0 | |
) | |
desiredTopPaddingConstraint.priority = UILayoutPriority.defaultHigh | |
shrinkingContentViewVerticalConstraint = desiredTopPaddingConstraint | |
NSLayoutConstraint.activate( | |
fhEdgeConstraints + | |
scvEdgeConstraints + | |
heightBoundsConstraints + | |
[desiredTopPaddingConstraint] | |
) | |
} | |
/// Call this method whenever your table view containing this cell scrolls, so that this cell can update its appearance. | |
func tableViewDidScroll(_ tableView: UIScrollView) { | |
let topShift = tableView.contentOffset.y + tableView.adjustedContentInset.top | |
// This number will be positive if the frame's top is below the "visible" top | |
let distanceFromTopOfTableToTopOfCell = self.frame.origin.y - topShift | |
effectiveTopInset = max(0, -1 * distanceFromTopOfTableToTopOfCell) | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment