18

I have a UITableView where the user can swipe left to reveal actions (like in iOS 8 mail). That all works as expected. I want to trigger this when the user taps on a certain part of the cell. How can I invoke this slide action programmatically?

Current behavior: User must swipe the cell left to disclose the action buttons.

Desired behavior: User taps (an actions button) on the cell. Cell slides over to disclose the action buttons.

rmaddy
  • 314,917
  • 42
  • 532
  • 579
VaporwareWolf
  • 10,143
  • 10
  • 54
  • 80

6 Answers6

21

Well I couldn't find a way to do this programmatically, but I came up with this workaround. When the user taps the cell, I animated (pan) it to the left to momentarily reveal afake "Swipe Me" button. This is quickly reversed so the cell is back to normal. This provides a visual cue to let the user know that they can swipe the cell:

- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath{
    UITableViewCell *cell = [tableView cellForRowAtIndexPath:indexPath];

    __block UILabel *swipeLabel = [[UILabel alloc]initWithFrame:CGRectMake(cell.bounds.size.width,
                                                                   0,
                                                                   200,
                                                                   cell.bounds.size.height)];

    swipeLabel.text = @"  Swipe Me";
    swipeLabel.backgroundColor = [UIColor greenColor];
    swipeLabel.textColor = [UIColor whiteColor];
    [cell addSubview:swipeLabel];

    [UIView animateWithDuration:0.3 animations:^{
        [cell setFrame:CGRectMake(cell.frame.origin.x - 100, cell.frame.origin.y, cell.bounds.size.width, cell.bounds.size.height)];
    } completion:^(BOOL finished) {
        [UIView animateWithDuration:0.3 animations:^{
            [cell setFrame:CGRectMake(cell.frame.origin.x + 100, cell.frame.origin.y, cell.bounds.size.width, cell.bounds.size.height)];
        } completion:^(BOOL finished) {
            [swipeLabel removeFromSuperview];
            swipeLabel = nil;
        }];
    }];
}

Hope this helps someone.

Note that you need to set your tableViewCell's selection type to none. Else the gray bar will obscure it.

Update. I thought I'd post a more Swifty version:

func previewActions(forCellAt indexPath: IndexPath) {
    guard let cell = tableView.cellForRow(at: indexPath) else {
        return
    }

    let label: UILabel = {
        let label = UILabel(frame: CGRect.zero)
        label.text = "  Swipe Me  "
        label.backgroundColor = .blue
        label.textColor = .white
        return label
    }()

    // Figure out the best width and update label.frame
    let bestSize = label.sizeThatFits(label.frame.size)
    label.frame = CGRect(x: cell.bounds.width - bestSize.width, y: 0, width: bestSize.width, height: cell.bounds.height)
    cell.insertSubview(label, belowSubview: cell.contentView)

    UIView.animate(withDuration: 0.3, animations: {
        cell.transform = CGAffineTransform.identity.translatedBy(x: -label.bounds.width, y: 0)
        label.transform = CGAffineTransform.identity.translatedBy(x: label.bounds.width, y: 0)
    }) { (finished) in
        UIView.animateKeyframes(withDuration: 0.3, delay: 0.25, options: [], animations: {
            cell.transform = CGAffineTransform.identity
            label.transform = CGAffineTransform.identity
        }, completion: { (finished) in
            label.removeFromSuperview()
        })
    }
}

func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
    previewActions(forCellAt: indexPath)
    return
}
VaporwareWolf
  • 10,143
  • 10
  • 54
  • 80
  • 1
    Awesome. For the second part I used a delay instead so text shows up for a split second `[UIView animateWithDuration:0.5 delay:0.5 options:0 animations:^{ [cell setFrame:CGRectMake(cell.frame.origin.x + 100, cell.frame.origin.y, cell.bounds.size.width, cell.bounds.size.height)]; } completion:^(BOOL finished) { [swipeLabel removeFromSuperview]; swipeLabel = nil; }];` – CyberMew Feb 13 '15 at 05:57
  • 1
    Great! Also make sure "Clip to bounds" is disabled or the label won't be visible. – rstewart22 Feb 22 '18 at 12:25
  • Brilliant. Thank you. – thecloud_of_unknowing May 30 '19 at 12:46
8

For anyone in search of the Swift version of VaporwareWolf's answer, here it is:

func animateRevealHideActionForRow(tableView: UITableView, indexPath: IndexPath) {
    let cell = tableView.cellForRow(at: indexPath)

    // Should be used in a block
    var swipeLabel: UILabel? = UILabel.init(frame: CGRect(x: cell!.bounds.size.width,
                                                          y: 0,
                                                          width: 200,
                                                          height: cell!.bounds.size.height))

    swipeLabel!.text = "  Swipe Me";
    swipeLabel!.backgroundColor = UIColor.init(red: 255/255, green: 41/255, blue: 53/255, alpha: 1) // Red
    swipeLabel!.textColor = UIColor.white
    cell!.addSubview(swipeLabel!)

    UIView.animate(withDuration: 0.3, animations: {
        cell!.frame = CGRect(x: cell!.frame.origin.x - 100, y: cell!.frame.origin.y, width: cell!.bounds.size.width + 100, height: cell!.bounds.size.height)
    }) { (finished) in
        UIView.animate(withDuration: 0.3, animations: {

            cell!.frame = CGRect(x: cell!.frame.origin.x + 100, y: cell!.frame.origin.y, width: cell!.bounds.size.width - 100, height: cell!.bounds.size.height)

        }, completion: { (finished) in
            swipeLabel?.removeFromSuperview()
            swipeLabel = nil;
        })
    }
}
Burak
  • 525
  • 4
  • 24
4

Don't go with a hacky way and use a library like this until they implement the feature.

https://github.com/SwipeCellKit/SwipeCellKit

This library let you do things like: cell.showSwipe(orientation: .right, animated: true)

Izumi.H
  • 147
  • 1
  • 4
1

And for a Swift 3 version where you want to call this function for every row.

override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {

    let cell = tableView.cellForRow(at: indexPath);
    UIView.animate(withDuration: 0.3, animations: {

        cell!.frame = CGRect(x: cell!.frame.origin.x - 100, y: cell!.frame.origin.y, width: cell!.bounds.size.width + 100, height: cell!.bounds.size.height)

    }) { (finished) in
        UIView.animate(withDuration: 0.3, animations: {

            cell!.frame = CGRect(x: cell!.frame.origin.x + 100, y: cell!.frame.origin.y, width: cell!.bounds.size.width - 100, height: cell!.bounds.size.height)

        }, completion: { (finished) in
        })
    }
}
Uyghur Lives Matter
  • 18,820
  • 42
  • 108
  • 144
Mikael Nyborg
  • 111
  • 3
  • 12
1

Another modified version of VaporwareWolf's answer, this one shows two rectangles instead of one (works nicely with iOS 11's UISwipeActionsConfiguration API). I'm using this on for cells which have two labels, laid out with constraints, and could only get this to work right when animating the constraints instead of the actual cell or labels.

func showActions(forRow row:Int) {
    guard let cell = (tableView.cellForRow(at: IndexPath(row: row, section: 0))) as? RequestTableViewCell else {
        return
    }

    let labelWidth:CGFloat = 20

    let createLabel:(UIColor)->UILabel = {
        color in
        let label = UILabel(frame: CGRect.zero)
        label.backgroundColor = color
        label.clipsToBounds = false
        label.frame = CGRect(x: cell.bounds.width, y: 0, width: labelWidth, height: cell.bounds.height)
        return label
    }

    let greenLabel = createLabel(.green)
    let redLabel = createLabel(.red)

    //ordering of the subviews is key to get it to look right
    cell.insertSubview(greenLabel, aboveSubview: cell.contentView)
    cell.insertSubview(redLabel, belowSubview: greenLabel)

    let originalLabelSeparationContstraint = cell.labelSeparationContstraint

    UIView.animate(withDuration: 0.5, delay: 0, usingSpringWithDamping: 1, initialSpringVelocity: 3, options: .curveEaseOut, animations: {
        cell.leftLabelLeadingConstraint.constant -= (labelWidth * 2)
        cell.labelSeparationContstraint.constant = cell.requestAgeLabel.frame.minX - cell.nameLabel.frame.maxX
        cell.rightLabelTrailingContstraint.constant += (labelWidth * 2)
        cell.layoutIfNeeded()

        greenLabel.transform = greenLabel.transform.translatedBy(x: -(labelWidth), y: 0)
        redLabel.transform = redLabel.transform.translatedBy(x: -(labelWidth * 2), y: 0)
    }, completion: {
        _ in
        UIView.animate(withDuration: 0.5, delay: 0, usingSpringWithDamping: 1, initialSpringVelocity: 3, options: .curveEaseOut, animations: {
            cell.leftLabelLeadingConstraint.constant += (labelWidth * 2)
            cell.labelSeparationContstraint = originalLabelSeparationContstraint
            cell.rightLabelTrailingContstraint.constant -= (labelWidth * 2)
            cell.layoutIfNeeded()

            greenLabel.transform = CGAffineTransform.identity
            redLabel.transform = CGAffineTransform.identity
        }, completion: {
            _ in
            greenLabel.removeFromSuperview()
            redLabel.removeFromSuperview()
        })
    })
}
OneWholeBurrito
  • 482
  • 4
  • 14
0

Swift 3 Equivalent of Burak's answer. You can programmatically fire off this call wherever you want, I put it in a helper function so any table view could show this. And the first time a user uses my app I swipe it open (I felt a longer animation time was needed).

class func animateRevealHideActionForRow(tableView: UITableView, indexPath: NSIndexPath) {
    let cell = tableView.cellForRow(at: indexPath as IndexPath);

    // Should be used in a block
    var swipeLabel: UILabel? = UILabel.init(frame: CGRect.init(x: cell!.bounds.size.width, y: 0, width: 200, height: cell!.bounds.size.height))

    swipeLabel!.text = "  Swipe Me";
    swipeLabel!.backgroundColor = UIColor.red
    swipeLabel!.textColor = UIColor.white
    cell!.addSubview(swipeLabel!)

    UIView.animate(withDuration: 1.0, animations: {
        cell!.frame = CGRect.init(x: cell!.frame.origin.x - 100, y: cell!.frame.origin.y, width: cell!.bounds.size.width + 100, height: cell!.bounds.size.height)
    }) { (finished) in
        UIView.animate(withDuration: 1.0, animations: {
            cell!.frame = CGRect.init(x: cell!.frame.origin.x + 100, y: cell!.frame.origin.y, width: cell!.bounds.size.width - 100, height: cell!.bounds.size.height)
        }, completion: { (finished) in
            swipeLabel?.removeFromSuperview()
            swipeLabel = nil;
        })
    }
}
Stu P.
  • 1,365
  • 1
  • 14
  • 31