The Preface
I have a working custom UITableViewCell
dismissal animation on iOS 10. The way it is implemented is as follows:
UITableViewCell
's subviews (underneath the cell's contentView
)contentView
is animated out of frame while the custom view's alpha is animated to 1.0contentView
is fully out-of-frame, the native UITableView
method deleteRows(at: [indexPath], with: .top)
is called. The UITableViewCell
collapses and the custom view is masked behind the previous UITableViewCell
as it collapses.Below is a slow-animation of it working:
Note: The tableView
has a clear background color, allowing a custom view (the blue) to show behind it. Each cell has a containerView
which contains all the content of the cell. The containerView
and contentView
both have clear background colors. All is fine and dandy.
The Problem
Migrating my app to iOS 11, this animation no longer works properly. Below is a slow-animation of it no longer working anymore.
As you can see, the custom view is overlayed on top of the previous cell upon cell dismissal with no changes to my code.
Investigation thus far
So far I've determined that the anatomy of a UITableView
has changed from this:
UITableView
UITableViewWrapperView
cell
custom view
contentView
cell separator view
cell
cell
cell
UIView
UIImageView (scroll view indicator bottom)
UIImageView (scroll view indicator right)
To this: (UITableViewWrapperView
has been removed)
UITableView
cell
custom view
contentView
cell separator view
cell
cell
cell
UIView
UIImageView (scroll view indicator bottom)
UIImageView (scroll view indicator right)
One thing I noticed about this UITableWrapperView
is that its layer's isOpaque
property is true and masksToBounds
property is false while the UITableView
and all of the UITableViewCell
s are opposite. Since this view was removed in iOS 11 this might contribute to why I'm having the erroneous effect. I really don't know.
Edit: Another thing I found was that the UITableWrapperView
in the working example inserts a mystery UIView
at the zero index of it's subviews (all the UITableViewCell
s) which properties isOpaque
is set to true and it has a compositingFilter
. This view is subsequently removed after the animation is complete. Since UITableWrapperView
is removed in iOS 11, this view is, by association, also missing.
Question
First of all, does anybody know why this this change in behavior is occurring? If not, is there an alternative way to achieve the effect that was working in iOS 10 in a better way? I want clear UITableViewCell
s but have a custom view that displays behind each cell upon dismissal that is masked by the other clear UITableViewCell
s when dismissed as shown in the first gif example above.
Before you delete the row, send the cell you're deleting to the back of the table view's subviews.
tableView.sendSubview(toBack: cell)
tableView.deleteRows(at: [indexPath], with: .top)
If you don't have the cell, you can retrieve it using the index path.
guard let cell = tableView.cell(for: indexPath) else { return }
// ...
This should work with an adjustment to the view hierarchy.
CardTableViewController.swift
class CardTableViewController: UITableViewController, CardTableViewCellDelegate {
// MARK: Outlets
@IBOutlet var segmentedControl: UISegmentedControl!
// MARK: Properties
private var cards: [Card] = [
Card(text: "Etiam porta sem malesuada magna mollis euismod. Maecenas faucibus mollis interdum."),
Card(text: "Nulla vitae elit libero, a pharetra augue. Cras justo odio, dapibus ac facilisis in, egestas eget quam."),
Card(text: "Aenean eu leo quam. Pellentesque ornare sem lacinia quam venenatis vestibulum. Sed posuere consectetur est at lobortis.")
]
var selectedIndex: Int { return segmentedControl.selectedSegmentIndex }
var tableCards: [Card] {
return cards.filter { selectedIndex == 0 ? !$0.isDone : $0.isDone }
}
// MARK: UITableViewDataSource
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return tableCards.count
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath) as! CardTableViewCell
cell.card = tableCards[indexPath.row]
return cell
}
override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
return UITableViewAutomaticDimension
}
// MARK: CardTableViewCellDelegate
func cardTableViewCell(_ cell: CardTableViewCell, didTouchUpInsideCheckmarkButton button: UIButton) {
guard let indexPath = tableView.indexPath(for: cell) else { return }
cell.card?.isDone = button.isSelected
cell.card?.dayCount += button.isSelected ? 1 : -1
cell.customView?.dayCount = cell.card?.dayCount
UIView.animate(withDuration: 0.7, delay: 0.3, options: .curveEaseIn, animations: {
cell.checkmarkButton?.superview?.transform = .init(translationX: cell.frame.width, y: 0)
cell.customView?.alpha = 1
}) { _ in
self.tableView.sendSubview(toBack: cell)
self.tableView.deleteRows(at: [indexPath], with: .top)
}
}
// MARK: Actions
@IBAction func didChangeSegmentedControl(_ sender: UISegmentedControl) {
tableView.reloadData()
}
}
CardTableViewCell.swift
@objc protocol CardTableViewCellDelegate {
func cardTableViewCell(_ cell: CardTableViewCell, didTouchUpInsideCheckmarkButton button: UIButton)
}
@IBDesignable class CardTableViewCell: UITableViewCell {
// MARK: Outlets
@IBOutlet var checkmarkButton: UIButton?
@IBOutlet var customView: CardCustomView?
@IBOutlet var delegate: CardTableViewCellDelegate?
@IBOutlet var taskTextLabel: UILabel?
// MARK: Properties
var card: Card? {
didSet {
updateOutlets()
}
}
// MARK: Lifecycle
override func awakeFromNib() {
super.awakeFromNib()
checkmarkButton?.layer.borderColor = UIColor.black.cgColor
}
override func prepareForReuse() {
super.prepareForReuse()
card = nil
}
// MARK: Methods
private func updateOutlets() {
checkmarkButton?.isSelected = card?.isDone == true ? true : false
checkmarkButton?.superview?.transform = .identity
customView?.alpha = 0
customView?.dayCount = card?.dayCount
taskTextLabel?.text = card?.text
}
// MARK: Actions
@IBAction func didTouchUpInsideCheckButton(_ sender: UIButton) {
sender.isSelected = !sender.isSelected
delegate?.cardTableViewCell(self, didTouchUpInsideCheckmarkButton: sender)
}
}
class CardCustomView: UIView {
// MARK: Outlets
@IBOutlet var countLabel: UILabel?
@IBOutlet var daysLabel: UILabel?
// MARK: Properties
var dayCount: Int? {
didSet {
updateOutlets()
}
}
override func awakeFromNib() {
super.awakeFromNib()
updateOutlets()
}
// MARK: Methods
private func updateOutlets() {
let count = dayCount.flatMap { $0 } ?? 0
countLabel?.text = "\(dayCount.flatMap { $0 } ?? 0)"
daysLabel?.text = "Day\(count == 1 ? "" : "s") in a row"
}
}
Card.swift
class Card: NSObject {
// MARK: Properties
var dayCount: Int = 0
var isDone: Bool = false
var text: String
// MARK: Lifecycle
init(text: String) {
self.text = text
}
}
First of all I don't think a missing UITableViewWrapperView
is a problem.
This is parent view and parents can't obscure child views.
Note that problem is which cell obscures which. In first case row A obscures row B and in second row B obscures row A (in this case your custom B cell has transparent background).
So looks like Apple has screw up relations between cells. To fix it I would try to fix this relations. So before second part of animation starts I would try:
bring cell to back
cell.superview.sendSubview(toBack: cell)
This shouldn't break anything (on older iOS versions) and should resolve issue wiht iOS 11.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With