Skip removing customly animated highlight from UITableViewCell when it's selected - ios

I have a custom animation in:
- (void)setHighlighted:(BOOL)highlighted animated:(BOOL)animated
of the UITableViewCell, something like:
[UIView animateWithDuration:0.15 animations:^{
self.center = CGPointMake(self.center.x + 110, self.center.y);
} completion:^(BOOL finished) {
isShifted = finished;
}];
The idea here is to shift the cell when the touch is down. Then if the cell is selected (no scrolling happened or cancel events) I proceed to another animation of the cell and transition to a new view.
The problem here is with an animation putting the cell back into its original position on setHighlighted:NO method call. Visually it looks like upon the selection the cell shifts (touch down), then starts going back (the system removes the highlight) and then starts the transition (the system calls setSelected:YES).
As far as I can see upon the selection of the cell in the table the following happens:
setHighlighted:YES
setHighlighted:NO
setSelected:YES
Is there any elegant solution to skip the call 2 and avoid the back-shifting-animation without introducing the timer that will check the selection?

You can think of using the delegate method tableView:shouldHighlightRowAtIndexPath:
- (BOOL)tableView:(UITableView *)tableView shouldHighlightRowAtIndexPath:(NSIndexPath *)indexPath {
// The cell has been touched, perform custom animation here
return NO;
}
setHighlight: will never be called on any cell, but you still have an entry point to perform the custom animation.

If you have a delegate for your UITableView (presumably your own view controller), you can catch the selection happening via something like "tableView: willSelectRowAtIndexPath:.
You can then do something "special" to your cell (such as set a property that forces the cell to do what you want in your animation) while the cell is selected.
And restore it back to what it was before when the cell is deselected via "tableView:willDeselectRowAtIndexPath:".
You also have the same type of delegate methods available for highlighting, e.g. "tableView:shouldHighlightRowAtIndexPath:"

Related

UIView transform property is being reset

I have a UICollectionView. I want a behaviour where when the user touches a cell, it scales down, as if it is getting pushed down slightly. I've accomplished this by using the UICollectionViewDelegate methods:
- (void)collectionView:(UICollectionView *)collectionView didHighlightItemAtIndexPath:(NSIndexPath *)indexPath
{
UICollectionViewCell *cell = [collectionView cellForItemAtIndexPath:indexPath];
[self scaleDownCell:cell];
}
- (void) collectionView:(UICollectionView *)collectionView didUnhighlightItemAtIndexPath:(NSIndexPath *)indexPath{
UICollectionViewCell *cell = [collectionView cellForItemAtIndexPath:indexPath];
[self scaleUpCell:cell];
}
My problem is that sometimes my cell will start to scale down, and then suddenly go back to full size, without performing the scale up animation, creating jerky effect. I have checked with breakpoints that it scales up before my scale up method is called. I am trying to figure out why.
The scale functions are as follows:
+ (void)scaleDownCell:(UICollectionViewCell *)cell
{
CGAffineTransform transform = CGAffineTransformMakeScale(0.95, 0.95);
[UIView animateWithDuration:SCALE_DOWN_ANIMATION_DURATION
delay:0.0
usingSpringWithDamping:SCALE_DOWN_SPRING_DAMPING
initialSpringVelocity:SCALE_DOWN_SPRING_VELOCITY
options:0
animations:^{
cell.transform = transform;
}
completion:^(BOOL finished) {
}];
}
+ (void)scaleUpCell:(UBCollectionViewCell *)cell
{
CGAffineTransform transform = CGAffineTransformMakeScale(1.0, 1.0);
[UIView animateWithDuration:SCALE_UP_ANIMATION_DURATION
delay:0.3
usingSpringWithDamping:SCALE_UP_SPRING_DAMPING
initialSpringVelocity:SCALE_UP_SPRING_VELOCITY
options:0
animations:^{
cell.transform = transform;
}
completion:^(BOOL finished) {
}];
}
EDIT: I basically want to replace highlighting in my cells with this scaling behaviour, it serves the exact same purpose and I want it to happen in exactly the same situation that the cell would be highlighted, so it seemed like an appropriate place to apply my transformation.
Regardless, I had first tried overriding the UIResponder touch methods in the cell subclass, but was observing the same behaviour. The sudden scale up happens even if I delete the scale up method so that it is never called, so my scale down is not being cancelled by the scale up animation. I can see through log statements that my scale down is called once, and the animation completion block is getting called with finsished == YES. The cell is getting scaled back up BEFORE the completion block is called (I have set the animation duration to be extra long to help in debugging). This seems to happen when I touch on the cell and then quickly scroll, so I guess the scrolling has something to do with it.
The problem is that you are hijacking the notion of "highlighting" for something that it isn't. Highlighting is a complicated and confusing business. In the course of being tapped and selected, the cell highlights and unhighlights more than once. Thus it is the wrong thing to respond to.
If what you are trying to detect is a touch, then you should respond to touch. If a gesture recognizer won't do, then use a cell subclass of your own so that you can implement UITouch detection directly.
One more thing to keep in mind is that, the way you've written this, if the scale up happens while the scale down is in progress, it will just kill it dead.
EDIT - One final thought - in your edit, you make a comment about scrolling having something to do with this. That makes sense. Think how a collection view layout works - it is responsible for the attributes of each cell, including its transform (https://developer.apple.com/library/ios/documentation/uikit/reference/UICollectionViewLayoutAttributes_class/Reference/Reference.html). So naturally if you change the transform of the cell yourself, and you don't tell the layout, and the layout takes charge, it will cancel your transform.

UICollectionView: activating, scrolling to, and assigning first responder to distant cell

I've got a text field inside of a UICollectionViewCell that can receive first-responder status. The cell currently isn't visible on-screen, and I want to scroll to the cell based off of a button hit from a UISegmentedControl. There's two segments to this control… and a hit to the second segment should scroll to the first cell in the 2nd section of the UICollectionView. After this happens, the cell should get selected programatically, and then the text field inside of that cell is supposed to get first responder status and bring up the keyboard.
What's happening now (inside my action method from a value change from the segmented control) is that a call to -[UICollectionView selectItemAtIndexPath:animated:scrollPosition:] isn't scrolling to it at all (and I'm using UICollectionViewScrollPositionTop; may as well be "…None"). If I thumb down the list manually, the cell is indeed selected (it gets a darker background color in that state), but the text field certainly doesn't have first responder status.
To fix the scroll problem, I've been able to ascertain the position of the cell in the list, and scroll to the cell's content offset (I've also used scrollRectToVisible here). Then I manually select it (as well as telling the delegate to fire its appropriate method as well, where the cell's text field gains first responder status).
- (void)directionSegmentedControlChanged:(UISegmentedControl *)sender {
NSIndexPath *path = [NSIndexPath indexPathForItem:0 inSection:sender.selectedSegmentIndex];
UICollectionViewLayoutAttributes *attributes = [self.collectionView layoutAttributesForItemAtIndexPath:path];
[self.collectionView setContentOffset:attributes.frame.origin animated:YES];
[self.collectionView selectItemAtIndexPath:path animated:NO scrollPosition:UICollectionViewScrollPositionNone];
[self.collectionView.delegate collectionView:self.collectionView didSelectItemAtIndexPath:path];
}
- (void)collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPath:(NSIndexPath *)indexPath {
BDKCollectionViewCell *cell = (BDKCollectionViewCell *)[collectionView cellForItemAtIndexPath:indexPath];
[cell.textField becomeFirstResponder];
}
The problem here is that the cell as it's seen in -[collectionView:didSelectItemAtIndexPath:] is nil, because it's not in the visible cell set of the collection view when the method gets fired.
What's the best way to solve this? I've tried tossing my scrolling code inside of a [UIView animateWithDuration:animations:completion:] block, and assigned first responder upon completion there, but manually animating the collection view in this manner neglects to load any of the cells that should be scrolled past. Any ideas?
Update: many thanks to #Esker, who suggested I simply perform the "focus selection" action after a delay using Grand Central Dispatch. My solution ended up looking like this.
- (void)directionSegmentedControlChanged:(UISegmentedControl *)sender {
NSIndexPath *path = [NSIndexPath indexPathForItem:0 inSection:sender.selectedSegmentIndex];
UICollectionViewLayoutAttributes *attributes = [self.collectionView layoutAttributesForItemAtIndexPath:path];
[self.collectionView setContentOffset:attributes.frame.origin animated:YES];
dispatch_time_t startAfter = dispatch_time(DISPATCH_TIME_NOW, 0.28 * NSEC_PER_SEC);
dispatch_after(startAfter, dispatch_get_main_queue(), ^{
[self.collectionView selectItemAtIndexPath:path animated:NO scrollPosition:UICollectionViewScrollPositionNone];
[self collectionView:self.collectionView didSelectItemAtIndexPath:path];
});
}
I had a similar challenge with a UITableView: scrolling to a cell that was not yet visible, and assigning first responder to a UITextField within the target cell once it was visible. Here's a simplified description of how I handle this. I imagine this approach could work with a UICollectionView, but I don't have much experience with collection views.
If the desired cell/text field is currently visible, immediately send it becomeFirstResponder, and scroll to the cell if desired.
Otherwise, set a property in your view controller or a similar class that indicates that a text field needs focus, and which one needs focus
Tell the table view/collection view to scroll to the desired index path
In collectionView:cellForItemAtIndexPath:, you could try to check that property to see if a text field at the given indexPath needs to get focus, and if so, send it becomeFirstResponder immediately, but I found this won't work if the cell is scrolling into view, presumably because at this point, when you're configuring the new cell, it's not yet actually in the view hierarchy. So I added a check, if becomeFirstResponder returns NO at this point, I try again after a delay:
dispatch_after(someDelay, dispatch_get_main_queue(), ^(void){
[self getFocus:textField];
});
The getFocus method will both send becomeFirstResponder to the text field and clear that property that tracks which text field needs focus.
My actual implementation is somewhat specialized for the view model associated with my table view, and encapsulated in a couple of classes and using some KVO, but I wanted to avoid that and focus on the minimum required logic in the description above.

Make UITableViewCell behave like a real button

I want to make UITableViewCell to behave like real button.
Until know I have been using the
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath`
trick, but this is not optimal because it doesn't behave like a button on tap/drag/release.
If you tap a cell row and drag your finger over the cell, it will not get selected when you release your finger (but a button would launch its action in the same case).
Is there any simple way of making a UITableViewCell to behave like a real button without resorting to insert an actual UIButton inside the cell?
You can just create table view cells with a button in them, set the buttons tag to the row so you can workout which row the button belongs to when you receive the button event. Just make sure you reset the buttons tag when you return a reused table view cell instead of creating a new one.
Subclass the UITableViewCell and use the following method:
- (void)setHighlighted:(BOOL)highlighted animated:(BOOL)animated {
[super setHighlighted:highlighted animated:animated];
if (highlighted) {
_backgroundImageView.image = [UIImage imageNamed:#"img_h"];
}
else {
_backgroundImageView.image = [UIImage imageNamed:#"img"];
}
}
Where img is a plain image and img_h is a highlighted version of that image.
One way is to create a UIButtton of size of your cell and added it to the cell.
Or else you could simply add a UITapGestureRecognizer to your UITableViewCell and that will do the work for you.

Change image with animation in reused UITableViewCell

In my application I have a feed with posts. Each post is represented by a cell in UITableView. A post has two states: read or unread. Each cell has an UIImageView, which has to display different images based on this state. I set this imageView's image in my table controller's tableView:cellForRowAtIndexPath: method.
I also check visibility of cells while scrolling the table, and, when a cell becomes visible, it's state is changed, so cell's image is changed with animation. This animation, however, is not working properly when cell is reused. First cell to become visible is animated properly, but all cells after the first one already have resulting image when they appear on screen, animation is not fired for them. Here's how I'm doing it:
// This is called when a cell becomes visible
- (void)setIsVisible:(Boolean)isVisible
{
if (isVisible)
{
UIImage * toImage = [UIImage imageNamed:#"img-cell-read.png"];
[UIView transitionWithView:self.myImageView
duration:2.0f
options:UIViewAnimationOptionTransitionCrossDissolve
animations:^{
self.myImageView.image = toImage;
} completion:NULL];
}
_isVisible = isVisible;
}
I realize that I should somehow reset this animation and start it again when I'm reusing a cell, but I don't understand how exactly should I do it. Also, if I try to animate cell's background color instead of image, it works fine with reusing.
You can override the prepareForReuse method in UITableViewCell to cancel any animations and also reset the image to the original state.
- (void)prepareForReuse{
[super prepareForReuse];
[self.myImageView.layer removeAllAnimations];
self.myImageView.image = YOUR_INITIAL_UIIMAGE;
}
also make sure to import quartz:
#import <QuartzCore/QuartzCore.h>

UITableView callback after row deletion animation complete

I have a table with shadows above the top and below the bottom cell (using Matt Gallagher's solution here: http://cocoawithlove.com/2009/08/adding-shadow-effects-to-uitableview.html). These are added in the layoutSubviews method of the UITableView class extension.
I dynamically add and delete cells below each main cell (these provide additional data) - let's call these "detail" cells. There is only one ever open at a time. When deleting the "detail cell" beneath the last main cell, as the animation begins, the shadow flicks upwards to the last cell (above the detail cell). It does this because the layoutSubview methods considers the last cell of the table to have changed the moment the animation for deleteRowsAtIndexPaths begins (rather than when the animation ends).
So, in essence, I need a way to keep the shadow below the detail cell as its being deleted. Not sure of the best way to do this. If the UITableView no longer considers that cell to be the last cell, then I am not sure even how to get the cell (since the UITableView gets the cell thus):
NSIndexPath *lastRow = [indexPathsForVisibleRows lastObject];
if ([lastRow section] == [self numberOfSections] - 1 &&
[lastRow row] == [self numberOfRowsInSection:[lastRow section]] - 1)
{
//adds shadow below it here
}
So even trapping the start of the animation is not much use if the UITableView still thinks the main cell above the "detail" cell is the "lastObject".
Thanks for any ideas.
Try this
[CATransaction begin];
[tableView beginUpdates];
//...
[CATransaction setCompletionBlock: ^{
// Code to be executed upon completion
}];
[tableView deleteRowsAtIndexPaths: indexPaths
withRowAnimation: UITableViewRowAnimationAutomatic];
[tableView endUpdates];
[CATransaction commit];
I am sure that you can easily achieve this by using a custom table view class instead of using dependencies from external frame work just inherit from the uitable view and add subviews to it.
But if you insist to keep it this way. take a reference in your own variable before deleting it.
Swift (the idea is the same, you can of course use this in obj-c):
UIView.animateWithDuration(0.3, animations: { () -> Void in
self.tableView.scrollToRowAtIndexPath(indexPathes, withRowAnimation: UITableViewRowAnimation.None)
}, completion: { (Bool) -> Void in
// The logic you want to execute after the animation
})

Resources