42

I need to change default icon for moving cells in UITableView.

This one:

enter image description here

Is it possible?

Baby Groot
  • 4,637
  • 39
  • 52
  • 71
devgeek
  • 4,117
  • 2
  • 18
  • 18

13 Answers13

65

This is a really hacky solution, and may not work long term, but may give you a starting point. The re-order control is a UITableViewCellReorderControl, but that's a private class, so you can't access it directly. However, you could just look through the hierarchy of subviews and find its imageView.

You can do this by subclassing UITableViewCell and overriding its setEditing:animated: method as follows:

- (void) setEditing:(BOOL)editing animated:(BOOL)animated
{
    [super setEditing: editing animated: YES];

    if (editing) {

        for (UIView * view in self.subviews) {
            if ([NSStringFromClass([view class]) rangeOfString: @"Reorder"].location != NSNotFound) {
                for (UIView * subview in view.subviews) {
                    if ([subview isKindOfClass: [UIImageView class]]) {
                        ((UIImageView *)subview).image = [UIImage imageNamed: @"yourimage.png"];
                    }
                }
            }
        }
    }   
}

Or in Swift

override func setEditing(_ editing: Bool, animated: Bool) {
    super.setEditing(editing, animated: animated)

    if editing {
        for view in subviews where view.description.contains("Reorder") {
            for case let subview as UIImageView in view.subviews {
                subview.image = UIImage(named: "yourimage.png")
            }
        }
    }
}

Be warned though... this may not be a long term solution, as Apple could change the view hierarchy at any time.

Ashley Mills
  • 50,474
  • 16
  • 129
  • 160
  • 1
    Has anyone found a way to do this without risking App Store rejection? This is a very fragile and hacky way of approaching this. – RealCasually Jul 19 '12 at 02:07
  • If this fails, I think the most likely result is that we'll just see the default icon again, which isn't so bad. I also tried Phil's approach and if that fails, it will give me the custom icon in the wrong location with the default icon also visible. So I'll take my chances with this approach. – arlomedia Aug 31 '12 at 21:12
  • My custom icon wasn't the same size as the default icon, so I also set the frame of the found subview to my UIImage's frame. I also extended this to customize the insert and delete icons that appear on the left side of the table cell. Nice! – arlomedia Aug 31 '12 at 23:43
  • Excellent. Still working in current SDK but does anyone have any solid confirmation if Apple accepts this approach? – imnk Sep 07 '12 at 13:15
  • One of my apps uses this approach, and the last three versions were all accepted for sale (meaning it has been reviewed three times since I added this). – arlomedia Oct 21 '12 at 23:33
  • 2
    This works again for iOS 8. So you can use this on iOS 5, 6 and 8 and OgreSwamp's answer for iOS 7. The only difference is that the view hierarchy starts one level deeper in iOS 7. – arlomedia Sep 11 '14 at 17:17
  • The solution I presented is running on iOS 5, iOS 7, and iOS 8. It should work on iOS 6, but I haven't tested it. Basically, I took the logic that finds the reorder control and split it out into a separate method that works for all recent iOS versions. – Rick Morgan Dec 04 '14 at 21:03
  • I think `[NSStringFromClass([view class]) isEqualToString:@"UITableViewCellReorderControl"]` makes more sense than `rangeOfString` from the perspective of writing self explanatory code. Furthermore, I guess the performance would be a bit better (I am not sure though). Anyways, I recommend modifying this snippet a bit since readability is so important. – Brian Jan 28 '15 at 09:06
  • 1
    @Brian - that would mean referencing a private class name in the code, which puts an app at risk of rejection. There's method in my madness! – Ashley Mills Jan 28 '15 at 09:23
14

Ashley Mills' answer was excellent at the time it was offered, but as others have noted in the comments, the view hierarchy has changed from version to version of iOS. In order to properly find the reorder control, I'm using an approach that traverses the entire view hierarchy; hopefully this will give the approach an opportunity to continue working even if Apple changes the view hierarchy.

Here's the code I'm using to find the reorder control:

-(UIView *) findReorderView:(UIView *) view
{
    UIView *reorderView = nil;
    for (UIView *subview in view.subviews)
    {
        if ([[[subview class] description] rangeOfString:@"Reorder"].location != NSNotFound)
        {
            reorderView = subview;
            break;
        }
        else
        {
            reorderView = [self findReorderView:subview];
            if (reorderView != nil)
            {
                break;
            }
        }
    }
    return reorderView;
}

And here's the code I'm using to override the -(void) setEditing:animated: method in my subclass:

-(void) setEditing:(BOOL)editing animated:(BOOL)animated
{
    [super setEditing:editing animated:animated];
    if (editing)
    {
        // I'm assuming the findReorderView method noted above is either
        // in the code for your subclassed UITableViewCell, or defined
        // in a category for UIView somewhere
        UIView *reorderView = [self findReorderView:self];
        if (reorderView)
        {
            // I'm setting the background color of the control
            // to match my cell's background color
            // you might need to do this if you override the
            // default background color for the cell
            reorderView.backgroundColor = self.contentView.backgroundColor;
            for (UIView *sv in reorderView.subviews)
            {
                // now we find the UIImageView for the reorder control
                if ([sv isKindOfClass:[UIImageView class]])
                {
                    // and replace it with the image we want
                    ((UIImageView *)sv).image = [UIImage imageNamed:@"yourImage.png"];
                    // note:  I have had to manually change the image's frame
                    // size to get it to display correctly
                    // also, for me the origin of the frame doesn't seem to
                    // matter, because the reorder control will center it
                    sv.frame = CGRectMake(0, 0, 48.0, 48.0);
                }
            }
        }
    }
}
Rick Morgan
  • 603
  • 9
  • 14
10

Swift 4

   // Change default icon (hamburger) for moving cells in UITableView
    func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
        let imageView = cell.subviews.first(where: { $0.description.contains("Reorder") })?.subviews.first(where: { $0 is UIImageView }) as? UIImageView

        imageView?.image = #imageLiteral(resourceName: "new_hamburger_icon") // give here your's new image
        imageView?.contentMode = .center

        imageView?.frame.size.width = cell.bounds.height
        imageView?.frame.size.height = cell.bounds.height
    }
Grzegorz R. Kulesza
  • 1,324
  • 16
  • 10
7

Swift version of Rick's answer with few improvements:

override func setEditing(editing: Bool, animated: Bool) {
    super.setEditing(editing, animated: animated)

    if editing {
        if let reorderView = findReorderViewInView(self), 
            imageView = reorderView.subviews.filter({ $0 is UIImageView }).first as? UIImageView {
            imageView.image = UIImage(named: "yourImage")
        }
    }
}

func findReorderViewInView(view: UIView) -> UIView? {
    for subview in view.subviews {
        if String(subview).rangeOfString("Reorder") != nil {
            return subview
        }
        else {
            findReorderViewInView(subview)
        }
    }
    return nil
}
Andrey Gordeev
  • 30,606
  • 13
  • 135
  • 162
5

Updated solution of Ashley Mills (for iOS 7.x)

if (editing) {
    UIView *scrollView = self.subviews[0];
    for (UIView * view in scrollView.subviews) {
        NSLog(@"Class: %@", NSStringFromClass([view class]));
        if ([NSStringFromClass([view class]) rangeOfString: @"Reorder"].location != NSNotFound) {
            for (UIView * subview in view.subviews) {
                if ([subview isKindOfClass: [UIImageView class]]) {
                    ((UIImageView *)subview).image = [UIImage imageNamed: @"moveCellIcon"];
                }
            }
        }
    }
}
OgreSwamp
  • 4,602
  • 4
  • 33
  • 54
4

I use editingAccessoryView to replace reorder icon.

  1. Make a subclass of UITableViewCell.
  2. Override setEditing. Simply hide reorder control and set editingAccessoryView to an uiimageview with your re-order image.
 - (void) setEditing:(BOOL)editing animated:(BOOL)animated
{

    [super setEditing: editing animated: YES];

    self.showsReorderControl = NO;

    self.editingAccessoryView = editing ? [[UIImageView alloc] initWithImage:[UIImage imageNamed:@"yourReorderIcon"]] : nil;

}

If you are not using editing accessory view, this may be a good choice.

Hubert Wang
  • 502
  • 5
  • 17
  • 2
    While this worked to show a custom editingAccessoryView, I couldn't actually move the cell by pressing and dragging on it. – jhelzer Aug 01 '17 at 15:16
  • 1
    Hmm...I don't remember where I used this.... i will update my answer when I found it. – Hubert Wang Aug 02 '17 at 15:51
4
- (void)tableView:(UITableView *)tableView willDisplayCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath
{
    for (UIControl *control in cell.subviews)
    {       
        if ([control isMemberOfClass:NSClassFromString(@"UITableViewCellReorderControl")] && [control.subviews count] > 0)
        {           
            for (UIControl *someObj in control.subviews)
            {
                if ([someObj isMemberOfClass:[UIImageView class]])
                {
                    UIImage *img = [UIImage imageNamed:@"reorder_icon.png"];
                    ((UIImageView*)someObj).frame = CGRectMake(0.0, 0.0, 43.0, 43.0);
                    ((UIImageView*)someObj).image = img;
                }
            }
        }
    }   
}
Padavan
  • 1,056
  • 1
  • 11
  • 32
  • 1
    This is likely to fail the app store approval process due to explicitly using the UITableViewCellReorderControl class name. – Ashley Mills Dec 22 '11 at 15:58
  • @Ashley Mills We use this in our app, which is in AppStore now. – Padavan Dec 23 '11 at 06:10
  • 1
    Good for you... but I wouldn't risk it myself! – Ashley Mills Dec 23 '11 at 09:27
  • This does not work since iOS 5 anymore (at least perfectly). willDisplayCell is not called immediately when you put cell in editing mode but when you finish dragging it. This means default reorder control is shown initially without change until you touch and move it to the other place in UITableView. It is being refreshed only when you drop it (and only then your control will be visible). – Lukasz Aug 31 '12 at 07:35
  • Technically this is not using undocumented APIs. You are using public API to traverse the view hierarchy and make changes as you see fit. The difference between this and using undocumented API is that if Apple changes the view hierarchy, this does not crash the app, it simply puts the original image back. As I understand it are undocumented APIs disallowed because they make apps crash once Apple changes them. I would vouch that this code is App Store Review safe. – Trenskow Dec 17 '12 at 19:18
2

I could not get any other answer to work for me, but I found a solution.

Grzegorz R. Kulesza's answer almost worked for me but I had to make a couple changes.

This works with Swift 5 and iOS 13:

// Change default reorder icon in UITableViewCell
func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
    let imageView = cell.subviews.first(where: { $0.description.contains("Reorder") })?.subviews.first(where: { $0 is UIImageView }) as? UIImageView
    imageView?.image = UIImage(named: "your_custom_reorder_icon.png")
    let size = cell.bounds.height * 0.6 // scaled for padding between cells
    imageView?.frame.size.width = size
    imageView?.frame.size.height = size
}
nickcin
  • 117
  • 4
1

I did this on iOS 12 with swift 4.2

I hope this helps:

    func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
        for view in cell.subviews {
            if view.self.description.contains("UITableViewCellReorderControl") {
                for sv in view.subviews {
                    if (sv is UIImageView) {
                        (sv as? UIImageView)?.image = UIImage(named: "your_image")
                        (sv as? UIImageView)?.contentMode = .center
                        sv.frame = CGRect(x: 0, y: 0, width: 25, height: 25)
                    }
                }
            }
        }
    }
meow2x
  • 2,056
  • 22
  • 27
1

After debuging the UITableViewCell, you can use KVC in UITableViewCell subclass to change it.

// key
static NSString * const kReorderControlImageKey = @"reorderControlImage";

// setting when cellForRow calling
UIImage *customImage;
[self setValue:customImage forKeyPath:kReorderControlImageKey];

// to prevent crash
- (void)setValue:(id)value forUndefinedKey:(NSString *)key {
    if ([key isEqualToString:kReorderControlImageKey]) return;
    else [super setValue:value forUndefinedKey:key];
}
bqlin
  • 11
  • 2
0

Swift 5 solution:

Subclass UITableViewCell and override didAddSubview method:

override func didAddSubview(_ subview: UIView) {
    if !subview.description.contains("Reorder") { return }  
    (subview.subviews.first as? UIImageView)?.removeFromSuperview()

    let imageView = UIImageView()
    imageView.image = UIImage()
    subview.addSubview(imageView)
        
    imageView.snp.makeConstraints { make in
        make.height.width.equalTo(24)
        make.centerX.equalTo(subview.snp.centerX)
        make.centerY.equalTo(subview.snp.centerY)
    }
}

I've used SnapKit to set constraints, you can do it in your way.

Please note, it could be temporary solution in order of Apple updates.

Arthur Stepanov
  • 511
  • 7
  • 4
0

Working with iOS 16 and Swift 5

I tried the above solution, but sometimes my custom image was not displayed in some cells. This code works fine for me into the UITableViewCell subclass:

private lazy var customReorderImgVw: UIImageView = {
    let img = UIImage(named: "imgCustomReorder")!
    let imgVw = UIImageView(image: img)
    imgVw.contentMode = .center
    imgVw.frame = CGRect(origin: .zero, size: img.size)
    imgVw.alpha = 0
    return imgVw
}()

override func setEditing(_ editing: Bool, animated: Bool) {
    super.setEditing(editing, animated: animated)
    if editing {
        for subVw in subviews {
            if "\(subVw.classForCoder)" == "UITableViewCellReorderControl" {
                subVw.subviews.forEach { $0.removeFromSuperview() }
                customReorderImgVw.center.y = subVw.center.y
                subVw.addSubview(customReorderImgVw)
                break
            }
        }
    }
    showOrHideCustomReorderView(isToShow: editing)
}

private func showOrHideCustomReorderView(isToShow: Bool) {
    let newAlpha: CGFloat = (isToShow ? 1 : 0)
    UIView.animate(withDuration: 0.25) {
        self.customReorderImgVw.alpha = newAlpha
    }
}
Miguel Gallego
  • 427
  • 4
  • 7
0

You can also simply add your own custom reorder view above all others inside your cell.

All you have to do is ensure this custom view is always above others, which can be checked in [UITableViewDelegate tableView: willDisplayCell: forRowAtIndexPath: indexPath:].

In order to allow the standard reorder control interaction, your custom view must have its userInteractionEnabled set to NO.

Depending on how your cell looks like, you might need a more or less complex custom reorder view (to mimic the cell background for exemple).

Rashad
  • 11,057
  • 4
  • 45
  • 73
Phil
  • 2,784
  • 2
  • 28
  • 26
  • Phil, how would you make that custom reorder view work as the built in one? Seems there is a clue for me there in the userInteractionEnabled note, but I don't get it. =) – PEZ Apr 21 '12 at 12:02
  • I tried this approach, adding a UIImageView for my custom icon to the cell's contentView and then moving its frame into position, and it worked. However, that required some hard-coded xy values that seemed likely to be inaccurate in some situations, which would result in a duplicate icon. Anyway, to answer PEZ's question, a UIImageView has its user interaction disabled by default, so my custom icon didn't interfere with the default functionality of the cell. – arlomedia Aug 31 '12 at 21:10