UICollectionView 'interactive movement' is not letting to drop the cell as a last cell in the target section - drag-and-drop

I am using UICollectionView with the minimum development target of iOS9 and found that can support drag and drop in terms of interactive movement. Everything works fine but the dropping the cell as the last cell in the target section is not working.
NB: I don't want the iOS11 drag and drop solution, because my App targets iOS9 onwards. Creating a dummy cell at the end of every section is not a straight forward solution.
Please see the image to get the problem visually.
Thanks in advance.

I have encountered the same issue, and finally managed to get it done. I used Apple documentation and multiple blog post about UICollectionView to build a fully functional Drag'N Drop feature between sections and even on invalid indexPaths.
Implementing default Drag'N Drop
First of all you have to disable the automatic Drag'N Drop feature offered by Apple, because you will be using your own GestureRecognizer.
So, add this in your viewDidLoad :
// deactivate default gesture recognizer for drag'n drop
self.installsStandardGestureForInteractiveMovement = NO;
Add a longPressGestureRecognizer to your collectionView, add the UIGestureRecognizerDelegate to your viewController protocols, and set the delegate in your viewDidLoad :
_longPressGestureRecognizer.delegate = self;
You now have to implement the longPressGestureRecognizer action :
// static variables to manage drag'n drop on invalid spaces
static NSIndexPath * longPressIndexPath = nil;
static CGPoint longPressLocation = (CGPoint) {0, 0};
- (IBAction)longPress:(UILongPressGestureRecognizer *)sender {
#autoreleasepool {
static BOOL isDragNDropping = NO;
longPressLocation = [sender locationInView:self.collectionView];
longPressIndexPath = [self.collectionView indexPathForItemAtPoint:longPressLocation];
// get the cell at indexPath (the one you long pressed)
UICollectionViewCell* cell = nil;
if (sender.state == UIGestureRecognizerStateChanged) {
NSLog(#"%s : STATE CHANGED", __FUNCTION__);
// Do stuff with cell
// Update the cell movement if the longPress location changes
if (isDragNDropping) {
[self.collectionView updateInteractiveMovementTargetPosition:longPressLocation];
} else {
// It's the first longPress movement, start the drag'n drop
if (longPressIndexPath) {
[self.collectionView beginInteractiveMovementForItemAtIndexPath:longPressIndexPath];
isDragNDropping = YES;
}
}
return;
}
if (sender.state == UIGestureRecognizerStateRecognized) {
NSLog(#"%s : STATE RECOGNIZED", __FUNCTION__);
// do stuff with the cell
// We were long pressing on a cell
if (isDragNDropping) {
[self.collectionView endInteractiveMovement];
}
isDragNDropping = NO;
return;
}
if (sender.state == UIGestureRecognizerStateBegan) {
NSLog(#"%s : STATE BEGAN", __FUNCTION__);
// do stuff with the newly selected cell
if (longPressIndexPath) {
cell = [self.collectionView cellForItemAtIndexPath: longPressIndexPath];
// You can add custom animations on the cell
[UIView animateWithDuration:0.35f animations:^{
[cell setTransform:CGAffineTransformMakeScale(1.2f, 1.2f)];
cell.layer.shadowColor = [UIColor blackColor].CGColor;
cell.layer.shadowRadius = 6.f;
cell.layer.shadowOpacity = 0.6f;
} completion:^(BOOL finished) {
}];
}
isDragNDropping = NO;
return;
}
}
}
Basically, at this point you have done the default Drag'N Drop that Apple was doing.
Bypassing the invalid indexPaths
The main thing is to implement this callback :
(NSIndexPath *)collectionView:(UICollectionView *)collectionView targetIndexPathForMoveFromItemAtIndexPath:(NSIndexPath *)originalIndexPath toProposedIndexPath:(NSIndexPath *)proposedIndexPath
It allows you to bypass the proposed indexPath to one of your choice.
You also have to implement this to keep track of the reordering :
(In case you are using CoreData and NSFetchedResultsController as your DataSource, you need to use a ranking to sort your items and this rank would have to be changed here)
-(void)collectionView:(UICollectionView *)collectionView moveItemAtIndexPath:(NSIndexPath *)sourceIndexPath toIndexPath:(NSIndexPath *)destinationIndexPath
Now the main problem is that you want to insert in an invalid indexPath (since it does not exists yet).
To workaround that, you have to keep a record of the longPressGestureRecognizer location and indexPath. I have declared these 2 variables as static but ViewController's property would do as well.
The key here is to generate virtual indexPaths at your longPressGestureRecognizerlocation instead of the proposed indexPath like this :
- (NSIndexPath *)collectionView:(UICollectionView *)collectionView targetIndexPathForMoveFromItemAtIndexPath:(NSIndexPath *)originalIndexPath toProposedIndexPath:(NSIndexPath *)proposedIndexPath
{
NSLog(#"%s", __FUNCTION__);
NSLog(#"original idxPath : %li - %li | proposed idxPath : %li - %li", originalIndexPath.section, originalIndexPath.item, proposedIndexPath.section, proposedIndexPath.item);
// The longPressIndexPath is valid, we are drag'n dropping on an valid location, keep the proposedIndexPath
if (longPressIndexPath != nil) {
return proposedIndexPath;
}
// The longPressIndexPath is nil, which means we are drag'n dropping on a new location (at the end of a section in most cases)
if (longPressIndexPath == nil) {
NSLog(#"%s virtualIndexPath", __FUNCTION__);
// This part is not actually needed,
// I have added a way to accept drops on header/footer, and add the item to the corresponding section
// Build an set with each sectionIndexPaths
NSMutableSet<NSIndexPath*>* visiblesSectionIndexPaths = [NSMutableSet setWithArray:[self.collectionView indexPathsForVisibleSupplementaryElementsOfKind:UICollectionElementKindSectionHeader]];
[visiblesSectionIndexPaths addObjectsFromArray:[self.collectionView indexPathsForVisibleSupplementaryElementsOfKind:UICollectionElementKindSectionFooter]];
// Find the section containing the gesture location
// Custom method that return the index of the section containing the given point, -1 if no section were found
NSInteger sectionIndex = [self sectionIndexContainingPoint:longPressLocation];
if (sectionIndex > -1) {
// If so, generate a virtual indexPath, where we can drop this item
// The management of the indexPath in the dataSource will happen in the moveItemAtIndexPath callback
// We return a new indexPath
int itemIndex = [self.collectionView numberOfItemsInSection:sectionIndex];
if (itemIndex<0) {
itemIndex=0;
}
// Generate a virtual indexPath
NSIndexPath * virtualIndexPath;
virtualIndexPath = [NSIndexPath indexPathForItem:itemIndex inSection:sectionIndex];
NSLog(#"virtual idxPath : %li - %li", virtualIndexPath.section, virtualIndexPath.item);
return virtualIndexPath;
}
return proposedIndexPath;
}
}
And finally, you implement the moveItemAtIndexPath method
You have to change the section your item belongs to in your backing data container, and call a reloadDatato update your collectionView. This part is entirely dependent on your dataSource, I have done it myself with CoreData and NSFetchedResultsController.
Hope this will help.
N.B.: this is mostly chunks of code extracted from my own implementation, I'm not sure it will work as is, but you have the keys to make it roll.

Related

Create infinite scroll effect by inserting new rows [duplicate]

How do I do an infinite scrolling in a UITableView? I know how to do it using a UIScrollView, in which apple has demonstrated in one of the WWDC's video. I tried doing the following in tableView:cellForRowAtIndexPath::
if (indexPath.row == [self.newsFeedData_ count] - 1)
{
[self.newsFeedData_ addObjectsFromArray:self.newsFeedData_];
[self.tableView reloadData];
}
but this fails. Any other idea?
If you need to know when you hit the bottom of the UITableView, become it's delegate (because it is a subclass of UIScrollView), and use the -scrollViewDidScroll: delegate method to compare the table's content height and it's actual scroll position.
EDIT (something like this):
- (void)scrollViewDidScroll:(UIScrollView *)scrollView_
{
CGFloat actualPosition = scrollView_.contentOffset.y;
CGFloat contentHeight = scrollView_.contentSize.height - (someArbitraryNumber);
if (actualPosition >= contentHeight) {
[self.newsFeedData_ addObjectsFromArray:self.newsFeedData_];
[self.tableView reloadData];
}
}
You can support infinite scroll with pull to refresh at the top and/or scroll continuously at the bottom with a spinner wheel using:
https://github.com/samvermette/SVPullToRefresh
SVPullToRefresh handles the logic when UITableView reaches the bottom. A spinner is shown automatically and a callback block is fired. You add in your business logic to the callback block.
Here's an example:
#import "UIScrollView+SVInfiniteScrolling.h"
// ...
[tableView addInfiniteScrollingWithActionHandler:^{
// append data to data source, insert new cells at the end of table view
// call [tableView.infiniteScrollingView stopAnimating] when done
}];
This project can be added to your project using CocoaPods or directly compiled into your project.
Here's a very quick and complete demo of an infinite scrolling UITableView I put together...
#interface InfiniteScrollViewController ()
#property (nonatomic) NSMutableArray *tableViewData;
#property (nonatomic) BOOL loadingMoreTableViewData;
#end
#implementation InfiniteScrollViewController
- (void)viewDidLoad {
self.tableViewData = [[NSMutableArray alloc] init];
[self addSomeMoreEntriesToTableView];
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
return self.tableViewData.count + 1;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
static NSString *CellIdentifier = #"Cell";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
if (cell == nil) {
cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier];
}
if (indexPath.row < self.tableViewData.count) {
cell.textLabel.text = [self.tableViewData objectAtIndex:indexPath.row];
} else {
cell.textLabel.text = #"Loading more data...";
// User has scrolled to the bottom of the list of available data so simulate loading some more if we aren't already
if (!self.loadingMoreTableViewData) {
self.loadingMoreTableViewData = YES;
[self performSelector:#selector(addSomeMoreEntriesToTableView) withObject:nil afterDelay:5.0f];
}
}
return cell;
}
- (void)addSomeMoreEntriesToTableView {
int loopTill = self.tableViewData.count + 20;
while (self.tableViewData.count < loopTill) {
[self.tableViewData addObject:[NSString stringWithFormat:#"%i", self.tableViewData.count]];
};
self.loadingMoreTableViewData = NO;
[self.tableView reloadData];
}
#end
'UITableView' is same as 'UIScrollView' in 'scrollViewDidScroll' method.
So, its easy to emulate infinite scrolling.
double the array so that head and tail are joined together to emulate circular table
use my following code to make user switch between 1st part of doubled table and 2nd part of doubled table when they tend to reach the start or the end of the table.
:
/* To emulate infinite scrolling...
The table data was doubled to join the head and tail: (suppose table had 1,2,3,4)
1 2 3 4|1 2 3 4 (actual data doubled)
---------------
1 2 3 4 5 6 7 8 (visualising joined table in eight parts)
When the user scrolls backwards to 1/8th of the joined table, user is actually at the 1/4th of actual data, so we scroll instantly (we take user) to the 5/8th of the joined table where the cells are exactly the same.
Similarly, when user scrolls to 6/8th of the table, we will scroll back to 2/8th where the cells are same. (I'm using 6/8th when 7/8th sound more logical because 6/8th is good for small tables.)
In simple words, when user reaches 1/4th of the first half of table, we scroll to 1/4th of the second half, when he reaches 2/4th of the second half of table, we scroll to the 2/4 of first half. This is done simply by subtracting OR adding half the length of the new/joined table.
*/
-(void)scrollViewDidScroll:(UIScrollView *)scrollView_
{
CGFloat currentOffsetX = scrollView_.contentOffset.x;
CGFloat currentOffSetY = scrollView_.contentOffset.y;
CGFloat contentHeight = scrollView_.contentSize.height;
if (currentOffSetY < (contentHeight / 8.0)) {
scrollView_.contentOffset = CGPointMake(currentOffsetX,(currentOffSetY + (contentHeight/2)));
}
if (currentOffSetY > ((contentHeight * 6)/ 8.0)) {
scrollView_.contentOffset = CGPointMake(currentOffsetX,(currentOffSetY - (contentHeight/2)));
}
}
P.S. - I've used this code on one of my apps called NT Time Table (Lite). If you want the preview, you can check out the app: https://itunes.apple.com/au/app/nt-time-table-lite/id528213278?mt=8
If your table can sometimes be too short, at the beginning of the above method you can add a if logic to exit the method when data count is say for example less than 9.
For me worked better scrollViewDidEndDragging: than scrollViewDidScroll:.
The second approach will send you each position during scroll and cause, if you are fetching remote resources you will hit your endpoint several times, which is not good.
Complete example based on #codafi solution with comments from #danielgomezrico about how to calculate contentHeight:
- (void)scrollViewDidEndDragging:(UIScrollView *)scrollView
willDecelerate:(BOOL)decelerate {
CGFloat actualPosition = scrollView.contentOffset.y;
CGFloat contentHeight = scrollView.contentSize.height - (self.tableView.frame.size.height);
if (actualPosition >= contentHeight) {
// fetch resources
[self.tableView reloadData];
}
}
Generally I override scrollViewDidEndDecelerating and inside it I put my code to request more data.
Example:
- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView{
float endScrolling = scrollView.contentOffset.y + scrollView.frame.size.height;
if (endScrolling >= scrollView.contentSize.height){
//put here your code
}
}
Recently I uploaded on GitHub a subclass of UITableView, that implements the infinite scroll.
You can download it here:
https://github.com/alchimya/iOS-LazyTableView
rather than overriding we can do this optimally in layoutSubviews.
Here's how I got it implemented. You can get to know more about the implementation here
- (void)layoutSubviews{
[super layoutSubviews];
if(self.delegateForViews){
CGPoint contentOffset = self.contentOffset;
if([self.delegateForViews noOfViews]>numOfReusableViews){
NSUInteger centerIndex=visibleViews.count/2;
NSUInteger noOfViews=[self.delegateForViews noOfViews];
UIView *centerView=[visibleViews objectAtIndex:centerIndex];
CGPoint centerViewOrigin=centerView.frame.origin;
CGSize centerViewSize=centerView.frame.size;
CGFloat offsetDifference=contentOffset.x-centerViewOrigin.x;
CGFloat offsetDifferenceAbs=fabs(contentOffset.x-centerViewOrigin.x);
if(offsetDifferenceAbs>=centerViewSize.width){
if(offsetDifference<0){
currentPosition--;
}else{
currentPosition++;
}
self.contentOffset=centerViewOrigin;
currentPosition=[self getPosition:currentPosition noOfViews:noOfViews];
[self.delegateForViews clearView:centerView];
[self.delegateForViews setupView:centerView forPosition:currentPosition];
for (int i=centerIndex-1; i>=0; i--) {
UIView* prevView=[visibleViews objectAtIndex:i];
[self.delegateForViews clearView:prevView];
[self.delegateForViews setupView:prevView forPosition:
[self getPosition:currentPosition-1 noOfViews:noOfViews]];
}
for (int i=centerIndex+1; i<visibleViews.count; i++) {
UIView* nextView=[visibleViews objectAtIndex:i];
[self.delegateForViews clearView:nextView];
[self.delegateForViews setupView:nextView forPosition:
[self getPosition:currentPosition+1 noOfViews:noOfViews]];
}
}
}
}
}
One of the simple and that offered me everything i need is this class:
https://github.com/jakemarsh/JMStatefulTableViewController
You just need to subclass JMStatefulTableViewController and the it has 3 methods that you need to overwrite:
one that is called on init, to get the initial data
statefulTableViewControllerWillBeginInitialLoading
one when the user pull to refresh
statefulTableViewControllerWillBeginLoadingFromPullToRefresh
one when is called for the infinite scroll (next page)
statefulTableViewControllerWillBeginLoadingNextPage
This can be used from Cocoapods too.
scrollviewDidScroll will call when you move through the rows in tableview
func scrollViewDidScroll(_ scrollView: UIScrollView) {
//check for the visible rows
let indexpath = self.tableView.indexPathsForVisibleRows?.last
//check if the visible row last is equal to the total number of counts
if(indexpath?.last == self.listCount){
//code for adding data to the tableview and reload the table view.
}
}
look in the link for more details about indexPathForVisibleRows
https://developer.apple.com/documentation/uikit/uitableview/1614885-indexpathsforvisiblerows

UITableView infinite scrolling

How do I do an infinite scrolling in a UITableView? I know how to do it using a UIScrollView, in which apple has demonstrated in one of the WWDC's video. I tried doing the following in tableView:cellForRowAtIndexPath::
if (indexPath.row == [self.newsFeedData_ count] - 1)
{
[self.newsFeedData_ addObjectsFromArray:self.newsFeedData_];
[self.tableView reloadData];
}
but this fails. Any other idea?
If you need to know when you hit the bottom of the UITableView, become it's delegate (because it is a subclass of UIScrollView), and use the -scrollViewDidScroll: delegate method to compare the table's content height and it's actual scroll position.
EDIT (something like this):
- (void)scrollViewDidScroll:(UIScrollView *)scrollView_
{
CGFloat actualPosition = scrollView_.contentOffset.y;
CGFloat contentHeight = scrollView_.contentSize.height - (someArbitraryNumber);
if (actualPosition >= contentHeight) {
[self.newsFeedData_ addObjectsFromArray:self.newsFeedData_];
[self.tableView reloadData];
}
}
You can support infinite scroll with pull to refresh at the top and/or scroll continuously at the bottom with a spinner wheel using:
https://github.com/samvermette/SVPullToRefresh
SVPullToRefresh handles the logic when UITableView reaches the bottom. A spinner is shown automatically and a callback block is fired. You add in your business logic to the callback block.
Here's an example:
#import "UIScrollView+SVInfiniteScrolling.h"
// ...
[tableView addInfiniteScrollingWithActionHandler:^{
// append data to data source, insert new cells at the end of table view
// call [tableView.infiniteScrollingView stopAnimating] when done
}];
This project can be added to your project using CocoaPods or directly compiled into your project.
Here's a very quick and complete demo of an infinite scrolling UITableView I put together...
#interface InfiniteScrollViewController ()
#property (nonatomic) NSMutableArray *tableViewData;
#property (nonatomic) BOOL loadingMoreTableViewData;
#end
#implementation InfiniteScrollViewController
- (void)viewDidLoad {
self.tableViewData = [[NSMutableArray alloc] init];
[self addSomeMoreEntriesToTableView];
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
return self.tableViewData.count + 1;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
static NSString *CellIdentifier = #"Cell";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
if (cell == nil) {
cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier];
}
if (indexPath.row < self.tableViewData.count) {
cell.textLabel.text = [self.tableViewData objectAtIndex:indexPath.row];
} else {
cell.textLabel.text = #"Loading more data...";
// User has scrolled to the bottom of the list of available data so simulate loading some more if we aren't already
if (!self.loadingMoreTableViewData) {
self.loadingMoreTableViewData = YES;
[self performSelector:#selector(addSomeMoreEntriesToTableView) withObject:nil afterDelay:5.0f];
}
}
return cell;
}
- (void)addSomeMoreEntriesToTableView {
int loopTill = self.tableViewData.count + 20;
while (self.tableViewData.count < loopTill) {
[self.tableViewData addObject:[NSString stringWithFormat:#"%i", self.tableViewData.count]];
};
self.loadingMoreTableViewData = NO;
[self.tableView reloadData];
}
#end
'UITableView' is same as 'UIScrollView' in 'scrollViewDidScroll' method.
So, its easy to emulate infinite scrolling.
double the array so that head and tail are joined together to emulate circular table
use my following code to make user switch between 1st part of doubled table and 2nd part of doubled table when they tend to reach the start or the end of the table.
:
/* To emulate infinite scrolling...
The table data was doubled to join the head and tail: (suppose table had 1,2,3,4)
1 2 3 4|1 2 3 4 (actual data doubled)
---------------
1 2 3 4 5 6 7 8 (visualising joined table in eight parts)
When the user scrolls backwards to 1/8th of the joined table, user is actually at the 1/4th of actual data, so we scroll instantly (we take user) to the 5/8th of the joined table where the cells are exactly the same.
Similarly, when user scrolls to 6/8th of the table, we will scroll back to 2/8th where the cells are same. (I'm using 6/8th when 7/8th sound more logical because 6/8th is good for small tables.)
In simple words, when user reaches 1/4th of the first half of table, we scroll to 1/4th of the second half, when he reaches 2/4th of the second half of table, we scroll to the 2/4 of first half. This is done simply by subtracting OR adding half the length of the new/joined table.
*/
-(void)scrollViewDidScroll:(UIScrollView *)scrollView_
{
CGFloat currentOffsetX = scrollView_.contentOffset.x;
CGFloat currentOffSetY = scrollView_.contentOffset.y;
CGFloat contentHeight = scrollView_.contentSize.height;
if (currentOffSetY < (contentHeight / 8.0)) {
scrollView_.contentOffset = CGPointMake(currentOffsetX,(currentOffSetY + (contentHeight/2)));
}
if (currentOffSetY > ((contentHeight * 6)/ 8.0)) {
scrollView_.contentOffset = CGPointMake(currentOffsetX,(currentOffSetY - (contentHeight/2)));
}
}
P.S. - I've used this code on one of my apps called NT Time Table (Lite). If you want the preview, you can check out the app: https://itunes.apple.com/au/app/nt-time-table-lite/id528213278?mt=8
If your table can sometimes be too short, at the beginning of the above method you can add a if logic to exit the method when data count is say for example less than 9.
For me worked better scrollViewDidEndDragging: than scrollViewDidScroll:.
The second approach will send you each position during scroll and cause, if you are fetching remote resources you will hit your endpoint several times, which is not good.
Complete example based on #codafi solution with comments from #danielgomezrico about how to calculate contentHeight:
- (void)scrollViewDidEndDragging:(UIScrollView *)scrollView
willDecelerate:(BOOL)decelerate {
CGFloat actualPosition = scrollView.contentOffset.y;
CGFloat contentHeight = scrollView.contentSize.height - (self.tableView.frame.size.height);
if (actualPosition >= contentHeight) {
// fetch resources
[self.tableView reloadData];
}
}
Generally I override scrollViewDidEndDecelerating and inside it I put my code to request more data.
Example:
- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView{
float endScrolling = scrollView.contentOffset.y + scrollView.frame.size.height;
if (endScrolling >= scrollView.contentSize.height){
//put here your code
}
}
Recently I uploaded on GitHub a subclass of UITableView, that implements the infinite scroll.
You can download it here:
https://github.com/alchimya/iOS-LazyTableView
rather than overriding we can do this optimally in layoutSubviews.
Here's how I got it implemented. You can get to know more about the implementation here
- (void)layoutSubviews{
[super layoutSubviews];
if(self.delegateForViews){
CGPoint contentOffset = self.contentOffset;
if([self.delegateForViews noOfViews]>numOfReusableViews){
NSUInteger centerIndex=visibleViews.count/2;
NSUInteger noOfViews=[self.delegateForViews noOfViews];
UIView *centerView=[visibleViews objectAtIndex:centerIndex];
CGPoint centerViewOrigin=centerView.frame.origin;
CGSize centerViewSize=centerView.frame.size;
CGFloat offsetDifference=contentOffset.x-centerViewOrigin.x;
CGFloat offsetDifferenceAbs=fabs(contentOffset.x-centerViewOrigin.x);
if(offsetDifferenceAbs>=centerViewSize.width){
if(offsetDifference<0){
currentPosition--;
}else{
currentPosition++;
}
self.contentOffset=centerViewOrigin;
currentPosition=[self getPosition:currentPosition noOfViews:noOfViews];
[self.delegateForViews clearView:centerView];
[self.delegateForViews setupView:centerView forPosition:currentPosition];
for (int i=centerIndex-1; i>=0; i--) {
UIView* prevView=[visibleViews objectAtIndex:i];
[self.delegateForViews clearView:prevView];
[self.delegateForViews setupView:prevView forPosition:
[self getPosition:currentPosition-1 noOfViews:noOfViews]];
}
for (int i=centerIndex+1; i<visibleViews.count; i++) {
UIView* nextView=[visibleViews objectAtIndex:i];
[self.delegateForViews clearView:nextView];
[self.delegateForViews setupView:nextView forPosition:
[self getPosition:currentPosition+1 noOfViews:noOfViews]];
}
}
}
}
}
One of the simple and that offered me everything i need is this class:
https://github.com/jakemarsh/JMStatefulTableViewController
You just need to subclass JMStatefulTableViewController and the it has 3 methods that you need to overwrite:
one that is called on init, to get the initial data
statefulTableViewControllerWillBeginInitialLoading
one when the user pull to refresh
statefulTableViewControllerWillBeginLoadingFromPullToRefresh
one when is called for the infinite scroll (next page)
statefulTableViewControllerWillBeginLoadingNextPage
This can be used from Cocoapods too.
scrollviewDidScroll will call when you move through the rows in tableview
func scrollViewDidScroll(_ scrollView: UIScrollView) {
//check for the visible rows
let indexpath = self.tableView.indexPathsForVisibleRows?.last
//check if the visible row last is equal to the total number of counts
if(indexpath?.last == self.listCount){
//code for adding data to the tableview and reload the table view.
}
}
look in the link for more details about indexPathForVisibleRows
https://developer.apple.com/documentation/uikit/uitableview/1614885-indexpathsforvisiblerows

How to know the UITableview row number

I have a UITableViewCell with UISwitch as accessoryview of each cell. When I change the value of the switch in a cell, how can I know in which row the switch is? I need the row number in the switch value changed event.
Tags, subclasses, or view hierarchy navigation are too much work!. Do this in your action method:
CGPoint hitPoint = [sender convertPoint:CGPointZero toView:self.tableView];
NSIndexPath *hitIndex = [self.tableView indexPathForRowAtPoint:hitPoint];
Works with any type of view, multi section tables, whatever you can throw at it - as long as the origin of your sender is within the cell's frame (thanks rob!), which will usually be the case.
And here it is in a UITableView Swift extension:
extension UITableView {
func indexPath(for view: UIView) -> IndexPath? {
let location = view.convert(CGPoint.zero, to: self)
return self.indexPathForRow(at: location)
}
}
If you set the tag property to the row number (as suggested by other answers), you have to update it every time in tableView:cellForRowAtIndexPath: (because a cell can be reused for different rows).
Instead, when you need the row number, you can walk up the superview chain from the UISwitch (or any other view) to the UITableViewCell, and then to the UITableView, and ask the table view for the index path of the cell:
static NSIndexPath *indexPathForView(UIView *view) {
while (view && ![view isKindOfClass:[UITableViewCell class]])
view = view.superview;
if (!view)
return nil;
UITableViewCell *cell = (UITableViewCell *)view;
while (view && ![view isKindOfClass:[UITableView class]])
view = view.superview;
if (!view)
return nil;
UITableView *tableView = (UITableView *)view;
return [tableView indexPathForCell:cell];
}
This doesn't require anything in tableView:cellForRowAtIndexPath:.
in cellForRowAtIndexPath:, set the tag property of your control to indexPath.row
Accepted solution is a clever hack.
However why do we need to use hitpoint if we can utilize already available tag property on UIView? You would say that tag can store only either row or section since its a single Int.
Well... Don't forget your roots guys (CS101).
A single Int can store two twice-smaller size integers.
And here is an extension for this:
extension Int {
public init(indexPath: IndexPath) {
var marshalledInt: UInt32 = 0xffffffff
let rowPiece = UInt16(indexPath.row)
let sectionPiece = UInt16(indexPath.section)
marshalledInt = marshalledInt & (UInt32(rowPiece) << 16)
marshalledInt = marshalledInt + UInt32(sectionPiece)
self.init(bitPattern: UInt(marshalledInt))
}
var indexPathRepresentation: IndexPath {
let section = self & 0x0000ffff
let pattern: UInt32 = 0xffff0000
let row = (UInt32(self) & pattern) >> 16
return IndexPath(row: Int(row), section: Int(section))
}
}
In your tableView(_:, cellForRowAt:) you can then:
cell.yourSwitch.tag = Int(indexPath: indexPath)
And then in the action handler you would can:
func didToogle(sender: UISwitch){
print(sender.tag.indexPathRepresentation)
}
However please note it's limitation: row and section need to be not larger then 65535. (UInt16.max)
I doubt your tableView's indexes will go that high but in case they do, challenge yourself and implement more efficient packing scheme. Say if we have a section very small, we don't need all 16 bits to represent a section. We can have our int layout like:
{section area length}{all remaining}[4 BITS: section area length - 1]
that is our 4 LSBs indicate the length of section area - 1, given that we allocate at least 1 bit for a section. Thus in case of our section is 0, the row can occupy up to 27 bits ([1][27][4]), which definitely should be enough.
I prefer using subviews, if you know your layout it's generally super simple and 1 line short...
NSIndexPath *indexPath = [tableView indexPathForCell:(UITableViewCell *)[[sender superview] superview]];
Thats it, if its more nested, add in more superviews.
Bit more info:
all you are doing is asking for the parent view and its parent view which is the cell. Then you are asking your tableview for the indexpath of that cell you just got.
One common way to do this is to set the tag of the control (in your case the switch) to something that can be used to identify the row or represented object.
For example, in tableView:cellForRowAtIndexPath: set the tag property of the switch to the indexPath.row and in your action method you can get the tag from the sender.
Personally, I don't like this approach and prefer subclassing UITableViewCell.
Also, it may be a good idea to add an "offset" to the tag to prevent any conflicts with the tags of other views.
The accepted answer on this post is perfectly fine. I'd like to suggest to readers that the following, derived from #robmayoff on this post, is also perfectly fine:
- (NSIndexPath *)indexPathForView:(UIView *)view inTableView:(UITableView *)tableView {
while (view && ![view isKindOfClass:[UITableViewCell class]])
view = view.superview;
UITableViewCell *cell = (UITableViewCell *)view;
return [tableView indexPathForCell:cell];
}
Some have asserted that this approach contains too much computational work because of the while loop. The alternative, convert the view's origin to table view coordinate space and call indexPathForRowAtPoint:, hides even more work.
Some have asserted that this approach is unsafe relative to potential SDK changes. In fact, Apple has already changed the tableview cell hierarchy once, adding a contentView to the cell. This approach works before and after such a change. As long as view ancestors can be found via a chain of superviews (which is as fundamental as anything in UIKit), this is good code.
A colleague suggested the following, which I made into a UITableView category:
+(UITableViewCell*)findParentCellForSubview:(UIView*)view
{
while (([view isKindOfClass:[UITableViewCell class]] == NO) && ([view superview] != nil))
view = [view superview];
if ([view superview] != nil)
return (UITableViewCell*)view;
return nil;
}
Still hackly - but it works.
One more variant of using superView. Works like category for UIView.
- (UITableViewCell *)superCell
{
if (!self.superview) {
return nil;
}
if ([self.superview isKindOfClass:[UITableViewCell class]]) {
return (UITableViewCell *)self.superview;
}
return [self.superview superCell];
}
i dont know about the multiple sections but i can give you for the one section...
-(void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
NSInteger index=indexPath.row;
NSString *string=[[NSString alloc]initWithFormat:#"%ld",(long)index];
}
from this you can get the row number and you can save it to the string....

UITableViewCell, Delete Button Frame?

Is there a way to alter the frame of the "swipe" [DELETE] button used on UITableViewCells? Currently its centred vertically within the cell, but if possible I would like to move it down to the cyan guide show.
If you are looking for a strongly true way to solve this problem then you should to subclass of UITableViewCell and override the all state handling methods for correct drawing your own delete button (do not call super in those methods). But there is another easy way:
#implementation CustomCell
- (void)layoutSubviews {
[super layoutSubviews];
if (self.showingDeleteConfirmation) {
if ([self.subviews count] < 4) return;
UIView *delBtn = [self.subviews objectAtIndex:3];
delBtn.frame = CGRectOffset(delBtn.frame, 0, 10);
}
}
#end
Instead of didTransitionToState:,
How about using the -(void)willTransitionToState: and setting the frame of the editingAccessoryView?
- (void)willTransitionToState:(UITableViewCellStateMask)state
{
if (state == UITableViewCellEditingStyleDelete)
{
NSInteger num = 10;
UIView.frame = CGRectMake(UIView.frame.origin.x,UIView.frame.origin.y - num,
UIView.size.width,UIView.size.height);
}
}
Try changing the frame for Accessory View

IPhone UITableView suppress disclosure-button when Delete Button is displayed

I have a UITableView that has a disclosure button on every row. When the table is put into edit mode and the the Deletion control is pressed ("-" sign), the Delete Button shows, however the disclosure button is not replaced, but instead just slides to the left of the delete button.
The apple UITableView guide explains the delegates for everything it seems except for the delegate that is called when the "-" sign is pressed, but before the delete button is displayed.
I would just like to suppress the disclosure indicator while the delete button is shown.
I'm guessing that I am missing something... I have set the setHidesAccessoryWhenEditing:NO
on the cells of the table so that the indicator is displayed to indicate to the user that if they select the row, they can edit it...
The behavior I am trying to copy is done in the contacts app when a contact is edited. Any help would be greatly appreciated...
Thanks, Greg
The standard way to do this is to use cell.hidesAccessoryWhenEditing = YES, and that editing is a modal action in which navigation is typically disabled.
The Contacts application actually uses custom table cells, and I wouldn't be surprised if it didn't so much use an accessory as have an image located on the cell's right edge, judging by its behaviour.
If you want to know when the delete button appears, I'd suggest that you try installing a Key-Value observer on the cell's showingDeleteConfirmation property, like so:
[cell addObserver: self forKeyPath: #"showingDeleteConfirmation"
options: NSKeyValueObservingOptionNew context: NULL];
Then you implement the observer callback method:
- (void)observeValueForKeyPath: (NSString *) keyPath ofObject: (id) object
change: (NSDictionary *) change context: (void *) context
{
if ( [keyPath isEqualToString: #"showingDeleteConfirmation"] )
{
UITableViewCell * cell = (UITableViewCell *) object;
BOOL isShowing = [[change objectForKey: NSKeyValueChangeNewKey] boolValue];
if ( isShowing == NO )
cell.accessoryType = UITableViewCellAccessoryDisclosureIndicator;
else
cell.accessoryType = UITableViewCellAccessoryNone;
}
}
I should note that I'm not sure whether the UITableViewCell class is KVO-compliant for this property, but it's got to be worth a try…
Or when you are creating your cell you can define the accessory type: cell.editingAccessoryType = UITableViewCellAccessoryNone;
The disclosure indicator is managed by tableView:accessoryTypeForRowWithIndexPath: so maybe you could change the accessory type while in editing mode.
I believe there's a tableView:accessoryButtonTappedForRowWithIndexPath:, maybe there you can note that you're going to edit mode and then change what the tableView:accessoryTypeForRowWithIndexPath: returns for each row - no accessory when in edit mode.
I would give it a try.
Here is a calling sequence (not sure if that helps) description/tutorial/examples from Apple reference about how to go about Inserting and Deleting Rows in Editing Mode.
My comment to Jim's solution didn't come across very well... Here is the version that solved the problem for me... Thanks again Jim!
- (void)observeValueForKeyPath: (NSString *) keyPath ofObject: (id) object
change: (NSDictionary *) change context: (void *) context
{
UITableViewCell * cell = object;
if ( [keyPath isEqualToString: #"showingDeleteConfirmation"] )
{
BOOL isShowing = [[change objectForKey: NSKeyValueChangeNewKey] boolValue];
if ( !isShowing )
{
cell.accessoryType = UITableViewCellAccessoryDisclosureIndicator;
}
else
{
cell.accessoryType = UITableViewCellAccessoryNone;
}
}
}