Read position of UIPickerView while scrolling? - iphone

I have a UIPickerView with custom cells. For every moment, I'd like to know which cell is in the center location (behind the tinted selection indicator). The UIPickerView delegate method
- (void)pickerView:(UIPickerView *)pickerView didSelectRow:(NSInteger)row inComponent:(NSInteger)component
does provide this information, but only after the picker view has stopped. What I'm looking for is a way to retrieve the currently "selected" cell while the picker view is still spinning and has not yet come to a halt.
As a work around, I've tried to go through KVO and register for each cell's view changes:
- (UIView *)pickerView:(UIPickerView *)pickerView viewForRow:(NSInteger)row forComponent:(NSInteger)component reusingView:(UIView *)view {
.
.
[cell addObserver:self forKeyPath:#"frame" options:NSKeyValueObservingOptionOld|NSKeyValueObservingOptionNew context:nil];
}
I do receive callbacks to observeValueForKeyPath:ofObject:change:context:, however no matter whether I scroll the picker view up or down, the received values seem to be always the same:
2011-03-09 08:00:34.429 Project[35069:207] Frame: x=2.00, y=-13.00
2011-03-09 08:00:34.430 Project[35069:207] Location: x=4.00, y=-26.00
2011-03-09 08:00:34.430 Project[35069:207] Change: {
kind = 1;
new = "NSRect: {{2, -13}, {118, 70}}";
old = "NSRect: {{2, -13}, {118, 70}}";
}
2011-03-09 08:00:34.431 Project[35069:207] Frame: x=2.00, y=-13.00
2011-03-09 08:00:34.431 Project[35069:207] Location: x=4.00, y=-26.00
2011-03-09 08:00:34.432 Project[35069:207] Change: {
kind = 1;
new = "NSRect: {{2, -13}, {118, 70}}";
old = "NSRect: {{2, -13}, {118, 70}}";
(Location coordinates are in window frame)
Any other ideas what I could try to get that value?

I think the method:
- (UIView *)pickerView:(UIPickerView *)pickerView viewForRow:(NSInteger)row forComponent:(NSInteger)component reusingView:(UIView *)view`
with:
[pickerView selectedRowInComponent:0]);
Is exactly what you want.

I think this is interesting. I wanted to do the similar thing with UIPicker, but haven't looked into much yet.
UIDatePicker actually does this, you can drag time around noon and it will toggle AM/PM even though the finger has not lifted from the screen (it should not have called "didSelectRow" yet).
UIDatePicker actually subclasses from UIControl, instead of UIView like UIPickerView. So, UIDatePicker is probably observing all UIControlEvents to do this.
Observing -pickerView:titleForRow:forComponent probably works for now, but if behavior changes later (ie caching behavior, ie) is changed, this will break...
One suggestion is to try KVO on center property, instead of frame.
Edit1: By the way, how does your observeValueForKeyPath method look like?
Edit2: Upon further research, it seems most of UIKit is not KVO compliant.
See this answer -
Key value Observing during UIView Animations
So, it seems like only option is to keep poling for the view's value when it's visible. How about forloop and pole frame/center property whenever it's in view?

You can use your picker delegate's -pickerView:titleForRow:forComponent: method to give you a pretty good idea of where the picker's wheel is. As you scroll in one direction or the other, the picker will ask its delegate for the title (or view) for a row. The number of rows between the "center" of the wheel and the row the picker is asking for depends on the row height, but you'll likely know what that is.
You'll have to experiment a bit to get it right. The picker seems to ask for one or two rows that aren't yet visible, perhaps to keep the scrolling smooth. In my test with a picker that displays 5 rows, there seems to be about a 3-row difference between the row the picker was asking for and the center. You'll obviously also have to keep track of the direction of rotation (up or down), and you won't find out if the first or last two or three rows are selected until your -pickerView:didSelectRow:inComponent: is called.

Just a quick thought, but you could add a UIScrollView (transparent with a contentView that is as tall as the number of components.) over the UIPickerView.
Then listen to the UIScrollViewDelegate methods to get the live scrolling data you need.
The tricky part would be passing along the touch events from the scroller to the picker so that it would act as expected.

The problem you have with UIPickerView is that the view you are setting for the cells are actually subViews.
UIPickerView is built from various imageViews, a TableView and a selection line.
The view you provide when using viewForRow are inserted as subViews of the tableView's cells and so their frame is never changed.
The only frame that changes is the frame of the actual tableView cell, to which of course you have no access.
A possible solution is to simply replace the UIPickerView's tableView with your own.
You can do this by setting the number of rows for the UIPickerView to be 0, and place a transparent tableView over the UIPickerView. Your tableView will hold the actual cells, and since a UITableView inherits from UIScrollView, you can get events about the scroll and simply check which UITableViewCell is currently under the selection line.

The frame does not change because the cell is not moving. One of its superviews is, and the cell is carried along with it. To figure out the cell's coordinate in the parent UIPickerView, try this:
CGRect theApparentRect = [theCell convertRect: theCell.bounds toView: thePicker];
The resultant rectangle will be in the coordinate system of thePicker, and you can then figure out if it's in the middle with the following simple test:
CGPoint middleOfThePicker = CGPointMake
(
CGRectGetMidX(thePicker.bounds),
CGRectGetMidY(thePicker.bounds)
);
Boolean isInMiddle = CGRectContainsPoint(theApparentRect, middleOfThePicker);

Related

Pushing a view controller after scrolling is complete

When a user adds an item to my list, I want to scroll to the new row, highlight it, and select it (which will push a new controller). The key part is waiting for the scroll animation to complete before pushing the new controller.
In this answer, I learned how to use the animation delegate to wait until the scroll is complete.
However, if the insertion row is already on scree, the table view will not scroll and the method will not fire.
How can I wait to push the new controller until the end of the scroll, and deal with the case where no scroll will be initiated - and how might I tell the difference between each case?
The easiest way to check whether a given row is visible in your table view is something like this:
if (!CGRectContainsRect([self.tableView bounds], [self.tableView rectForRowAtIndexPath:indexPath])
{
// the row is partially outside the table view’s boundaries and needs to be scrolled for full visibility
}
else
{
// the row is within the boundaries and does not need to be scrolled
}
Try creating a method to see if scrolling is needed. If no scrolling is needed, call the push right away, otherwise wait for the delegate call and push.
- (BOOL)isSrollingingNeededForIndexPath:(NSIndexPath *)indexPath {
NSArray *visibleIndices = [self.tableView indexPathsForVisibleRows];
for (NSIndexPath *visibleIndexPath in visibleIndices)
if ([indexPath compare:visibleIndexPath] == NSOrderedSame)
return NO;
return YES;
}
Edit: Good point. Since indexPathsForVisibleRows is used for data rendering.
You could do essentially the same thing with indexPathsForRowsInRect where you use the content.offset.y and the tableview.frame.size.height to determine your "visible rect".
Then to account for partially visible rows at the top and bottom you could add rowHeight-1 to the top of the rect and subtract rowHeight - 1 from the bottom of the rect. Code shouldn't be too gnarly if you have static height rows. If you have varying height rows it would still work, but it would be a bit more involved.
All said though, it seems like a lot of code for something which you'd think would have a simple answer.

How to get the mins and secs in UIPickerView as in figure?

How can we achieve such type of selected values to be changed with mins and secs in UIPickerView???
First, you use the pickerView:viewForRow:inComponent:reusingView: data source method to return the "00" label. This is actually a bit tricky, since pickerview will alter the frame of this view to fill the row. That means you have to embed your properly positioned view inside another view, and then return that container view instead.
As for the labels, those are simply UILabel objects that have been positioned correctly and added as subviews of the pickerview itself.
And to get them to update correctly, just change their titles in the pickerView:didSelectRow:inComponent: method.
This is exactly what UIDatePicker is doing.
You should implement UIPickerViewDelegate Protocol and UIPickerViewDataSource Protocol.
In method - (NSString *)pickerView:(UIPickerView *)pickerView titleForRow:(NSInteger)row forComponent:(NSInteger)component just return needful title.
You can also add delegator on valueChanged event of UIPickerView. And each time your value changed you can reload all visible rows and set labels mins and secs at right positions.
Oooor you can just add subview with 2 UILabels over the black line.
Oooor you can add subview with black gradient line with labels mins and secs.
Then using method : - (CGFloat)pickerView:(UIPickerView *)pickerView widthForComponent:(NSInteger)component you can set appropriate width of your columns.

Objective-C: Is there any event when a Table Cell goes away?

Is there any message I can override called when a Table Cell goes away (on scrolling the table)?
I think it should be something like dealoc.
I'm asking this because I have below situation:
I have a table with many cells (100+) and each of this cell contains a ImageView. For loading the image (from a URL) I'm using NSOperationQueue/NSInvocationOperation. The problem appears when user is scrolling the table before the image is completely loaded: because I'm reusing the cells the image is displayed in wrong cell.
To avoid this I'm thinking to use "cancelAllOperations" of NSOperationQueue object when the cell goes away.
Note: I've tried but is not working if I call this message on "prepareForReuse".
iOS 6:
- (void)tableView:(UITableView *)tableView didEndDisplayingCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath {
// Cancel operation here for cell at indexPath
}
Why not keeping the images in your table data source - the array that holds all the data for the table?
This way you won't have to load these images once again when scrolling back and it will solve your problem...
You can subclass UITableViewCell (or any UIView) and override willMoveToWindow:. It is called whenever the cell appears (or scrolls off screen).
When it goes out of the window the parameter will be nil:
- (void)willMoveToWindow:(UIWindow *)newWindow
{
[super willMoveToWindow:newWindow];
if (newWindow==nil) {
// Cell is no longer in window
}
}
If there were, it would be in the UITableViewDelegate class reference: http://developer.apple.com/iphone/library/documentation/UIKit/Reference/UITableViewDelegate_Protocol/Reference/Reference.html#//apple_ref/occ/intf/UITableViewDelegate
The only thing they have is willDisplayCell which lets you do last minute adjustments BEFORE the cell appears. They don't have anything for when it disappears, but you could probably figure that out since there are only a certain number of cells on the screen at a time for a given cell height.
So if one is appearing and for a cell height of 80 for instance (in portrait mode so 480px screen height), then you can say that the one 6 cells away is about to disappear (6 cells * 80 pixels = 480). There are a couple other things to consider like which way you are scrolling, but you get the general idea.
Example Code: You should also look at lazy table loading via Apple's sample code http://developer.apple.com/iphone/library/samplecode/LazyTableImages/Introduction/Intro.html
I had the same issue and got some nice feedback in the developer forum. Quinn - The Eskimo from Apple:
As an aside, cancelling a network
small transfer because something has
scrolled off the screen is probably a
performance negative. For small
transfers, it's usually more efficient
to let it run to completion (and cache
the results in case they're needed in
the future). This is because of the
way that NSURLConnection manages HTTP
connection reuse. If you cancel a
transfer, NSURLConnection has to
either a) drop the underlying HTTP
connection on the floor, which means
it can't be reused, or b) continue
reading and just junk the data.
Neither of this is the best use of
resources.
Share and Enjoy
-- Quinn "The Eskimo!"
So, I'm not cancelling all the ImageDownload Operation, but rather only start them, when the user stops scrolling. Up to then only a placeholder is shown:
- (void)scrollViewWillBeginDragging:(UITableView *) tableView
{
self.dragging = TRUE;
}
- (void) scrollViewDidEndDragging: (UITableView *) tableView willDecelerate: (BOOL) decelerate
{
if(!decelerate && self.dragging)
[self loadThumbsForVisibleCells];
else
self.dragging = FALSE;
}
- (void) scrollViewDidEndDecelerating: (UITableView *) tableView
{
[self loadThumbsForVisibleCells];
}
Hope this helps!

How do I resize the UITableView's height dynamically?

In my app, I would like to resize the tableview's height when it's in edit mode vs when it's not (in order to make room for editing controls below the table view)
How should this be done?
I found that manipulating the "bounds" property can result in some unexpected behavior when you have a floating table inside another view. Sometimes the table expands upward when increasing the height, even though the origin is still 0,0.
The "frame" property might be more effective:
CGRect tvframe = [tableView frame];
[tableView setFrame:CGRectMake(tvframe.origin.x,
tvframe.origin.y,
tvframe.size.width,
tvframe.size.height + 20)];
You need to set the bounds of the tableview:
CGRect tvbounds = [tableView bounds];
[tableView setBounds:CGRectMake(tvbounds.origin.x,
tvbounds.origin.y,
tvbounds.size.width,
tvbounds.size.height + 20)];
You can implement the table view delegate's tableView:willBeginEditingRowAtIndexPath: and tableView:didEndEditingRowAtIndexPath: to figure out when a given row (and thus the table) goes into and out of editing mode, respectively.
From there, you can just tweak the value of the frame.size.height property of the table view (optionally inside a UIView animations block) as appropriate. It may also be helpful to check the editing property of the table view as a whole inside the delegate methods, in case you receive multiple calls to either delegate method before receiving any calls to their complementary methods.
More info:
UITableView: editing property
UITableViewDelegate: tableView:willBeginEditingRowAtIndexPath: method (didEndEditing is also in this document)

How to make a custom drawn UITableViewCell resize properly?

For performance reasons, I draw the strings for my UITableViewCell in a custom view that overrides its drawRect method to draw strings directly in the view rectangle using NSString:drawInRect. This is similar to Apple's TableViewSuite Example 5-CustomTableViewCell.
However, when I invoke setEditing on the cell to bring up the delete button, the view ends up with a squeezed appearance after the animation completes. To demonstrate this, invoke setEditing:YES on the CustomTableViewCell example mentioned above and observe the distortion. Is there any way around this or should I just revert back to using UILabels for my text?
I had a similar problem with a UIView inside a UITableViewCell. I solved it by changing the UIView's contentMode to UIViewContentModeLeft. (I wrote it up here, with screenshots.)
I had this problem too, and in my case I fixed it by handling the 2 states in my drawRect method, one while editting, the other while not. In other words I accounted for the size of the delete button, and got my UI to repaint the cell differently. I'm not sure if it's the most efficient way to go, but here is the code that I used to force a repaint:
-(void)_refreshTableAndCells{
//refresh the table
[myCustomTableView reloadData];
//refresh all the visible cells
for (UITableViewCell *cell in myCustomTableView.visibleCells){
LocationCellView *locationCell = [cell.contentView.subviews objectAtIndex:0];
[locationCell setNeedsDisplay];
}
}
I'm an Objective-C n00b though, so I'd be more than happy for someone to suggest a better way than this.
I usually just modify the x and width values (or whatever else) of whatever I want to be different when editing or not. UITableView automatically calls layoutSubviews when you begin editing, so you don't have to loop through your cells and do it yourself.
- (void)layoutSubviews {
[super layoutSubviews];
CGFloat editingPadding = 5.0;
self.textLabel = CGRectMake((self.editing ? self.textLabel.frame.origin.x + editingPadding : self.textLabel.frame.origin.x), self.textLabel.origin.y, (self.editing ? self.textLabel.frame.size.width - editingPadding : self.textLabel.frame.size.width), self.textLabel.frame.size.height);
}
Try setting the contentMode of your own custom view (which resides inside the cell's contentView) to UIViewContentModeLeft. The "squeezing" is due to the fact that the default contentMode is UIViewContentModeScaleToFill.