I'm getting some weird behavior (actually a crash) when I drag a cell outside of the CollectionView Bounds. How can I limit the user interaction of the CollectionView to only the frame of the CollectionView? When the user long-presses and drags a view cell, I only want them to be able to move the cell within the CollectionView, and not all over the screen
private void HandleLongPressOnCollection(UILongPressGestureRecognizer gesture)
{
switch (gesture.State)
{
case UIGestureRecognizerState.Began:
var myIndexPath = MyCollectionView.IndexPathForItemAtPoint(gesture.LocationInView(MyCollectionView));
this.selectedIndexPath = myIndexPath;
BeginInteractiveMovementForItem();
break;
case UIGestureRecognizerState.Changed: // This is not working correctly
if (MyCollectionView.Frame.Contains(gesture.LocationInView(MyCollectionView)))
{
MyCollectionView.UpdateInteractiveMovement(gesture.LocationInView(MyCollectionView));
}
break;
case UIGestureRecognizerState.Ended:
EndInteractiveMovementForItem();
break;
default:
MyCollectionView.CancelInteractiveMovement();
break;
}
}
private void BeginInteractiveMovementForItem()
{
if (selectedIndexPath != null)
{
MyCollectionView.BeginInteractiveMovementForItem(selectedIndexPath);
var cell = MyCollectionView.CellForItem(selectedIndexPath) as CustomViewCell;
cell.MarkCellAsMoving();
}
}
private void EndInteractiveMovementForItem()
{
if (selectedIndexPath != null)
{
var cell = MyCollectionView.CellForItem(selectedIndexPath) as CustomViewCell;
cell?.SetToNormalState();
}
MyCollectionView.EndInteractiveMovement();
}
You should compare the gesture's location with ContentView not Framelike:
case UIGestureRecognizerState.Changed:
CGPoint gesturePoint = gesture.LocationInView(MyCollectionView);
CGSize contentSize = MyCollectionView.ContentSize;
if (gesturePoint.X > 0 && gesturePoint.X < contentSize.Width &&
gesturePoint.Y > 0 && gesturePoint.Y < contentSize.Height)
{
MyCollectionView.UpdateInteractiveMovement(gesture.LocationInView(MyCollectionView));
}
break;
Adjust the constant in the if statement to feed your requirement.
Moreover the crash may occur in the event MoveItem() when you call MyCollectionView.EndInteractiveMovement();. You can try to post some code about it.
You can use
collectionView.bounces = false
if you want to drag until the edge.
Can you also post the crash you are getting?
I want to scale the collectionview when longpress and dragged and when user end drag then cell should come in regular size.
I am creating demo using below steps which works fine but enlarge is not working as I expected.
Collection View Example
Here is the gesture code I used :
func handleLongGesture(gesture: UILongPressGestureRecognizer) {
switch(gesture.state) {
case UIGestureRecognizerState.Began:
guard let selectedIndexPath = self.collectionView.indexPathForItemAtPoint(gesture.locationInView(self.collectionView)) else {
break
}
collectionView.beginInteractiveMovementForItemAtIndexPath(selectedIndexPath)
case UIGestureRecognizerState.Changed:
collectionView.updateInteractiveMovementTargetPosition(gesture.locationInView(gesture.view!))
case UIGestureRecognizerState.Ended:
collectionView.endInteractiveMovement()
default:
collectionView.cancelInteractiveMovement()
}
}
Try this on a collection view layout sub-class:
- (UICollectionViewLayoutAttributes*) layoutAttributesForInteractivelyMovingItemAtIndexPath:(NSIndexPath *)indexPath withTargetPosition:(CGPoint)position
{
UICollectionViewLayoutAttributes *attributes = [super layoutAttributesForInteractivelyMovingItemAtIndexPath:indexPath withTargetPosition:position];
attributes.zIndex = NSIntegerMax;
attributes.transform3D = CATransform3DScale(attributes.transform3D, 1.2f, 1.2f, 1.0);
return attributes;
}
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.
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.
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
}
}