iOS UITableViewCell setSelected:animated: always has animated = NO - ios

I'm trying to make my own selection animation. I've created a subclass of UITableViewCell. I do my selection animation in -setSelected:animated: method. It works as intended when you select or deselect cells by tapping them. Problem is that animation is also seen during scrolling, since -setSelected:animated: is called on each cell before it appears. This is how reusing cells mechanism works, I get it. What I don't get is that it always calls this method with animated = NO either on tap or on scroll. This seems like a logic mistake to me. I presumed it was supposed to select cells with animation when you tap them and without animation when reused cell appears. Is animated parameter even ever used anywhere except manual calls? Here's my code:
- (void)setSelected:(BOOL)selected animated:(BOOL)animated {
BOOL alreadySelected = (self.isSelected) && (selected);
BOOL alreadyDeselected = (!self.isSelected) && (!selected);
[super setSelected:selected animated:animated];
if ((alreadySelected) || (alreadyDeselected)) return;
NSLog(#"Animated selection: %#", animated ? #"YES" : #"NO");
NSTimeInterval duration;
if (animated) {
duration = 0.25;
} else {
duration = 0.0;
}
[CATransaction begin];
[CATransaction setAnimationDuration:duration];
if (selected) {
//layer properties are changed here...
} else {
//layer properties are changed here...
}
[CATransaction commit];
}
This always goes without the animation.
I can't think of any other such an easy way to handle custom selection. Implementing -didSelectRow methods in controller seems so much worse and it's not called during scrolling, so reused cells will appear in a wrong state. Any idea how to fix this?
UPDATE:
I've found a temporary solution:
#pragma mark - Table View Delegate
- (NSIndexPath *)tableView:(UITableView *)tableView willSelectRowAtIndexPath:(NSIndexPath *)indexPath {
UITableViewCell *cell = [tableView cellForRowAtIndexPath:indexPath];
[cell setSelected:YES animated:YES];
return indexPath;
}
- (NSIndexPath *)tableView:(UITableView *)tableView willDeselectRowAtIndexPath:(NSIndexPath *)indexPath {
UITableViewCell *cell = [tableView cellForRowAtIndexPath:indexPath];
[cell setSelected:NO animated:YES];
return indexPath;
}
It does work, but I don't like it. The fact that TableView's delegate has to know something about selection and it's not all contained in one place bugs me a lot. And -setSelected is called twice when taping a row - with and without animation.

This is how table views were designed to work. When you select it it highlights immediately, so no animation is needed. However, you will (you should, anyway) see the table view cell deselect with animation when you pop back to the table view. You can see that by using this code in the -viewWillAppear:override:
[self.tableView deselectRowAtIndexPath:[self.tableView indexPathForSelectedRow] animated:animated]
That will happen for you automatically if you are using a UITableViewController and have its clearsSelectionOnViewWillAppear property to YES.
If you want a different behavior, you need to code it yourself. If the code you posted here is working to your liking, keep it. You could also modify the cell subclass to always pass YES to the superclass in the -setSelected:animated: method override.

Although being late in the discussion, here's a simple tweak to add in your cell code:
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
super.touchesEnded(touches, with: event)
// Whatever you need to do here
DispatchQueue.main.asyncAfter(deadline: .now() + 100.milliseconds) { self.setSelected(false, animated: true) }
}
Using touchesEnded instead of setSelected does the trick, either on the iPad or the iPhone.

I found a little workaround with prepareForReuse method which is called every time before the cell is reused:
class MyTableViewCell: UITableViewCell {
private var shouldAnimate = false
override init(style: UITableViewCellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
selectionStyle = .none
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func prepareForReuse() {
shouldAnimate = false
}
override func setSelected(_ selected: Bool, animated: Bool) {
super.setSelected(selected, animated: animated)
if shouldAnimate {
//your animation
}
else {
shouldAnimate = true
}
}
Hope it helps.

Related

tableView.scrollToRow(...) with callback [duplicate]

This is a follow-up to How to get notified when a tableViewController finishes animating the push onto a nav stack.
In a tableView I want to deselect a row with animation, but only after the tableView has finished animating the scroll to the selected row. How can I be notified when that happens, or what method gets called the moment that finishes.
This is the order of things:
Push view controller
In viewWillAppear I select a certain row.
In viewDidAppear I scrollToRowAtIndexPath (to the selected row).
Then when that finishes scrolling I want to deselectRowAtIndexPath: animated:YES
This way, the user will know why they were scrolled there, but then I can fade away the selection.
Step 4 is the part I haven't figured out yet. If I call it in viewDidAppear then by the time the tableView scrolls there, the row has been deselected already which is no good.
You can use the table view delegate's scrollViewDidEndScrollingAnimation: method. This is because a UITableView is a subclass of UIScrollView and UITableViewDelegate conforms to UIScrollViewDelegate. In other words, a table view is a scroll view, and a table view delegate is also a scroll view delegate.
So, create a scrollViewDidEndScrollingAnimation: method in your table view delegate and deselect the cell in that method. See the reference documentation for UIScrollViewDelegate for information on the scrollViewDidEndScrollingAnimation: method.
try this
[UIView animateWithDuration:0.3 animations:^{
[yourTableView scrollToRowAtIndexPath:indexPath
atScrollPosition:UITableViewScrollPositionTop
animated:NO];
} completion:^(BOOL finished){
//do something
}];
Don't forget to set animated to NO, the animation of scrollToRow will be overridden by UIView animateWithDuration.
Hope this help !
To address Ben Packard's comment on the accepted answer, you can do this. Test if the tableView can scroll to the new position. If not, execute your method immediately. If it can scroll, wait until the scrolling is finished to execute your method.
- (void)someMethod
{
CGFloat originalOffset = self.tableView.contentOffset.y;
[self.tableView scrollToRowAtIndexPath:path atScrollPosition:UITableViewScrollPositionMiddle animated:NO];
CGFloat offset = self.tableView.contentOffset.y;
if (originalOffset == offset)
{
// scroll animation not required because it's already scrolled exactly there
[self doThingAfterAnimation];
}
else
{
// We know it will scroll to a new position
// Return to originalOffset. animated:NO is important
[self.tableView setContentOffset:CGPointMake(0, originalOffset) animated:NO];
// Do the scroll with animation so `scrollViewDidEndScrollingAnimation:` will execute
[self.tableView scrollToRowAtIndexPath:path atScrollPosition:UITableViewScrollPositionMiddle animated:YES];
}
}
- (void)scrollViewDidEndScrollingAnimation:(UIScrollView *)scrollView
{
[self doThingAfterAnimation];
}
You can include the scrollToRowAtIndexPath: inside a [UIView animateWithDuration:...] block which will trigger the completion block after all included animations conclude. So, something like this:
[UIView
animateWithDuration:0.3f
delay:0.0f
options:UIViewAnimationOptionAllowUserInteraction
animations:^
{
// Scroll to row with animation
[self.tableView scrollToRowAtIndexPath:indexPath
atScrollPosition:UITableViewScrollPositionTop
animated:YES];
}
completion:^(BOOL finished)
{
// Deselect row
[self.tableView deselectRowAtIndexPath:indexPath animated:YES];
}];
Swift 5
The scrollViewDidEndScrollingAnimation(_ scrollView: UIScrollView) delegate method is indeed the best way to execute a completion on a scroll-to-row animation but there are two things worth noting:
First, the documentation incorrectly says that this method is only called in response to setContentOffset and scrollRectToVisible; it's also called in response to scrollToRow (https://developer.apple.com/documentation/uikit/uiscrollviewdelegate/1619379-scrollviewdidendscrollinganimati).
Second, despite the fact that the method is called on the main thread, if you're running a subsequent animation here (one after the scroll has finished), it will still hitch (this may or may not be a bug in UIKit). Therefore, simply dispatch any follow-up animations back onto the main queue which just ensures that the animations will begin after the end of the current main task (which appears to include the scroll-to-row animation). Doing this will give you the appearance of a true completion.
func scrollViewDidEndScrollingAnimation(_ scrollView: UIScrollView) {
DispatchQueue.main.async {
// execute subsequent animation here
}
}
Implementing this in a Swift extension.
//strong ref required
private var lastDelegate : UITableViewScrollCompletionDelegate! = nil
private class UITableViewScrollCompletionDelegate : NSObject, UITableViewDelegate {
let completion: () -> ()
let oldDelegate : UITableViewDelegate?
let targetOffset: CGPoint
#objc private func scrollViewDidEndScrollingAnimation(scrollView: UIScrollView) {
scrollView.delegate = oldDelegate
completion()
lastDelegate = nil
}
init(completion: () -> (), oldDelegate: UITableViewDelegate?, targetOffset: CGPoint) {
self.completion = completion
self.oldDelegate = oldDelegate
self.targetOffset = targetOffset
super.init()
lastDelegate = self
}
}
extension UITableView {
func scrollToRowAtIndexPath(indexPath: NSIndexPath, atScrollPosition scrollPosition: UITableViewScrollPosition, animated: Bool, completion: () -> ()) {
assert(lastDelegate == nil, "You're already scrolling. Wait for the last completion before doing another one.")
let originalOffset = self.contentOffset
self.scrollToRowAtIndexPath(indexPath, atScrollPosition: scrollPosition, animated: false)
if originalOffset.y == self.contentOffset.y { //already at the right position
completion()
return
}
else {
let targetOffset = self.contentOffset
self.setContentOffset(originalOffset, animated: false)
self.delegate = UITableViewScrollCompletionDelegate(completion: completion, oldDelegate: self.delegate, targetOffset:targetOffset)
self.scrollToRowAtIndexPath(indexPath, atScrollPosition: scrollPosition, animated: true)
}
}
}
This works for most cases although the TableView delegate is changed during the scroll, which may be undesired in some cases.

UICollectionView Segues and Multiple Selection on same UICollectionViewCell

I have a UICollectionView. If I touch a cell, it triggers a segue. If "trash" or "save" is enabled, then users should be able to touch cells to add to an array that is processed for the corresponding action.
When trash/save is enabled, the segue triggers instead of allowing multiple selection. How do I do this so that I can have 2 modes: 1 for segues and 1 for multiple selection.
-(void)collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPath:(NSIndexPath *)indexPath{
if (self.saveEnabled == YES) {
NSArray *itemsToDelete = [self.collectionView indexPathsForSelectedItems];
[self.itemsArray addObjectsFromArray:itemsToDelete];
}
else if (self.trashEnabled == YES) {
NSArray *itemsToDelete = [self.collectionView indexPathsForSelectedItems];
[self.itemsArray addObjectsFromArray:itemsToDelete];
}
else{
[self performSegueWithIdentifier:#"collectionUnwind" sender:self];
}
}
The key step is that you need return false in shouldPerformSegueWithIdentifier when in multi-selection mode.
Here is how I do this in swift:
override func shouldPerformSegue(withIdentifier identifier: String, sender: Any?) -> Bool {
return !self.collectionView.allowsMultipleSelection
}
And I switch between single-select and multi-select mode with long press, you can use button to do the same.
func setupLongPressGesture() {
let longPressGesture:UILongPressGestureRecognizer = UILongPressGestureRecognizer(target: self, action: #selector(self.handleLongPress))
longPressGesture.minimumPressDuration = 1.0 // 1 second press
longPressGesture.delegate = self
self.collectionView.addGestureRecognizer(longPressGesture)
}
#objc func handleLongPress(_ gestureRecognizer: UILongPressGestureRecognizer){
if gestureRecognizer.state == .began {
self.labelState.text = "multiple selection enabled"
} else if gestureRecognizer.state == .ended {
self.collectionView.allowsMultipleSelection = !self.collectionView.allowsMultipleSelection
}
}
I refer to the tutorial here: https://www.appcoda.com/ios-collection-view-tutorial/
Not sure of what you really want. Tell us exactly what you do (buttons) and in which order to delete (or save) multiple cells.
By the way did you enable multiple selection on your collection view ?
[self.collectionView setAllowsMultipleSelection:YES];
EDIT :
In your storyboard, just add a segue with "collectionUnwind" identifier from your current ViewController (and not the cells of its CollectionView) to the new ViewController you want to push. If you link it to the cells, Xcode will assume you need the new ViewController to be pushed on every cell selection.
Your current code should do the rest.

How to properly override reloadRowsAtIndexPaths:withRowAnimation?

I would like to create a special animation for when I'm reloading the rows of a UITableView. The thing is I don't want to use this animation all the time, as I am sometimes using built-in animation for reloading the rows.
So how can I do this? By overriding reloadRowsAtIndexPaths:withRowAnimation in my own UITableView implementation? How?
Or maybe there is a better way to get my own animation while reloading a row?
I think you should not override reloadRowsAtIndexPaths:withRowAnimation. Just implement a custom method reloadRowsWithMyAnimationAtIndexPaths:in UITableView's category and use it when it is needed.
But if you want to override this method in UITableView's subclass, you could do this:
- (void)reloadRowsAtIndexPaths:(NSArray *)indexPaths
withRowAnimation:(UITableViewRowAnimation)animation {
if (self.useMyAnimation)
[self reloadRowsWithMyAnimationAtIndexPaths:indexPaths];
else
[super reloadRowsAtIndexPaths:indexPaths withRowAnimation:animation];
}
self.useMyAnimation is just a flag (BOOL property) that indicates what animation to use. Set this flag before reload operation.
For 2 or more customAnimations you could implement enum:
enum MyTableViewReloadAnimationType {
case None
case First
case Second
case Third
}
Then make a MyTableViewReloadAnimationType property (for example, reloadAnimationType) and select an appropriate animation method with switch:
var reloadAnimationType = MyTableViewReloadAnimationType.None
override func reloadRowsAtIndexPaths(indexPaths: [AnyObject], withRowAnimation animation: UITableViewRowAnimation) {
switch self.reloadAnimationType {
case .None:
super .reloadRowsAtIndexPaths(indexPaths, withRowAnimation:animation)
default:
self .reloadRowsAtIndexPaths(indexPaths, withCustomAnimationType:self.reloadAnimationType)
}
}
func reloadRowsAtIndexPaths(indexPaths: [AnyObject], withCustomAnimationType animationType: MyTableViewReloadAnimationType) {
switch animationType {
case .First:
self .reloadRowsWithFirstAnimationAtIndexPaths(indexPaths)
case .Second:
self .reloadRowsWithSecondAnimationAtIndexPaths(indexPaths)
case .Third:
self .reloadRowsWithThirdAnimationAtIndexPaths(indexPaths)
}
}
You could call the custom method reloadRowsAtIndexPaths:withCustomAnimationType: directly:
self.tableView .reloadRowsAtIndexPaths([indexPath], withCustomAnimationType: MyTableViewReloadAnimationType.First)
Inside the custom method you need to get a current cell and a new cell with method of dataSource:
func reloadRowsWithFirstAnimationAtIndexPaths(indexPaths: [AnyObject]) {
for indexPath in indexPaths {
var currentCell = self .cellForRowAtIndexPath(indexPath as! NSIndexPath)
var newCell = self.dataSource .tableView(self, cellForRowAtIndexPath: indexPath as! NSIndexPath)
var newCellHeight = self.delegate .tableView(self, heightForRowAtIndexPath: indexPath)
var frame: CGRect = currentCell.frame
frame.size.height = newCellHeight
newCell.frame = frame
self .replaceCellWithFirstAnimation(currentCell!, withAnotherCell: newCell);
}
}
func replaceCellWithFirstAnimation(firstCell : UITableViewCell, withAnotherCell secondCell: UITableViewCell) {
var cellsSuperview = firstCell.superview!
//make this with animation
firstCell .removeFromSuperview()
cellsSuperview .addSubview(secondCell)
}
You need to handle a situation when newCell's height > or < then currentCell's height. All other cells frames must be recalculated. I think it could be done with beginUpdates and endUpdates methods. Call them before the manipulation with cells.

How to hide shadows in UITableViewCell when cell is dragging

I have UITableView with hided separator line, and when I dragging cell, shadows appears some kind as borders comes out on up and down. How to hide this? Please see example:
Great thanks!
So, I have answer, just subclass of UITableView with method:
- (void) didAddSubview:(UIView *)subview
{
[super didAddSubview:subview];
if([subview.class.description isEqualToString:#"UIShadowView"]) {
subview.hidden = YES;
}
}
NoShadowTableView.m
#import "NoShadowTableView.h"
#interface NoShadowTableView ()
{
// iOS7
__weak UIView* wrapperView;
}
#end
#implementation NoShadowTableView
- (void) didAddSubview:(UIView *)subview
{
[super didAddSubview:subview];
// iOS7
if(wrapperView == nil && [[[subview class] description] isEqualToString:#"UITableViewWrapperView"])
wrapperView = subview;
// iOS6
if([[[subview class] description] isEqualToString:#"UIShadowView"])
[subview setHidden:YES];
}
- (void) layoutSubviews
{
[super layoutSubviews];
// iOS7
for(UIView* subview in wrapperView.subviews)
{
if([[[subview class] description] isEqualToString:#"UIShadowView"])
[subview setHidden:YES];
}
}
#end
Quick hack is subclassing UITableViewCell and add method:
override func layoutSubviews() {
super.layoutSubviews()
superview?.subviews.filter({ "\(type(of: $0))" == "UIShadowView" }).forEach { (sv: UIView) in
sv.removeFromSuperview()
}
}
This code works for me!
import UIKit
class NoShadowTableView: UITableView {
override func didAddSubview(_ subview: UIView) {
super.didAddSubview(subview)
if "\(type(of: subview))" == "UIShadowView" {
subview.removeFromSuperview()
}
}
}
I was facing a similar problem by using default UITableView reordering controls. So I used this external third-party library which solved my problem.
https://github.com/shusta/ReorderingTableViewController
Hope this helps
Swift 3 implementation (removed iOS6 support)
import UIKit
class NoShadowTableView: UITableView {
weak var wrapperView: UIView?
override func didAddSubview(_ subview: UIView) {
super.didAddSubview(subview)
if wrapperView == nil && "\(type(of: subview))" == "UITableViewWrapperView" {
wrapperView = subview
}
}
override func layoutSubviews() {
super.layoutSubviews()
wrapperView?.subviews.forEach({ view in
if "\(type(of: view))" == "UIShadowView" {
view.isHidden = true
}
})
}
}
For me hacks with the UIShadowView didn't work. I checked the solution on iOS 10. But one line in a cell class has done the trick:
override func layoutSubviews() {
super.layoutSubviews()
self.subviews.filter{ $0 is UIImageView }.forEach { $0.isHidden = true }
}
Swift 4 solution; use extended uitableviewcontroller because now shadow is in the table and only added when cell is moved.
class UITableViewControllerEx: UITableViewController {
override func viewWillLayoutSubviews() {
super.viewWillLayoutSubviews()
view.subviews.filter({ String(describing: type(of: $0)) == "UIShadowView" }).forEach { (sv: UIView) in
sv.isHidden = true
}
}
}
All of the other answers appear to be using private APIs which is obviously not a good thing to do. Additionally, they don't appear to work anymore (at least not on iOS 16).
I found that using UIDragPreviewParameters allowed me to get rid of the horrible background colour and reshape the shadow to fit one of the subviews in my table view cell.
-(UIDragPreviewParameters *)tableView:(UITableView *)tableView dragPreviewParametersForRowAtIndexPath:(nonnull NSIndexPath *)indexPath {
return [self createParametersForTableView:tableView atIndexPath:indexPath];
}
-(UIDragPreviewParameters *)tableView:(UITableView *)tableView dropPreviewParametersForRowAtIndexPath:(NSIndexPath *)indexPath {
return [self createParametersForTableView:tableView atIndexPath:indexPath];
}
-(UIDragPreviewParameters *)createParametersForTableView:(UITableView *)tableView atIndexPath:(NSIndexPath *)indexPath {
// Get the selected table view cell.
CustomCell *cell = [tableView cellForRowAtIndexPath:indexPath];
// Create the dragged cell preview parameters.
UIDragPreviewParameters *parameters = [[UIDragPreviewParameters alloc] init];
[parameters setBackgroundColor:[UIColor clearColor]];
[parameters setVisiblePath:[UIBezierPath bezierPathWithRoundedRect:cell.customSubView.frame cornerRadius:22.2]];
[parameters setShadowPath:[UIBezierPath bezierPathWithRoundedRect:cell.customSubView.frame cornerRadius:22.2]];
return parameters;
}
If you use this method, you will need to implement two delegates: UITableViewDragDelegate and UITableViewDropDelegate.
[yourTableView setDragDelegate:self];
[yourTableView setDropDelegate:self];
The drag delegate method will allow you to control the preview background when dragging the cell. When dropping the cell, you need to use the drop delegate method to avoid the preview background going back to the default white background.

Change default icon for moving cells in UITableView

I need to change default icon for moving cells in UITableView.
This one:
Is it possible?
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' 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);
}
}
}
}
}
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
}
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
}
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"];
}
}
}
}
}
- (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;
}
}
}
}
}
I use editingAccessoryView to replace reorder icon.
Make a subclass of UITableViewCell.
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.
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
}
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)
}
}
}
}
}
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];
}
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).
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.
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
}
}

Resources