I use UICollectionViewDragDelegate & UICollectionViewDropDelegate to implement Drag-N-Drop functionality for my collectionView. However, I don't know how I can detect the following actions:
If the user just lifts the photo but doesn't move, and then release.
If the user drags the cell & move but still drop at the original position.
In summary, I just found that if the destination position is the same as the original position, the function performDropWithCoordinator will not be called. So I don't know how to check if the new position is the same as the original position.
To detect when a drag starts, implement this in the collection view controller
- (BOOL) collectionView: ( UICollectionView * ) collectionView
canMoveItemAtIndexPath: ( NSIndexPath * ) indexPath
This will fire when the drag begins. Then, to detect when it ends, you need to subclass the UICollectionViewFlowLayout and implement this
// ZZZ!!!
- ( UICollectionViewLayoutInvalidationContext * ) invalidationContextForEndingInteractiveMovementOfItemsToFinalIndexPaths:( NSArray < NSIndexPath * > * ) indexPaths
previousIndexPaths:( NSArray < NSIndexPath * > * ) previousIndexPaths
movementCancelled:( BOOL ) movementCancelled
BUT be careful. If the item was moved then the usual
// XXX!!!
- (void) collectionView:( UICollectionView * ) collectionView
moveItemAtIndexPath:( NSIndexPath * ) sourceIndexPath
toIndexPath:( NSIndexPath * ) destinationIndexPath
would have been called BEFORE this is called. If the item was not moved but just dropped in place, then XXX!!! above will not be called but ZZZ!!! will still be called, so you need to implement some logic to get it working properly.
I suggest you set the source index path when the drag starts and whenever it is handled you set it to nil. Then you can check for that before you apply the move.
For me it sometimes happens when an item is dragged to an adjacent cell (that the XXX is not called, only ZZZ) but I suspect that is due to the way in which I handle other aspects of the drag.
EDIT
Just to make it clear - ZZZ is always called last. XXX is not always called but if it is, it is called before ZZZ. So maybe XXX then ZZZ tells you the drag ended.
PS : I did not use drop delegate (mine is iOS 10 compatible) but I think you'd be able to transfer this to your drop-delegate-enabled solution.
this is my second answer.
The previous one was really for iOS 10, this one is a bit more modern even if it is incomplete.
Take a look at dragStateDidChange. That message is send to the UICollectionViewCell and so you won't find it in either the drag- or drop delegates that you use, but you might be able to accomplish what you wish there.
Then, another comment, in the drop delegate, the message collectionView:dropSessionDidUpdate:withDestinationIndexPath: gives you a destination path but you typically need to translate that with something like
NSIndexPath * dst = [collectionView presentationIndexPathForDataSourceIndexPath:destinationIndexPath];
to get the real destination, especially if you want to check that it is different from the original destination path.
Related
I have two table view filled in their viewDidLoad with data coming from a sqllite db containing names of shops. In the first view i choose one element (i.e. one shop), then in the second i have to choose another shop different from the first.
I can disable one of the row of the second table view after it has been filled?
I tried not to load the name (of the first shop choosen) in the second view but i fill the table view in its viewDidLoad and unfortunately the data regarding the name of the first shop choosen doesn't seem to be available until the viewDidAppear and that moment is too late to fill the table view.
I tried, also, to add an alert to the event of choosing one row in the second view in this way
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
NSString *choosenSecondShop= [secondShop objectAtIndex:indexPath.row];
NSString* nameFirstShop = [function loadFirstShopChoosen];
if ([choosenSecondShop isEqualToString:nameFirstShop]) {
NSString *message=#"Second shop cannot match the first";
alert = [[UIAlertView alloc] initWithTitle:#"Errore"
message: message
delegate: self cancelButtonTitle: #"Ok"
otherButtonTitles:nil,
nil];
[alert show];
}else {
// go on with the app's stuff!
}
}
but in this case the alert is displayed but also the segue followed. I can prevent the segue to go on?
There are a few different ways to attack this problem:
1. Conditional segue
This should work, though I'm not sure it's a great idea from a user experience perspective.
Presumably you made a segue in IB starting from the table cell? When you do that, you get the convenience of not needing any code to perform the segue (it's automatically done when you tap the table cell), but you don't get any run-time control over it either.
If you need programmatic control over whether/when/which segue to perform, you should create a segue starting from the view controller itself (not from a control within it), and give the segue a unique identifier in IB. Then, in your tableView:didSelectRowAtIndexPath: implementation, you can call [self performSegueWithIdentifier:#"myIdentifier"] once you know you want to perform the segue.
2. Disable cell selection
To prevent a specific cell from being selected, your table view controller can implement tableView:willSelectRowAtIndexPath: to return nil for any index path you don't want the user to select.
If you do that, you might want to make it clear to the user which rows can be selected -- you can alter the cell's appearance in tableView:cellForRowAtIndexPath:.
3. Don't show the cell
You say you're populating the table view in viewDidLoad, but it doesn't really work that way: Table views populate themselves by calling your data source & delegate methods when they need to. If you want to prevent an item from your data set from being shown as a cell in the table, you just need to alter the behavior of your data source & delegate methods:
tableView:numberOfRowsInSection: should return a number one less than it would otherwise
tableView:cellForRowAtIndexPath: should reflect the removal of the item; something like:
MyShop *shop;
if (indexPath.row < indexOfFirstShop)
shop = [shops objectAtIndex:indexPath.row];
else
shop = [shops objectAtIndex:(indexPath.row + 1)];
// then configure cell for the chosen shop
Now, if you don't know which row you want to hide as of the first time these methods are called, all you need to do once you get that information is tell the table view that it needs to call them again: [self.tableView reloadData] should do the trick.
I have situation where I use view based Table Views and don't want to use bindings between data source and table view. This is mainly due to the fact that my NSTableCellView can have multiple subviews, complex validation and triggered calls to methods in other objects.
We have very clear path of updating NSTableView with data source with:
tableView:viewForTableColumn:row:
However, for backwards, that is updating datasource with updates in NSTableView we have nothing of the sort we have for cell based Table views:
tableView:setObjectValue:forTableColumn:row:
Target Action pattern is suggested instead. So, I have basically 2 questions:
If I set target and action for one specific view, or its subview, how do I get proper row and column info to know what to update in data source?
Should clickedRow and clickedColumn from NSTableView do the trick, although I have edited or changed one subview object?
How can I inform the target (as other object, not NSTableView instance) about row and column if action will pass for example NSTextField as parameter?
I can basically come to clickedColumn and clickedRow (if those 2 properties are proper answer to first question) through subview tree, but I find this as pretty much non-elegant solution and have hunch there is a better way....
THanks in advance....
NSTableCellView has an objectValue. Presumably you're already setting it, so the action can use [(NSTableCellView *)[sender superview] objectValue] to find out which object it needs to manipulate.
I suggest that you also subclass NSTableCellView and implement the action there. If you need access to other parts of the model, you can add an outlet for your view controller.
If you really need the row number, you can call indexOfObject on your content array.
The two NSTableView Methods rowForView and columnForView should do the trick.
You can call them with the sender of an Target/Action Method, like one triggered by an NSButton in your TableView (its ok, to have it somewhere in a subwiew)
Or you can call these Methods from within a delegate method implementation like the textDidChange from NSTextDelegate. So you can easily update your corresponding Array.
If you don't want continous updates textDidEndEditing would also do the job.
- (void)textDidChange:(NSNotification *)notification
{
NSTextView *tv = [notification object];
int r = [tableView rowForView:tv];
int c = [tableView columnForView:tv];
NSLog(#"Row: %d Column: %d", r, c);
// updating code here
}
I'm trying to create a UITableView with dates, shouldn't be very exciting, I know. It starts around the current date, but the user should be able to scroll down (the future) and up (the past) as far as he/she wants. This results in a potentially infinite number of rows. So what's the way to go about creating this?
Returning NSIntegerMax as the number of rows already crashes the app, but even if it wouldn't, that still doesn't account for being able to scroll up. I could start half way of course, but eventually there's a maximum.
Any ideas how to do or fake this? Can I update/reload the table somehow without the user noticing, so I never run into a border?
SOLUTION:
I went with #ender's suggestion and made a table with a fixed amount of cells. But instead of reloading it when the user scrolls to near the edges of the fixed cells, I went with reloading the table when the scrolling grinds to a halt. To accomodate with a user scrolling great distances without stopping, I just increased the row count to 1000, putting the ROW_CENTER constant to 500. This is the method that takes care of updating the rows.
- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView
{
NSArray *visible = [self.tableView indexPathsForVisibleRows];
NSIndexPath *upper = [visible objectAtIndex:0];
NSIndexPath *lower = [visible lastObject];
// adjust the table to compensate for the up- or downward scrolling
NSInteger upperDiff = ROW_CENTER - upper.row;
NSInteger lowerDiff = lower.row - ROW_CENTER;
// the greater difference marks the direction we need to compensate
NSInteger magnitude = (lowerDiff > upperDiff) ? lowerDiff : -upperDiff;
self.offset += magnitude;
CGFloat height = [self tableView:self.tableView heightForRowAtIndexPath:lower];
CGPoint current = self.tableView.contentOffset;
current.y -= magnitude * height;
[self.tableView setContentOffset:current animated:NO];
NSIndexPath *selection = [self.tableView indexPathForSelectedRow];
[self.tableView reloadData];
if (selection)
{
// reselect a prior selected cell after the reload.
selection = [NSIndexPath indexPathForRow:selection.row - magnitude inSection:selection.section];
[self.tableView selectRowAtIndexPath:selection animated:NO scrollPosition:UITableViewScrollPositionNone];
}
}
The magic breaks when a user scrolls to the edge of the table without stopping, but with the table view bounces property disabled, this merely feels like a minor glitch, yet totally acceptable. As always, thanks StackOverflow!
You should establish a fixed number of cells and adjust your datasource when the user scrolls near the end of the tableview. For example, you have an array with 51 dates (today, 25 future and 25 past). When the app tries to render a cell near one of the borders, reconfigure your array and call reloadData
You might also have a look at the "Advanced Scroll View Techniques" talk of the WWDC 2011. They showed how you would create a UIScrollView which scrolls indefinitely. It starts at about 5 mins. in.
Two thoughts:
Do you really need a UITableView? You could use a UIScrollView, three screens high. If end of scrolling is reached, layout your content and adjust scrolling position. This gives the illusion of infinite scrolling. Creating some date labels and arranging them in layoutSubviews: should not be too much of an effort.
If you really want to stick to UITableView you could think about having two UITableViews. If scrolling in your first one reaches a critical point, spin off a thread and populate the second one. Then at some point, exchange the views and trigger the scrolling manually so that the user does not note the change. This is just some idea from the top of my head. I have not implemented something like this yet, but I implemented the infinite UIScrollView.
I answered this question in another post: https://stackoverflow.com/a/15305937/2030823
Include:
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.
#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 question has already been asked: implementing a cyclic UITableView
I'm copying that answer here to make it easier because the asker hasn't ticked my answer.
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 (visualizing 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.
Written and posted by Anup Kattel. Feel free to use this code. Please keep these comments if you don't mind.
*/
-(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 when data count is say for example less than 9.
The UIScrollView has a lot of information available to the programmer, but I dont see an obvious way to control the location that the control stop at after decelerating from a scroll gesture.
Basically I would like the scrollview to snap to specific regions of the screen. The user can still scroll like normal, but when they stop scrolling the view should snap to the most relevant location, and in the case of a flick gesture the deceleration should stop at these locations too.
Is there an easy way to do something like this, or should I consider the only way to accomplish this effect to write a custom scrolling control?
Since the UITableView is a UIScrollView subclass, you could implement the UIScrollViewDelegate method:
- (void)scrollViewWillEndDragging:(UIScrollView *)scrollView
withVelocity:(CGPoint)velocity
targetContentOffset:(inout CGPoint *)targetContentOffset
And then compute what the closest desired target content offset is that you want, and set that on the inout CGPoint parameter.
I've just tried this and it works well.
First, retrieve the unguided offset like this:
CGFloat unguidedOffsetY = targetContentOffset->y;
Then Figure out through some math, where you'd want it to be, noting the height of the table header. Here's a sample in my code dealing with custom cells representing US States:
CGFloat guidedOffsetY;
if (unguidedOffsetY > kFirstStateTableViewOffsetHeight) {
int remainder = lroundf(unguidedOffsetY) % lroundf(kStateTableCell_Height_Unrotated);
log4Debug(#"Remainder: %d", remainder);
if (remainder < (kStateTableCell_Height_Unrotated/2)) {
guidedOffsetY = unguidedOffsetY - remainder;
}
else {
guidedOffsetY = unguidedOffsetY - remainder + kStateTableCell_Height_Unrotated;
}
}
else {
guidedOffsetY = 0;
}
targetContentOffset->y = guidedOffsetY;
The last line above, actually writes the value back into the inout parameter, which tells the scroll view that this is the y-offset you'd like it to snap to.
Finally, if you're dealing with a fetched results controller, and you want to know what just got snapped to, you can do something like this (in my example, the property "states" is the FRC for US States). I use that information to set a button title:
NSUInteger selectedStateIndexPosition = floorf((guidedOffsetY + kFirstStateTableViewOffsetHeight) / kStateTableCell_Height_Unrotated);
log4Debug(#"selectedStateIndexPosition: %d", selectedStateIndexPosition);
NSIndexPath *indexPath = [NSIndexPath indexPathForRow:selectedStateIndexPosition inSection:0];
CCState *selectedState = [self.states objectAtIndexPath:indexPath];
log4Debug(#"Selected State: %#", selectedState.name);
self.stateSelectionButton.titleLabel.text = selectedState.name;
OFF-TOPIC NOTE: As you probably guessed, the "log4Debug" statements are just logging. Incidentally, I'm using Lumberjack for that, but I prefer the command syntax from the old Log4Cocoa.
After the scrollViewDidEndDecelerating: and scrollViewDidEndDragging:willDecelerate: (the last one just when the will decelerate parameter is NO) you should set the contentOffset parameter of your UIScrollView to the desired position.
You also will know the current position by checking the contentOffset property of your scrollview, and then calculate the closest desired region that you have
Although you don't have to create your own scrolling control, you will have to manually scroll to the desired positions
To add to what felipe said, i've recently created a table view that snaps to cells in a similar way the UIPicker does.
A clever scrollview delegate is definitely the way to do this (and you can also do that on a uitableview, since it's just a subclass of uiscrollview).
I had this done by, once the the scroll view started decelerating (ie after scrollViewDidEndDragging:willDecelerate: is called), responding to scrollViewDidScroll: and computing the diff with the previous scroll event.
When the diff is less than say a 2 to 5 of pixels, i check for the nearest cell, then wait until that cell has been passed by a few pixels, then scroll back in the other direction with setContentOffset:animated:.
That creates a little bounce effect that is very nice for user experience, as it gives a good feedback on the snapping.
You'll have to be clever and not do anything when the table is bouncing at the top or bottom (comparing the offset to 0 or the content size will tell you that).
It works pretty well in my case because the cells are small (about 80-100px high), you might run into problems if you have a regular scroll view with bigger content areas.
Of course, you will not always decelerate past a cell, so in this case i just scroll to the nearest cell, and the animation looks jerky. Turns out with the right tuning, it barely ever happens, so i'm cool with this.
Spend a few hours tuning the actual values depending on your specific screen and you can get something decent.
I've also tried the naive approach, calling setContentOffset:animated: on scrollViewDidEndDecelerating: but it creates a really weird animation (or just plain confusing jump if you don't animate), that gets worse the lower the deceleration rate is (you'll be jumping from a slow movement to a much faster one).
So to answer the question:
- no, there is no easy way to do this, it'll take some time polishing the actual values of the previous algorithm, which might not work at all on your screen,
- don't try to create your own scroll view, you'll just waste time and badly reinvent a beautiful piece of engineering apple created with truck loads of bug. The scrollview delegate is the key to your problem.
Try something like this:
- (void) snapScroll;
{
int temp = (theScrollView.contentOffset.x+halfOfASubviewsWidth) / widthOfSubview;
theScrollView.contentOffset = CGPointMake(temp*widthOfSubview , 0);
}
- (void) scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate;
{
if (!decelerate) {
[self snapScroll];
}
}
- (void) scrollViewDidEndDecelerating:(UIScrollView *)scrollView
{
[self snapScroll];
}
This takes advantage of the int's drop of the post-decimal digits. Also assumes all your views are lined up from 0,0 and only the contentOffset is what makes it show up in different areas.
Note: hook up the delegate and this works perfectly fine. You're getting a modified version - mine just has the actual constants lol. I renamed the variables so you can read it easy
I'm using a UISearchDisplayController based on this tutorial:
http://developer.apple.com/library/ios/#samplecode/TableSearch/Introduction/Intro.html
I've a table view in navigation controller with that search controller. And as usual, you can search, click on the search result and go to detail view.
I'm using such code like the following to detect whether the current table view is searchResultsTableView.
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
if(tableView == self.searchDisplayController.searchResultsTableView)
return 1;
else
return [self.arrCharacters count];
}
My problem is when the user come back from detail view to the search result table view, that tableView becomes normal table view and not a searchResultsTableView anymore. But the table is still filtered and only show the search results. Just the tableView is no longer recognized as a searchResultsTableView. So all my index calculations go wrong and app crashes.
Any help is greatly appreciated.
Many thanks,
One alternative way to solve this situation is set a flag whether your table is searchResultTableView or just normal tableView. For instance, when user did search in any chance, set your flag as true, and keep this value unless user click on cancel or any other way to finish search. Since you are using searchDisplayController, delegate method
-(void)searchDisplayControllerWillEndSearch:(UISearchDisplayController *)controller
will be called at the end of search, so you can change flag value to indicate normal table. Based on this flag value, you can put if statement in tableView delegate methods to set up table in a way you want to. However, when I attempt to check same situation as your case (search and click one of table cell and go back to previous table) in my app, tableView still hold as searchResultTableView.