Scrolling speed during UITableView re-ordering mode (not performance related) - iphone

Is there a way to increase the speed that you can drag a cell up/down during a table's movable row mode of a UITableView? For instance, there seems to be a standard speed that the table will allow you to drag the cell when you are moving it around and the scroll speed seems to increase if you hold it near the top/bottom edge of the device screen. For a given cell height and a whole bunch of cells, this might take a while to re-order them all if I have to use their standard slow scrolling.
So basically, I would like to increase the speed that it scrolls up/down when you are dragging the cell so it won't take as long to get to where you want to drop the cell in place.
I understand that you can speed up the moving process by decreasing cell height to place more cells on the device screen, but I'd rather do this only if I can't increasing scrolling speed.
Any suggestions or ideas from past experiences with this? Any advice is greatly appreciated!

Here you go. A proof of concept of a speed enhancer of the scroll when you move a cell, SDK 3.1. It may not pass Apple's requirements since overriding _beginReorderingForCell and _endReorderingForCell looks a little off-spec. There are other ways to determine if a cell starts or ends reordering (e.g. subclassing UITableViewCell and finding some measure) but this is the easiest I think.
The approach is quite simple: for every movement of Y pixels down, we move 2*Y pixels down, only when reordering.
The problem is that the currently dragged cell is a subview of the table view, so it shifts with the table view if we move it. If we are to correct for that within this setContentOffset, it has no effect since the position of the cell will be set based on values calculated apart from the current contentOffset. Therefore we correct an instant later using performSelector.
I left the debugging lines in there for convenience. All you need to do is to use FastUITableView instead of UITableView (esp. in you NIB)
You may of course want to add some timing things, so that the speed only goes up after 1 second or so. That will be trivial.
#interface FastUITableView : UITableView
{
UITableViewCell *draggingCell;
CGFloat lastY;
}
#end
#implementation FastUITableView
-(void)_beginReorderingForCell:(UITableViewCell*)cell
{
printf("begin reordering for cell %x\n",cell);
draggingCell = cell;
lastY = -1.0f;
[super _beginReorderingForCell:cell];
}
-(void)_endReorderingForCell:(UITableViewCell*)cell wasCancelled:(BOOL)cancelled animated:(BOOL)animated
{
printf("end reordering for cell %x\n",cell);
draggingCell = nil;
[super _endReorderingForCell:cell wasCancelled:cancelled animated:animated];
}
-(void)setContentOffset:(CGPoint)pt
{
if ( !draggingCell )
{
[super setContentOffset:pt];
return;
}
if ( lastY < 0 ) lastY = pt.y;
CGPoint fast = pt;
float diff = pt.y - lastY;
//diff *= 0.5; /// <<--- control speed here
fast.y = pt.y + diff;
if ( fast.y > self.contentSize.height - self.superview.frame.size.height )
{
CGFloat corr = fast.y - self.contentSize.height + self.superview.frame.size.height;
printf("Correction: %f\n",corr);
fast.y -= corr;
diff -= corr;
} else if ( fast.y < 0.0f ) {
CGFloat corr = -fast.y;
printf("Correction: %f\n",corr);
diff += corr;
fast.y += corr;
}
[self performSelector:#selector(moveCell:) withObject:[NSNumber numberWithFloat:diff] afterDelay:0.01];
lastY = fast.y;
// printf("setting content offset: %f -> %f of max %f\n",pt.y, fast.y, self.contentSize.height);
[super setContentOffset:fast];
}
-(void)moveCell:(NSNumber*)diff
{
CGRect frame = draggingCell.frame;
frame.origin.y += [diff floatValue];
// printf("shifting cell %x with %f\n",draggingCell,[diff floatValue]);
draggingCell.frame = frame;
}

If the list is going to get that long, you may want to consider other reordering mechanisms. For example, my Netflix queue includes a number in each call specifying the order of the queue, 1 for the movie about to ship, 2 for the next and so on to 187 or so for the most recent additions to the queue. I can drag an entry to reorder like on the iPhone, but I can also change the order numbers in the cells to reorder the entries, which is much easer than having to drag #187 to the 10th spot in the queue. There is also a button in each entry to put that entry on the very top.
In your app, you can add extra controls in edit view to assist in reordering such as the order number or "up/down 10" buttons.
If there are common reasons your users would want to order entries, you can have a button that handles that, such as "move up to next nearest blue entry."

From my experience, tableView scroll speed depends on distance between dragged cell and content origin/end points.
So to increase scroll speed, you don't need to subclass UITableView. Instead, you can set contentInset property of a tableView.
For example, call this in viewDidLoad:
tableView.contentInset = UIEdgeInsetsMake(64, 0, 64, 0)
Value 64 assumes that there are statusBar and navBar above tableView, top/bottom constraint between tableView and superView is set to 0 and edgesForExtendedLayout is set to .all.
That tableView goes under navBar and statusBar but because of contentInset it looks like it's attached to navBar.

Related

Loop UIScrollView but continue decelerating

I've set up an infinite scroll view and when it reaches 0 content offset I set it to max content offset and vice versa.
i.e.
[scrollView setContentOffset:CGPointMake(0,0) animated:NO];
This works but it stops the UIScrollView decelerating.
Is there a way to do this but keep the UIScrollView decelerating?
I tried this...
float declerationRate = scrollView.decelerationRate;
[scrollView setContentOffset:CGPointMake(scrollView.frame.size.width, 0) animated:NO];
scrollView.decelerationRate = declerationRate;
but it didn't work.
EDIT
I just realised that decelerationRate is the setting to determine how slow/fast the deceleration is.
What I need is to get the current velocity from the scrollView.
Right I had to tweak the idea a bit.
Turns out trying to set the velocity of a UIScrollView is difficult... very difficult.
So anyway, I kind of tweaked it.
This is actually a mini project after answering someone else's SO question and thought I'd try to solve it myself.
I want to create a spinner app that I can swipe to spin an arrow around so it spins and decelerates to a point.
What I did was set up a UIImageView with the arrow pointing up.
Then covering the UIImageView is a UIScrollView.
Then in the code...
#interface MyViewController () <UIScrollViewDelegate>
#property (nonatomic, weak) IBOutlet UIScrollView *scrollView;
#property (nonatomic, weak) IBOutlet UIImageView *arrowView;
#end
#implementation MyViewController
- (void)viewDidLoad
{
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
//make the content size really big so that the targetOffset of the deceleration will never be met.
self.scrollView.contentSize = CGSizeMake(self.scrollView.frame.size.width * 100, self.scrollView.frame.size.height);
//set the contentOffset of the scroll view to a point in the center of the contentSize.
[self.scrollView setContentOffset:CGPointMake(self.scrollView.frame.size.width * 50, 0) animated:NO];
}
- (void)didReceiveMemoryWarning
{
[super didReceiveMemoryWarning];
// Dispose of any resources that can be recreated.
}
- (void)rotateImageView
{
//Calculate the percentage of one "frame" that is the current offset.
// each percentage of a "frame" equates to a percentage of 2 PI Rads to rotate
float minOffset = self.scrollView.frame.size.width * 50;
float maxOffset = self.scrollView.frame.size.width * 51;
float offsetDiff = maxOffset - minOffset;
float currentOffset = self.scrollView.contentOffset.x - minOffset;
float percentage = currentOffset / offsetDiff;
self.arrowView.transform = CGAffineTransformMakeRotation(M_PI * 2 * percentage);
}
- (void)scrollViewDidScroll:(UIScrollView *)scrollView
{
//the scrollView moved so update the rotation of the image
[self rotateImageView];
}
- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView
{
//the scrollview stopped moving.
//set the content offset back to be in the middle
//but make sure to keep the percentage (used above) the same
//this ensures the arrow is pointing in the same direction as it ended decelerating
float diffOffset = scrollView.contentOffset.x;
while (diffOffset >= scrollView.frame.size.width) {
diffOffset -= scrollView.frame.size.width;
}
[scrollView setContentOffset:CGPointMake(scrollView.frame.size.width * 50 + diffOffset, 0) animated:NO];
}
#end
This gives the desired effect of a spinner like on Wheel of Fortune that will spin endlessly in either direction.
There is a flaw though. If the user keeps spinning and spinning without letting it stop it will only go 50 spins in either direction before coming to a stop.
As I said in the comment, the way you're doing is producing an expected result, one way to do what you want is to set the content offset to the top but then by using the content size height and deceleration value, you could animate the content offset again, check out this answer: https://stackoverflow.com/a/6086521/662605
You will have to play around with some math before it feel right but I think this is a reasonable workaround.
The lower the deceleration, the longer the animation (time) and the more it will animate (distance). Let me know what you think.
As you've said, the deceleration is probably not the only thing you need. So you could try KVO on the contentOffset to calculate the mean velocity over half a second perhaps to get an idea of speed.
There are probably a few different ways to do this. Ideally, you won't have to do any type of calculation to simulate the remaining deceleration physics or mess around at all with UIScrollView's internals. It's error-prone, and it's not likely to perfectly match the UIScrollView physics everyone's used to.
Instead, for cases where your scroll view is merely a driver for a gesture (i.e., you don't actually need to display anything in it), I think it's best to just have a massively wide scroll view. Set its initial content offset to the center of its content size. In scrollViewDidScroll(_:), calculate a percentage of the traversed screen width:
let pageWidth = scrollView.frame.width
let percentage = (scrollView.contentOffset.x % pageWidth) / pageWidth
This will basically loop from 0.0 to 1.0 (moving right) or from 1.0 to 0.0 (moving left) over and over. You can forward those normalized values to some other function that can respond to it, perhaps to drive an animation. Just structure whatever code responds to this such that it appears seamless when jumping from 0.0 to 1.0 or from 1.0 to 0.0.
Of course, if you need whatever you're looping to occur faster or slower than the normal scroll view speed, just use a smaller or larger fraction of the screen width. I just picked that arbitrarily.
If you're worried about hitting the edges of the scrollable content, then when the scroll view comes to a complete rest1, reset its content offset to the same initial center value, plus whatever remainder of the screen width (or whatever you're using) the scroll view was at when it stopped scrolling.
Given the resetting approach above, even for scroll views where you're hosting visible content, you can effectively achieve an infinitely scrolling view as long as you update whatever view frames/model values to take into account the reset scroll offset.
1 To properly capture this, implement scrollViewDidEndDecelerating(_:) and scrollViewDidEndDragging(_:willDecelerate:), only calling your "complete rest" function in the latter case when willDecelerate is false. Always call it in the former case.
I found that if you call
[scrollView setContentOffset:CGPointMake(0,0) animated:NO];
at point(0,0), it will trigger bounces action for UIScrollView, then the deceleration will stop. The same situation takes place when you call setContentOffset to bound of the content.
So you can call setContentOffset to point(10,10) or somewhere else to keep the deceleration easily.

How to zoom when scroll UIScrollView

I add few labels in UIScrollView and I want when I scroll, the the middle label font size can become bigger. And the previous middle label font size shrinks to smaller. And the change happens gradually. Like below. Label 1 move to left shrink smaller and label 2 move to middle becomes bigger. All labels in a UIScroll view.
I tried some, like when scroll I tried zoom scroll page, seems complex than I thought...
Anyone has good idea? Thanks!
Pretty simple really. Just stick a UIScrollViewDelegate to the scrollview and do something like this..
-(void)scrollViewDidScroll:(UIScrollView *)scrollView
{
for (int i = 0; i < [scrollView.subviews count]; i++) {
UIView *v = [scrollView.subviews objectAtIndex:i];
float position = v.center.x - scrollView.contentOffset.x;
float offset = 2.0 - (abs(scrollView.center.x - position) * 1.0) / scrollView.center.x;
v.transform = CGAffineTransformIdentity;
v.transform = CGAffineTransformScale(v.transform, offset, offset);
}
}
But, if you aren't impressed by the affine transform, you could still scale the rect using the offset factor and set the UILabel to adjustsFontSizeToFitWidth... and you are done!
Just make sure there is enough space! Else it could get out of hand very easily.
Assumptions :
There are no other views in the scrollview apart from these labels.
Its required to work just horizontally.
It could be done with CoreAnimation. You have to keep the index of the main label (that one in the center), and after scrolling is done or when scrolling starts (use some proper for you method in UIScrollViewDelegate) and simply shrink side labels by animation.
Just make the size of the font bigger (with an animation block) when it is the middle one, and smaller when it is not.
You can add a category to UILabel with -(BOOL)isMiddle and set it to true/false.

Calculating actual size of UITableView

I need to scroll my UITableWay programmatically. Scrolling is performed when user touches a button. I want my UITableWay scroll up until the bottom of the actual table reaches the bottom of the table's frame. I thought it would be easy to implement by doing something like this:
static CGFloat offset;
- (IBAction)pageUpClick:(id)sender
{
if (offset < ([rankingTableView numberOfRowsInSection:[rankingTableView numberOfSections] - 1] * rankingTableView.rowHeight) - rankingTableView.frame.size.height)
{
offset += 10.0;
[rankingTableView setContentOffset:CGPointMake(0, offset)];
}
}
but it doesn't work. The table just goes up even when the rows are over the bottom of the frame. i cannot find the actual size of my table.... but obviously it is quantity of rows * row height. since i want my scrolling stop when both the bottom of the table and the bottom of its frame lies on the same line i'm substracting the height of the frame. And all in vain. My only possible explanation is maybe i'm using wrong section number (i'm assuming it to be [rankingTableView numberOfSections] - 1 cause i got just one section ) ... but how can i check it? Any ideas? Thanks in advance
What about contentSize property? Try this:
CGSize mySize = [myTableView contentSize];

How can I stop UITable from overriding the content offset I set when it is decelerating?

I have a table that I'm doing some special loading for. The user starts scrolled to the bottom. When the user scrolls near the top, I detect this through scroll view delegate methods, and I quickly load some additional content, and populate more of the top part of the list. I want this to look seamless, like an "infinite scroll" upward. To do this, I have to set the content offset, so that the user doesn't see the table "jump" upward. When I scroll slowly, this works perfectly. When I scroll quickly, so that the table is decelerating, the content offset I set is ignored. Here is the code I'm using:
CGFloat oldHeight = self.tableView.contentSize.height;
CGFloat oldOffset = self.tableView.contentOffset.y;
self.tableContentsArray = newTableContentsArray;
[self.tableView reloadData];
CGFloat newHeight = self.tableView.contentSize.height;
CGFloat newOffset = oldOffset + (newHeight - oldHeight);
self.tableView.contentOffset = CGPointMake(0, newOffset);
So if I scroll up quickly with a table 100px high and hit the top while decelerating, I load more data, get a new table height of, say, 250px. I set the offset to 150. However, since it's still decelerating, the Apple code leaves the offset set to 150 for .1 seconds or something, then goes to the next calculated offset for deceleration, and sets the offset to 0, which makes it look to the user like they just skipped 150px of content, and are now at the new top of the list.
Now I'd LOVE to keep the acceleration from the list, so that it keeps going up for a while, slows down, and ends up somewhere around 120px offset, just like you would expect. Question is, how?
If I use [self.tableView setContentOffset: CGPointMake(0, newOffset) animated: NO]; it stops the content offset from being ignored, but stops the list dead.
We had an interesting situation like this at work a few months back. We wanted to use the UITableViewController because of it's caching, loading, and animations, but we wanted it to scroll horizontally (in your case it would be scroll upward). The solution? Rotate the table, then rotate the cells the other direction.
In your case, the code would look like this:
#define degreesToRadian(x) (M_PI * (x) / 180.0) in the header
- (void) viewDidLoad {
[super viewDidLoad];
self.table.transform = CGAffineTransformMakeRotation(degreesToRadian(-180));
...
}
Then rotate the cell, so it appears in the right orientation for the user
- (UITableViewCell *) tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
...
cell.transform = CGAffineTransformMakeRotation(degreesToRadian(180));
}
Now you can postpend your cells like any normal table, and they'll be added on top instead of the bottom.
I am afraid you are trying to do things too complicated without really understanding them.
Do you really have an infitite table or a very long table? I think it would be possible to just tell the table it has 1000000 cells. Each cell is loaded when you need it. And that's basically what you want to do.

Grouped UITableView with custom cells with subviews/layers in editing mode

I have a grouped UITableView with custom cells (created by subclassing UITableViewCell). I add subviews and insert sublayers just like this:
[self.contentView addSubview:myUILabel];
and
[self.contentView.layer insertSublayer:myCALayer];
When entering editing mode for deleting rows, the cells move right and myUILabel and myCALayer go beyond the borders of the cell, which looks ugly.
I tried this:
Grouped UITableView with custom UILabels in Editing Mode
... but it didn't help.
What "kind of" worked is to override setEditing: in my custom cell
- (void)setEditing:(BOOL)editing animated:(BOOL)animated {
CGRect oldBounds = self.myCALayer.bounds;
CGRect newBounds = oldBounds;
CGPoint oldPosition = self.myCALayer.position;
CGPoint newPosition = oldPosition;
//move right
if (editing && !self.showingDeleteConfirmation) {
newBounds.size.width -=32;
newPosition.x -= 15;
}
//move back left
else if (self.editing) {
newBounds.size.width += 32;
newPosition.x += 15;
}
self.myCALayer.bounds = newBounds;
self.myCALayer.position = newPosition;
[super setEditing:editing animated:animated];
}
... however, the animations are not performed synchronously. What happens is while the cell moves right when entering editing mode, first the width of the layer shrinks, and then the position changes. Although the layer finally fits the cell, the animation looks bad.
Thanks for your advice!
Not sure about your layers, but you should be able to set a smart autoresizingMask on your subviews so they re-layout appropriately when the size of contentView is altered by the tableview (on entry to edit mode).
More here.
Edit: with a little looking, I think CALayer's anchorPoint property may help with your sublayers. (more here)
Alternatively (and this is a bit of a hack, and could hurt performance somewhat), you could set your custom layers on a UIView w/ an appropriately set autoresizingMask.