27

I am using the 'swipe to delete' functionality of the UITableView.

The problem is I am using a customised UITableViewCell which is created on a per item basis in

- (UITableViewCell *)tableView:(UITableView *)aTableView cellForRowAtIndexPath:(NSIndexPath *)indexPath

I need to alter the position of the delete button (simply to move it around 10px to the left), how would I go about doing this?

Here is my existing code for creating the cell:

- (UITableViewCell *)tableView:(UITableView *)aTableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    NSLog(@"cellForRowAtIndexPath");
#if USE_CUSTOM_DRAWING
    const NSInteger TOP_LABEL_TAG = 1001;
    const NSInteger BOTTOM_LABEL_TAG = 1002;
    UILabel *topLabel;
    UILabel *bottomLabel;
#endif

    static NSString *CellIdentifier = @"Cell";
    UITableViewCell *cell = [aTableView dequeueReusableCellWithIdentifier:CellIdentifier];
    if (cell == nil)
    {
        //
        // Create the cell.
        //
        cell =
        [[[UITableViewCell alloc]
          initWithFrame:CGRectZero
          reuseIdentifier:CellIdentifier]
         autorelease];

#if USE_CUSTOM_DRAWING


        const CGFloat LABEL_HEIGHT = 20;
        UIImage *image = [UIImage imageNamed:@"trans_clock.png"];

        //
        // Create the label for the top row of text
        //
        topLabel =
        [[[UILabel alloc]
          initWithFrame:
          CGRectMake(
                     image.size.width + 2.0 * cell.indentationWidth,
                     0.5 * (aTableView.rowHeight - 2 * LABEL_HEIGHT),
                     aTableView.bounds.size.width -
                     image.size.width - 4.0 * cell.indentationWidth
                     ,
                     LABEL_HEIGHT)]
         autorelease];
        [cell.contentView addSubview:topLabel];

        //
        // Configure the properties for the text that are the same on every row
        //
        topLabel.tag = TOP_LABEL_TAG;
        topLabel.backgroundColor = [UIColor clearColor];
        topLabel.textColor = fontColor;
        topLabel.highlightedTextColor = [UIColor colorWithRed:1.0 green:1.0 blue:0.9 alpha:1.0];
        topLabel.font = [UIFont systemFontOfSize:[UIFont labelFontSize]];

        //
        // Create the label for the top row of text
        //
        bottomLabel =
        [[[UILabel alloc]
          initWithFrame:
          CGRectMake(
                     image.size.width + 2.0 * cell.indentationWidth,
                     0.5 * (aTableView.rowHeight - 2 * LABEL_HEIGHT) + LABEL_HEIGHT,
                     aTableView.bounds.size.width -
                     image.size.width - 4.0 * cell.indentationWidth
                     ,
                     LABEL_HEIGHT)]
         autorelease];
        [cell.contentView addSubview:bottomLabel];

        //
        // Configure the properties for the text that are the same on every row
        //
        bottomLabel.tag = BOTTOM_LABEL_TAG;
        bottomLabel.backgroundColor = [UIColor clearColor];
        bottomLabel.textColor = fontColor;
        bottomLabel.highlightedTextColor = [UIColor colorWithRed:1.0 green:1.0 blue:0.9 alpha:1.0];
        bottomLabel.font = [UIFont systemFontOfSize:[UIFont labelFontSize] - 2];

        //
        // Create a background image view.
        //
        cell.backgroundView =
        [[[UIImageView alloc] init] autorelease];
        cell.selectedBackgroundView =
        [[[UIImageView alloc] init] autorelease];
#endif
    }

#if USE_CUSTOM_DRAWING
    else
    {
        topLabel = (UILabel *)[cell viewWithTag:TOP_LABEL_TAG];
        bottomLabel = (UILabel *)[cell viewWithTag:BOTTOM_LABEL_TAG];
    }
    topLabel.text  = @"Example Text";
    topLabel.textColor = fontColor;

    bottomLabel.text = @"More Example Text";
    bottomLabel.textColor = fontColor;

    //
    // Set the background and selected background images for the text.
    // Since we will round the corners at the top and bottom of sections, we
    // need to conditionally choose the images based on the row index and the
    // number of rows in the section.
    //
    UIImage *rowBackground;
    UIImage *selectionBackground;
    NSInteger sectionRows = [aTableView numberOfRowsInSection:[indexPath section]];
    NSInteger row = [indexPath row];
    if (row == 0 && row == sectionRows - 1)
    {
        rowBackground = [UIImage imageNamed:@"topAndBottomRow.png"];
        selectionBackground = [UIImage imageNamed:@"topAndBottomRowSelected.png"];
    }
    else if (row == 0)
    {
        rowBackground = [UIImage imageNamed:@"topRow.png"];
        selectionBackground = [UIImage imageNamed:@"topRowSelected.png"];
    }
    else if (row == sectionRows - 1)
    {
        rowBackground = [UIImage imageNamed:@"bottomRow.png"];
        selectionBackground = [UIImage imageNamed:@"bottomRowSelected.png"];
    }
    else
    {
        rowBackground = [UIImage imageNamed:@"middleRow.png"];
        selectionBackground = [UIImage imageNamed:@"middleRowSelected.png"];
    }
    ((UIImageView *)cell.backgroundView).image = rowBackground;
    ((UIImageView *)cell.selectedBackgroundView).image = selectionBackground;

    cell.imageView.image = [UIImage imageNamed:@"Example_Image.png"];
#else
    cell.text = @"Example";
#endif
    return cell;
}
Brian Tompsett - 汤莱恩
  • 5,753
  • 72
  • 57
  • 129
Mick Walker
  • 3,862
  • 6
  • 47
  • 72

6 Answers6

58

For me the best way to solve this was overriding -(void)layoutSubviews in MyCell:UITableViewCell

Here you can see not only the Delete button custom position, but also repositioning the Edit and Reorder controls for Edit mode

- (void)layoutSubviews
{
    [super layoutSubviews];

    [UIView beginAnimations:nil context:NULL];
    [UIView setAnimationBeginsFromCurrentState:YES];
    [UIView setAnimationDuration:0.0f];

    for (UIView *subview in self.subviews) {


        if ([NSStringFromClass([subview class]) isEqualToString:@"UITableViewCellDeleteConfirmationControl"]) { 
            CGRect newFrame = subview.frame;
            newFrame.origin.x = 200;
            subview.frame = newFrame;
        }
        else if ([NSStringFromClass([subview class]) isEqualToString:@"UITableViewCellEditControl"]) {             
            CGRect newFrame = subview.frame;
            newFrame.origin.x = 100;
            subview.frame = newFrame;
        }
        else if ([NSStringFromClass([subview class]) isEqualToString:@"UITableViewCellReorderControl"]) {             
            CGRect newFrame = subview.frame;
            newFrame.origin.x = 200;
            subview.frame = newFrame;
        }
    }
    [UIView commitAnimations];
}
Paras Joshi
  • 20,427
  • 11
  • 57
  • 70
Remizorrr
  • 2,322
  • 1
  • 17
  • 25
  • 4
    A better solution than the one accepted, as there is no artificial delay in button animation here. – Morrowless Jun 06 '12 at 02:30
  • great stuff, one thing to note is the animation does not seem to be necessary here... – Nick Hingston Nov 09 '12 at 09:44
  • Nope. The animation is necessary in most cases. – Obiwahn Nov 10 '12 at 11:17
  • 3
    This is superb. Thanks so much for this, best answer to be found, look no further everyone. – n.evermind Nov 22 '12 at 11:26
  • Unfortunately this is not a public API so it may cause your app to be rejected. – SimplyKiwi Jan 14 '13 at 18:42
  • @iBradApps: has anyone got rejected by Apple because of this? – user523234 Mar 12 '13 at 22:21
  • Everything is fine. it is not an "API" so they won't reject your app. I made this in my iOS "Travel:Route Planner" app. – Remizorrr Mar 14 '13 at 00:46
  • @obiwahn Why the animation is necessary? With this animation red "minus" button looks like "jumping" to new position and without this animation everything looks just fine. **upd** Haven't tried "swipe to delete". Now I see why animation should be here. – derpoliuk Apr 03 '13 at 15:22
  • You should **not** be doing animations in `layoutSubviews`. – titaniumdecoy Jun 21 '13 at 16:54
  • @titaniumdecoy it is not animation, it is animation block. – Remizorrr Jun 24 '13 at 00:46
  • @Remizorrr It makes no difference, you should not be doing animation of any kind in `layoutSubviews`. Use an appropriate method such as `didTransitionToState:` for animations. – titaniumdecoy Jun 25 '13 at 17:32
  • 1
    As I stated in the comment of another answer: beware that the class name may change to something diffrent, like: UITableViewCellDeleteConfirmationControl_Legacy in future iOS'es ;-) Better to use: if([NSStringFromClass([subview class]) rangeOfString:@"Delete"].location != NSNotFound). – Lukasz Jul 16 '13 at 07:27
24

Iwat's code doesn't work for me. But this works.

- (void)willTransitionToState:(UITableViewCellStateMask)state {

    [super willTransitionToState:state];

    if ((state & UITableViewCellStateShowingDeleteConfirmationMask) == UITableViewCellStateShowingDeleteConfirmationMask) {

        for (UIView *subview in self.subviews) {

            if ([NSStringFromClass([subview class]) isEqualToString:@"UITableViewCellDeleteConfirmationControl"]) {             

                subview.hidden = YES;
                subview.alpha = 0.0;
            }
        }
    }
}

- (void)didTransitionToState:(UITableViewCellStateMask)state {

    [super didTransitionToState:state];

    if (state == UITableViewCellStateShowingDeleteConfirmationMask || state == UITableViewCellStateDefaultMask) {
        for (UIView *subview in self.subviews) {

            if ([NSStringFromClass([subview class]) isEqualToString:@"UITableViewCellDeleteConfirmationControl"]) {

                UIView *deleteButtonView = (UIView *)[subview.subviews objectAtIndex:0];
                CGRect f = deleteButtonView.frame;
                f.origin.x -= 20;
                deleteButtonView.frame = f;

                subview.hidden = NO;

                [UIView beginAnimations:@"anim" context:nil];
                subview.alpha = 1.0;
                [UIView commitAnimations];
            }
        }
    }
}
Jasarien
  • 58,279
  • 31
  • 157
  • 188
nonamelive
  • 6,510
  • 8
  • 40
  • 47
  • Thanks for your reply, one question (I havent tested the code as yet) but what is the line subview.hidden = NO;** – Mick Walker Feb 05 '10 at 23:42
  • 1
    Sorry. I'm new to stackoverflow. I just wanna make this piece of codes bold. When I clicked the bold button, ** was added to the end. – nonamelive Feb 06 '10 at 08:09
  • Thanks for the solution! There's a small copy&paste error in the didTransitionToState: you're calling [super willTransitionToState.. but it doesn't affect work. – marcio Aug 04 '10 at 08:21
  • 5
    Might I add that this is relying on private class names. Even though the private symbols are being hidden from the compiler and linker, they're still being used. – Jasarien Mar 22 '11 at 16:21
  • @Jasarien Can you please state which are the private symbols used in the code above? – Krishnabhadra Jul 28 '11 at 04:22
  • 8
    `UITableViewCellDeleteConfirmationControl` Is not a public class. – Jasarien Jul 28 '11 at 06:58
  • @Jasarien You can use some similar technique which only uses part of the class name, such as [className hasSuffix:@"DeleteConfrimationControl"]. – nonamelive Jan 20 '12 at 12:54
  • @nonamelive That doesn't make it a better solution. Apple reject apps that use private APIs for a reason. The API is private for a reason. it might change. It might be buggy when not used correctly. Apple don't do it for funsies. – Jasarien Jan 20 '12 at 14:49
  • @Jasarien Yes. I know this is not an actual solution, it's just a hacky workaround. I admit that private APIs should not be used in any form, but sometimes we have no choice if we really want to achieve something which is against Cocoa. – nonamelive Jan 20 '12 at 16:14
  • @nonamelive: Then, at the very least, you should clearly note it in your answer. Stuff like this (string comparison to private class name, fiddling with subviews of classes that you don't control yourself (`[subview.subviews objectAtIndex:0]`) should *never* be done in apps targeted for submission/production use! – Daniel Rinser Sep 09 '12 at 16:16
  • That solution doesn't work in general. It moves the button image but not the button control. Increase the offset to make the problem obvious: f.origin-=100. Taps on the button image miss the button. To make it works you need to move the subview rather than deleteButtonView. `CGRect ff = subview.frame; ff.origin.x -= 200; subview.frame = ff; subview.hidden = NO;` – dmitri Apr 30 '13 at 23:54
  • Even @nonamelive in not completely right as the class name may change to something diffrent, like: UITableViewCellDeleteConfirmationControl_Legacy in future iOS'es ;-) Better to use: if([NSStringFromClass([subview class]) rangeOfString:@"Delete"].location != NSNotFound) – Lukasz Jul 16 '13 at 07:26
2

I use a solution that on first sight looks kind of hacky but does the trick and is not relying on undocumented apis:

/**
 * Transition to state
 */
-(void)willTransitionToState:(UITableViewCellStateMask)state {

    if(state & UITableViewCellStateShowingDeleteConfirmationMask)
        _deleting = YES;
    else if(!(state & UITableViewCellStateShowingDeleteConfirmationMask))
        _deleting = NO;
    [super willTransitionToState:state];

}

/**
 * Reset cell transformations
 */
-(void)resetCellTransform {

    self.transform = CGAffineTransformIdentity;
    _background.transform = CGAffineTransformIdentity;
    _content.transform = CGAffineTransformIdentity;
}

/**
 * Move cell around if we are currently in delete mode
 */
-(void)updateWithCurrentState {

    if(_deleting) {

        float x = -20;
        float y = 0;
        self.transform = CGAffineTransformMakeTranslation(x, y);
        _background.transform = CGAffineTransformMakeTranslation(-x, -y);
        _content.transform = CGAffineTransformMakeTranslation(-x, -y);
    }
}

-(void)setFrame:(CGRect)frame {

    [self resetCellTransform];
    [super setFrame:frame];
    [self updateWithCurrentState];
}

-(void)layoutSubviews {

    [self resetCellTransform];
    [super layoutSubviews];
    [self updateWithCurrentState];
}

Basically this shifts the cell position and readjusts the visible portions. WillTransitionToState just sets an instance variable indicating whether the cell is in delete mode. Overriding setFrame was necessary to support rotating the phone to landscape and vice-versa. Where _background is the background view of my cell and _content is a view added to the contentView of the cell, holding all labels etc.

Stefan
  • 661
  • 6
  • 5
2

I haven't been able to change the actual look of the delete button, but you can change the text. Perhaps you can use this to get the effect you are looking for?

Check out the following member of the UITableViewDelegate protocol:

- (NSString *)tableView:(UITableView *)tableView titleForDeleteConfirmationButtonForRowAtIndexPath:(NSIndexPath *)indexPath

Additionally, you can detect when the delete button is being displayed by subclassing UITableViewCell and overriding the the following:

- (void)willTransitionToState:(UITableViewCellStateMask)state
- (void)didTransitionToState:(UITableViewCellStateMask)state

For either of these, the state mask will be UITableViewCellStateShowingDeleteConfirmationMask when the delete button is being displayed.

Hopefully these clues will point you in the right direction.

Ethan
  • 486
  • 1
  • 6
  • 15
1

I don't know of any way to "move the delete button 10px to the left". However, you can animate the custom contents of your table cell around the static position of the unmovable delete button by listening for the willTransitionToState: message from a UITableViewCell sub-class see here.

To quote the docs:

Subclasses of UITableViewCell can implement this method to animate additional changes to a cell when it is changing state. UITableViewCell calls this method whenever a cell transitions between states, such as from a normal state (the default) to editing mode. The custom cell can set up and position any new views that appear with the new state. The cell then receives a layoutSubviews message (UIView) in which it can position these new views in their final locations for the new state. Subclasses must always call super when overriding this method.

What you are looking for is the UITableViewCellStateShowingDeleteConfirmationMask value. This is one of those times where creating UITableViewCell subclass might be better, so from the controller you are just creating an instance of your custom cell. Then the cell can just adjust itself like animateLeft and animateRight and handle the inner workings of adjusting its own subviews.

slf
  • 22,595
  • 11
  • 77
  • 101
  • I considered subclassing UITableViewCell in the first place, the reason I decided to implement it via code was because I didn't want to manage 3 custom UITableViewCell objects (top, middle and bottom); and now to suit my needs would require creating 4 (an extra one for delete) unless I am very much mistaken? – Mick Walker Jan 20 '10 at 21:39
0

I don't know the final solution, but as far as I tried the following code might be useful.

// subclass UITableViewCell

- (void)willTransitionToState:(UITableViewCellStateMask)state
{
    [super willTransitionToState:state];

    if ((state & UITableViewCellStateShowingDeleteConfirmationMask) == UITableCellStateShowingDeleteConfirmationMask)
    {
        for (UIView *subview in self.subviews)
        {
            if ([NSStringFromClass([subview class]) isEqualToString:@"UITableViewCellDeleteConfirmationControl"])
            {
                subview.hidden = YES;
                subview.alpha = 0;
            }
        }
    }
}

- (void)didTransitionToState:(UITableViewCellStateMask)state
{
    [super willTransitionToState:state];

    if ((state & UITableViewCellStateShowingDeleteConfirmationMask) == UITableCellStateShowingDeleteConfirmationMask)
    {
        for (UIView *subview in self.subviews)
        {
            if ([NSStringFromClass([subview class]) isEqualToString:@"UITableViewCellDeleteConfirmationControl"])
            {
                subview.frame = CGRectMake(subview.frame.origin.x - 10, subview.frame.origin.y, subview.frame.size.width, subview.frame.size.height);
                subview.hidden = NO;

                [UIView beginAnimations:@"anim" context:nil];
                subview.alpha = 1;
                [UIView commitAnimations];
            }
        }
    }
}
iwat
  • 3,591
  • 2
  • 20
  • 24