David West
David West

Reputation: 1590

Swift - Animating When TableView Cell Selected (didSelectRowAtIndexPath)

I've been playing around trying to animate contents of my tableviewcell when it is selected, but i'm having issues. I had read some places that you cannot run animatewithduration within didSelectRowAtIndexPath or at least it doesn't animate if you do. It seems to be the case with the code I have below, the view moves but doesn't animate.

I have tried incorporating logic into willDisplayCell to monitor for the selection but to no avail, and so I would love it if someone is able to find the right solution for me because the answer is seemingly not available, despite a lot of searching:

func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
    let currentCellDescriptor = getCellDescriptorForIndexPath(indexPath)
    let cell = tableView.dequeueReusableCellWithIdentifier(currentCellDescriptor["cellIdentifier"] as! String, forIndexPath: indexPath) as! CustomCell
    let indexOfTappedRow = visibleRowsPerSection[indexPath.section][indexPath.row]
    if cellDescriptors[indexPath.section][indexOfTappedRow]["isExpandable"] as! Bool == true {
        if cellDescriptors[indexPath.section][indexOfTappedRow]["isExpanded"] as! Bool == false {


            // INTERESTING BIT: Animates cell contents to Right
            if currentCellDescriptor["cellIdentifier"] as! String == "CellHeader" {
                UIView.animateWithDuration(0.5, delay: 0, usingSpringWithDamping: 0.7, initialSpringVelocity: 12, options: .CurveLinear, animations: {
                    cell.headerTitleLeftConstraint.priority = 999
                    cell.headerTitleRightConstraint.priority = 1
                    cell.layoutIfNeeded()
                    }, completion: nil)
            }

       } else if cellDescriptors[indexPath.section][indexOfTappedRow]["isExpanded"] as! Bool == true {
           // MARK: Animates cell contents back to left
           if currentCellDescriptor["cellIdentifier"] as! String == "CellHeader" {
                UIView.animateWithDuration(0.5, delay: 0, usingSpringWithDamping: 0.7, initialSpringVelocity: 12, options: .CurveLinear, animations: {
                    cell.headerTitleLeftConstraint.priority = 1
                    cell.headerTitleRightConstraint.priority = 999
                    cell.layoutIfNeeded()
                     }, completion: nil)
                    }
            }

Upvotes: 1

Views: 7917

Answers (3)

laka
laka

Reputation: 796

You do not need to use UIView.animate(...). UITableView offers animated updating by default. After changing the constraints, you can achieve this by calling:

tableView.performBatchUpdates(nil, completion: nil)     

Do not forget to give your height property a lower priority than .required when creating it. Otherwise AutoLayout will break constraints while updating (You can observe this in your debugger). Instead of setting constraints' priorities to 999 or something like that you should use static properties of UILayoutPriority for more readable code.

/// Set this wherever you create your height constraint, e.g. in your custom cell's class.
heightConstraint?.priority = .defaultHigh

The full code for didSelectRowAt would look like this:

    let expandedHeight: CGFloat = 500
    let defaultHeight: CGFloat = 85

    func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
      guard let cell = tableView.cellForRow(at: indexPath) as? YourCustomTableViewCell else { return }

      /// Save the status of your cell in a property of your view model.
      if viewModel[indexPath.row].isFullsize {
        cell.heightConstraint?.constant = defaultHeight
        viewModel[indexPath.row].isFullsize = false
      } else {
        cell.heightConstraint?.constant = expandedHeight
        viewModel[indexPath.row].isFullsize = true
      }

      /// Call this for an animated update (since iOS 11)
      tableView.performBatchUpdates(nil, completion: nil)

      /// For the same result you could also use
      /// tableView.beginUpdates()
      /// tableView.endUpdates()
      /// ...but the docs say that this will become deprecated soon

      /// Scrolls to your selected cell's top
      tableView.scrollToRow(at: indexPath, at: .top, animated: true)
    }

Upvotes: 0

David West
David West

Reputation: 1590

With Tanguy's help, this is how we're looking. I'm still having some animation issues because the table itself won't actually animate the next level of cells underneath, but I will improve that code and get it working. Think this is fit for purpose now, so worth posting to show the solution itself. Thanks!!

 override func setSelected(selected: Bool, animated: Bool) {
    super.setSelected(selected, animated: animated)
        // Adjusts constraints everytime switch is tapped
        isLeftAligned = !isLeftAligned
            if userHanded == "Right" {
            UIView.animateWithDuration(0.5, delay: 0, usingSpringWithDamping: 0.7, initialSpringVelocity: 12, options: .CurveLinear, animations: {
                self.leftHandedHeaderConstraint.priority = (self.isLeftAligned) ? 1 : 999
                self.rightHandedHeaderConstraint.priority = (self.isLeftAligned) ? 999 : 1
                self.layoutIfNeeded()
            }, completion: nil)

        } else if userHanded == "Left" {
            UIView.animateWithDuration(0.5, delay: 0, usingSpringWithDamping: 0.7, initialSpringVelocity: 12, options: .CurveLinear, animations: {
                self.leftHandedHeaderConstraint.priority = (self.isLeftAligned) ? 999 : 1
                self.rightHandedHeaderConstraint.priority = (self.isLeftAligned) ? 1 : 999
                self.layoutIfNeeded()
                }, completion: nil)
        }
}

Upvotes: 0

tgyhlsb
tgyhlsb

Reputation: 1915

First, don't use dequeueReusableCellWithIdentifier here. It will use a cell not visible on screen and prepare to show it. What you want is cellForRowAtIndexPath which returns a cell already on screen at the given indexPath.

Then I understand you want to play with 2 constraint and animate the layout changes. In order to do so, the animation should only contain layoutIfNeeded:

cell.headerTitleLeftConstraint.priority = 999
cell.headerTitleRightConstraint.priority = 1
UIView.animateWithDuration(0.5, delay: 0, usingSpringWithDamping: 0.7, initialSpringVelocity: 12, options: .CurveLinear, animations: {
                cell.layoutIfNeeded()
                }, completion: nil)

I'd also advice you to transfer this logic from the controller to your CustomCell class. For example use the selected attribute and the setSelected(animated: Bool) to handle the state change visually.

Upvotes: 3

Related Questions