Как повторно получить прозрачный эффект маски UITableViewCell цвета во время увольнения ячейки на iOS 11, который ранее работал на iOS 10?

Предисловие

У меня есть рабочая пользовательская анимация отбрасывания UITableViewCell на iOS 10. Способ ее реализации выглядит следующим образом:

  • Пользователь нажимает кнопку отправки
  • Пользовательский вид создается, позиционируется и вставляется с нулевым индексом подтекстов UITableViewCell (под ячейкой contentView)
  • contentView анимируется вне кадра, а пользовательский альфа-просмотр анимируется до 1.0
  • Когда contentView полностью выходит за рамки, вызывается собственный UITableView метод deleteRows(at: [indexPath], with: .top). Сбой UITableViewCell и пользовательское представление маскируется за предыдущим UITableViewCell по мере его сбрасывания.
  • Ячейка (и, следовательно, все ее подпункты, включая мое пользовательское представление) удаляется.

Ниже приведена медленная анимация:

Маска UITableViewCell, работающая на iOS 10

Примечание. tableView имеет четкий цвет фона, позволяя отображать за ним пользовательский вид (синий). Каждая ячейка имеет containerView, которая содержит все содержимое ячейки. containerView и contentView оба имеют четкие цвета фона. Все в порядке и денди.

Проблема

Миграция моего приложения на iOS 11, эта анимация больше не работает должным образом. Ниже приведена медленная анимация, которая больше не работает.

Маска UITableViewCell, работающая на iOS 11

Как вы можете видеть, пользовательский вид накладывается поверх предыдущей ячейки при удалении соты без изменений в моем коде.

Исследование до сих пор

До сих пор я решил, что анатомия a UITableView изменилась из этого:

UITableView
    UITableViewWrapperView
        cell
            custom view
            contentView
            cell separator view
        cell
        cell
        cell
    UIView
    UIImageView  (scroll view indicator bottom)
    UIImageView  (scroll view indicator right)

Для этого: (UITableViewWrapperView был удален)

UITableView
    cell
        custom view
        contentView
        cell separator view
    cell
    cell
    cell
    UIView
    UIImageView (scroll view indicator bottom)
    UIImageView (scroll view indicator right)

Одна вещь, которую я заметил об этом UITableWrapperView, заключается в том, что его свойство isOpaque имеет значение true, а свойство masksToBounds - false, а UITableView и все UITableViewCell противоположны. Поскольку это представление было удалено в iOS 11, это может способствовать тому, что у меня есть ошибочный эффект. Я действительно не знаю.

Edit: Еще одна вещь, которую я обнаружил, заключалась в том, что UITableWrapperView в рабочем примере вставляет тайну UIView в нулевом индексе ее subviews (все UITableViewCell s), для свойств которой isOpaque установлено значение true и он имеет compositingFilter. Это представление впоследствии удаляется после завершения анимации. Поскольку UITableWrapperView удаляется в iOS 11, это представление также связывается с ассоциацией.

Вопрос

Прежде всего, кто-нибудь знает, почему это изменение в поведении происходит? Если нет, есть ли альтернативный способ добиться эффекта, который лучше работает в iOS 10? Я хочу clear UITableViewCell, но у вас есть пользовательский вид, который отображается за каждой ячейкой при увольнении, который маскируется другим clear UITableViewCell при увольнении, как показано в первом примере gif выше.

Ответ 1

Перед удалением строки отправьте ячейку, которую вы удаляете, в обратную сторону подзонов представления таблицы.

tableView.sendSubview(toBack: cell)
tableView.deleteRows(at: [indexPath], with: .top)

Если у вас нет ячейки, вы можете получить ее с помощью указательного пути.

guard let cell = tableView.cell(for: indexPath) else { return }

// ...

Это должно работать с настройкой иерархии представлений.

Просмотр настройки иерархии

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
    }
}

Ответ 2

Прежде всего, я не считаю недостающую UITableViewWrapperView проблемой. Это родительское представление, и родители не могут скрывать представления пользователей.

Обратите внимание, что проблема в том, какая ячейка скрывает, какая. В первом случае строка A затеняет строку B, а во второй строке B скрывает строку A (в этом случае ваша пользовательская ячейка B имеет прозрачный фон).

Итак, похоже, что Apple завязывает отношения между ячейками. Чтобы исправить это, я попытался бы исправить эти отношения. Поэтому перед началом второй части анимации я бы попытался:

вернуть ячейку обратно

cell.superview.sendSubview(toBack: cell)

Это не должно ничего сломать (на старых версиях iOS) и должно решить проблему с iOS 11.

Ответ 3

Я много дней учился, но не смог.

iOS10

После deleteRows, TableViewCell имеет представление с свойством compositingFilter, значение равно copy, которое позволяет скрыть все представления позади этого.

Мы можем попытаться удалить subview TableViewCell, сгенерированный в то время как deleteRows. Тогда iOS 10 будет вести себя как iOS11.

Затем мы можем попробовать снова добавить представление (UIView()) в TableViewCell и установить compositingFilter на copy (String), он снова достигнет in iOS10.

Но любой сбой действия в iOS11. Он создаст черный фон в iOS11.

Я считаю, что поведение слоя было изменено на iOS11.

Возможно, нам стоит подумать о том, чтобы временно отказаться от анимации up в ios11, когда фон TableViewCell прозрачен.

Ответ 4

Я столкнулся с такой проблемой после просмотра приложения на iOS 11, проблема была в анимации при удалении строк из представления таблицы:

введите описание изображения здесь

Понятно, что коллапсирующая анимация не подходит, потому что удаляется более одной строки.

Вот ответственный код для расширения/свертывания строк:

// inserting rows:                
tableView.beginUpdates()
tableView.insertRows(at: [IndexPath(row: 4 + count, section: 0), IndexPath(row: 5 + count, section: 0), IndexPath(row: 6 + count, section: 0)], with: .automatic)
tableView.endUpdates()

// deleting rows:
tableView.beginUpdates()
tableView.deleteRows(at: [IndexPath(row: 4 + count, section: 0), IndexPath(row: 5 + count, section: 0), IndexPath(row: 6 + count, section: 0)], with: .automatic)
tableView.endUpdates()

, очевидно, существует также код для редактирования массива источников данных.


Причиной возникновения этой проблемы было - просто - назначенный UITableViewRowAnimation в automatic; Все, что я сделал, это изменить анимацию как fade:

// inserting rows:                
tableView.beginUpdates()
tableView.insertRows(at: [IndexPath(row: 4 + count, section: 0), IndexPath(row: 5 + count, section: 0), IndexPath(row: 6 + count, section: 0)], with: .fade)
tableView.endUpdates()

// deleting rows:
tableView.beginUpdates()
tableView.deleteRows(at: [IndexPath(row: 4 + count, section: 0), IndexPath(row: 5 + count, section: 0), IndexPath(row: 6 + count, section: 0)], with: .fade)
tableView.endUpdates()

И результат был исправлен как:

введите описание изображения здесь

И

Возможно, вы захотите проверить следующий вопрос, который может быть связан с вашей проблемой:

Ответ 5

Я нашел решение для UITableView с ячейками фиксированного размера

представление решения в действии

В этом подходе я решил добавить дополнительное представление к представлению содержимого UITableViewCell и анимировать его ограничение высоты до 0 рядом с анимацией удаления строки.

Чтобы все было просто, я покажу только ключевые моменты этого решения.

  • Кнопка удаления нажатий пользователя.
  • Ячейка перемещает внутренний просмотр содержимого вправо.
  • Режим просмотра таблицы вызывает метод deleteRows(at:with:).
  • Ячейка оживляет высоту дополнительного представления до 0.

Я не буду описывать всю установку автоматического макета здесь, потому что это затмит основную идею решения. Все строительные блоки довольно просты, но их несколько. Вы можете найти полный рабочий проект в этом репозитории GitHub.


К сожалению, это решение не очень хорошо работает с динамическими ячейками высоты из-за времени анимации. Я не уверен, что это решит вашу проблему, поскольку, насколько я вижу, вы используете динамические ячейки высоты. Тем не менее, я надеюсь, что это подтолкнет вас в правильном направлении или поможет кому-то с аналогичной проблемой.