I've been working on a new app and was really hoping to implement a swipe to reveal more options menu inside my application. I've searched and searched, but it seems no one else has successfully made it work (aside from Loren). What I'm trying to do is swipe the cell, and simultaneously use CABasicAnimation to push it to x: 320, and add a subview below this that would have the buttons etc.. I'm using willBeginEditing to detect the swipe to avoid subclassing. Here's the code:
- (void)tableView:(UITableView *)tableView willBeginEditingRowAtIndexPath:(NSIndexPath *)indexPath {
UITableViewCell *cell = [tableView cellForRowAtIndexPath:indexPath];
CABasicAnimation *theAnimation;
theAnimation=[CABasicAnimation animationWithKeyPath:#"transform.translation.x"];
theAnimation.duration=0.0;
theAnimation.repeatCount=0;
theAnimation.autoreverses=NO;
theAnimation.removedOnCompletion = NO;
theAnimation.fillMode = kCAFillModeForwards;
theAnimation.fromValue=[NSNumber numberWithFloat:0];
theAnimation.toValue=[NSNumber numberWithFloat:+320];
[cell.layer addAnimation:theAnimation forKey:#"animateLayer"];
CGRect frame = CGRectMake(0, 59 * indexPath.row, 320, 59);
UIView *menu = [[[UIView alloc] initWithFrame:frame] autorelease];
NSString *path = [NSString stringWithFormat:#"%#%#",
[[NSBundle mainBundle] resourcePath],
#"/flick.wav"];
SystemSoundID soundID;
NSURL *filePath = [NSURL fileURLWithPath:path isDirectory:NO];
AudioServicesCreateSystemSoundID((CFURLRef)filePath, &soundID);
AudioServicesPlaySystemSound(soundID);
self.menu.backgroundColor = [UIColor colorWithPatternImage:[UIImage imageNamed:#"dots.png"]];
[self.view addSubview:menu];
[self.view sendSubviewToBack:menu];
}
- (void)animationDidStop:(NSString*)animationID finished:(BOOL)finished context:(void *)context
{
// Release
[self release];
}
#pragma mark - Swipe Menu II
- (void)scrollViewDidScroll:(UIScrollView *)tableView {
UITableViewCell *cell = [tableView cellForRowAtIndexPath:nil];
CABasicAnimation *theAnimation;
theAnimation=[CABasicAnimation animationWithKeyPath:#"transform.translation.x"];
theAnimation.duration=0.2;
theAnimation.repeatCount=0;
theAnimation.autoreverses=NO;
theAnimation.fromValue=[NSNumber numberWithFloat:320];
theAnimation.toValue=[NSNumber numberWithFloat:0]
;
[cell.layer addAnimation:theAnimation forKey:#"animateLayer"];
}
The problem is the sliding back of the cell - I want to do it when any touch outside the menu view is received, but because it is a scrollView, I can't. The ScrollViewdidScroll Method only animates the cell back to its normal place once it is scrolled of the viewport. (As in, under the NavigationBar or object that obscures it) The last key issue is the ability to detect if a menu is already visible or active and a cell is already off of the screen, slide the cell back to its original position, remove the menu view, and then slide the other cell out and add the menu.
I would like to be the first beside Loren to implement this as so many others have tried, especially on StackOverflow.
I apologize for the poor formatting in the code.
Thanks in advance,
Kolin
I've actually made this work quite nicely, you can check out the project here: http://www.thermoglobalnuclearwar.com/opensource/
It also overcomes the problem in Tweetie where you cannot swipe the same cell twice without first swiping something else.
If you make any constructive changes, or need any help, let me know!
Loren's implementation has a fatal flaw, which is if you swipe on a cell, then scroll the table, you cannot re-swipe the same cell until you swipe a different cell. I believe this is because the tableview believes the cell is still being edited until you try and "edit" another cell.
You may want to investigate doing something else instead, like using a UISwipeGestureRecognizer attached to the cell to detect the swipe.
I can think of 2 approaches to try and detect taps outside of the cell as well. The first is to subclass UIApplication and override -sendEvent:. You can use this to detect the tap event and dismiss the cell "editing". The main problem here is giving the UIApplication subclass knowledge about your cell. The second approach is to slap a custom UIView subclass over the entire screen and in -hitTest:withEvent: you can test to see if the touch belongs to the area over your cell. If it doesn't, dismiss the cell "editing". In both cases, return nil to allow the event to proceed to the underlying views (and don't forget to remove this overlay view after the event happens). You may also want to test the UIEvent to make sure it contains a new touch before dismissing the cell.
Related
If you look closely to the bottom of the UISearchBar in a UISearchDisplayController, you'll notice it has a subtile drop shadow. This shadow doesn't fit in the design of the app I'm currently working on, so I'm trying to remove/hide it. Unfortunately I have not yet succeeded.
During my research into this drop shadow, I found that it's not part of the UISearchBar. When I remove the UISearchDisplayController's UISearchBar from its superview in - (void)searchDisplayControllerWillBeginSearch:(UISearchDisplayController *)controller, the shadow remains visible.
The shadow turned out to be part of the UISearchDisplayController's searchResultsTableView: when I hide the searchResultsTableView, the shadow disappears. However, I have not been able to trace down the view that has the shadow on its layer. I tried recursively iterating through all visible views (starting at [[UIApplication sharedApplication] window]) and then hiding the drop shadow of each view and setting its clipsToBounds property to YES, which also did not yield the desired result.
Any ideas?
I finally found a solution. Since setting clipsToBounds to YES and hiding the drop shadow of each view in the hierarchy didn't work, it's obvious that the shadow is an image. After iterating through all subviews of the searchResultsTableView and printing their class property, I found an instance of _UISearchBarShadowView, which obviously is the culprit. So what I'm doing now is finding the _UISearchBarShadowView and setting its alpha to 0.0f.
- (void)searchDisplayController:(UISearchDisplayController *)controller didShowSearchResultsTableView:(UITableView *)tableView
{
[self _findAndHideSearchBarShadowInView:tableView];
}
- (void)_findAndHideSearchBarShadowInView:(UIView *)view
{
NSString *usc = #"_";
NSString *sb = #"UISearchBar";
NSString *sv = #"ShadowView";
NSString *s = [[usc stringByAppendingString:sb] stringByAppendingString:sv];
for (UIView *v in view.subviews)
{
if ([v isKindOfClass:NSClassFromString(s)]) {
v.alpha = 0.0f;
}
[self _findAndHideSearchBarShadowInView:v];
}
}
The accepted answer uses a private API, which could get your app rejected. Instead, I'd just locate and hide any subviews that are also custom subclasses of UIImageView, like so:
- (void)searchDisplayController:(UISearchDisplayController *)controller didShowSearchResultsTableView:(UITableView *)tableView
{
for (UIView *view in tableView.subviews) {
if ([view.class isSubclassOfClass:[UIImageView class]]) {
view.alpha = 0.f;
}
}
}
I'm currently working on an iPhone app that's doing some strange things with a UIScrollView inside a UITableView. This is my first foray into iPhone dev, so I'm sure it's something silly I'm missing.
In each UITableViewCell I am putting in a basic UITableViewCell. In that UITableViewCell is a Label and a UIScrollView.
The label and scrollview is setup and working properly, but when it first displays it is offset about 30 pixels down on the y axis than it should be, or is positioned by the XIB/NIB. I am not moving it around manually. The label shows up in the right spot. at 0,0. The UIScrollView should be showing up at 0,22 but is showing up closer to 0,40.
When I swipe to scroll the containing UITableView, then all the UIScrollViews will show up in the right spot assuming that when the UITableView scrolled that UITableViewCell went offscreen.
Here is the code for the UITableView.cellForRowAtIndexPath
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
static NSString *CellIdentifier = #"GalleryRowCell";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
if (cell == nil) cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:CellIdentifier] autorelease];
[cell.layer setMasksToBounds:TRUE];
[cell.layer setCornerRadius:10.0];
Contagion *c = (Contagion *)[self.dataSet objectAtIndex:indexPath.row];
GalleryRowViewController *subView = [[GalleryRowViewController alloc] initWithContagion:c];
[cell.contentView addSubview:subView.view];
subView.contagionName.text = c.name;
subView.contagion = c;
return cell;
}
Here is the code for my GalleryRowViewController.viewDidLoad
- (void)viewDidLoad {
[super viewDidLoad];
self.imageScroll.delegate = self;
[self.imageScroll setBackgroundColor:[UIColor blackColor]];
[self.imageScroll setCanCancelContentTouches:NO];
self.imageScroll.indicatorStyle = UIScrollViewIndicatorStyleWhite;
self.imageScroll.clipsToBounds = NO;
self.imageScroll.scrollEnabled = YES;
self.imageScroll.pagingEnabled = NO;
NSInteger x = 0;
CGFloat xPos = 0;
for (x=0;x<=10;x++) {
UIImage *image = [UIImage imageNamed:#"57-icon.png"];
UIImageView *imageView = [[UIImageView alloc] initWithImage:image];
[imageView setBackgroundColor:[UIColor yellowColor]];
CGRect rect = imageView.frame;
rect.size.height = 70;
rect.size.width = image.size.width;
rect.origin.x = xPos;
rect.origin.y = 5;
imageView.frame = rect;
[self.imageScroll addSubview:imageView];
xPos += imageView.frame.size.width+5;
}
[self.imageScroll setContentSize:CGSizeMake(xPos, [self.imageScroll bounds].size.height)];
}
--- EDIT FOR IMAGES ---
After App Loads: http://img809.imageshack.us/img809/4576/screenshot20110927at427.png
After Scrolling the rows offscreen and back: http://img690.imageshack.us/img690/9461/screenshot20110927at428.png
Well, as my previous response was at too low a level, let me take another shot at it.
First, I just noticed the core problem that you're using a viewcontroller for each cell. To quote Apple, " "A single view controller typically manages the views associated with a single screen’s worth of content." That would also get rid of your XIB (just manually configuring your scrollview), which I bet will get rid of your problem.
To proceed, your main choice is whether to create a ContagionTableViewCell class or not as suggested by Scott.
If so, following the Elements example, create a subclass of UITableViewCell ContagionTableViewCell with properties of a scrollView, a labelview and a contagion. Like they use a custom setter for the element, use one for the contagion, so that whenever it is assigned, it also updates the cells label (and associated pictures).
Move your imageScroll code from GalleryRowViewController.viewDidLoad into the ContagionTableViewCell init code. Put the image code into a new routine, which will be called from the contagion setter.
If NOT, then move the GalleryRowView Controller code into your UITableView. I suggest you take a look at cellForRowAtIndexPath in Apple's tableViewSuite, the fourth example on subviews. In particular, it shows this pattern of separating the creation of a cell (when you need a brand new one) vs configuring the cell (when reusing it). As you have 10 imageViews inside your scrollView, you'll have to decide whether to delete all those (and/or the scrollview), or just reach inside and update their images when a cell is reused.
Can you post a screenshot. Its a bit hard to visualize what you are describing. I'm not sure how you are computing y origin to be 22.
As a side note I believe its cleaner to do this by creatint your own TableViewCell subclass and use that instead of the default UITableViewCell. There is an example called Elements which shows how to do this properly: http://developer.apple.com/library/ios/#samplecode/TheElements/Introduction/Intro.html
Well, I don't know it's the cause of your problem, but you've definitely got an issue. Note that every time you are asked for a cell, you're adding the galleryRow subview. When a cell goes off-screen, it's put on the reusableCell queue. Then you're asked for another cell; you get it from the queue, it still has the old galleryRow subview, and now you add another one; so that's not good. You should either reuse or delete the old one.
Finally, why are you using UITableViewCellStyleSubtitle, and then not using any of the default fields in that UITableView?
In my app, I have a table view that has about eight cells. There is a navigation bar at the top. When a user touches a cell, nothing happens for about 1/2 second. Then the touched cell highlights blue and immediately the new view slides into position.
The problem is that there is no feedback to the user about which cell he touched until just before the new view slides into position.
For example, when I explore the tables in the iPhone's Settings application, when you touch a cell, the cell immediately turns blue, and then there is a 1/2 second delay, and then you see the new view.
How do I get my table's feedback of highlighting the cells to happen immediately? I am using tableView didSelectRowAtIndexPath:, and each cell has an accessory button.
Thanks for any insight.
are you using a custom-drawn cell (with a drawRect override) or something like that?
if you have a custom drawRect method, you'll need to do something like this (based off the code for tweetie, found here):
//default colors for cell
UIColor *backgroundColor = [UIColor whiteColor];
UIColor *textColor = [UIColor blackColor];
//on highlight, swap colors
if(self.highlighted){
backgroundColor = [UIColor clearColor];
textColor = [UIColor whiteColor];
}
that should give you the default behavior.
It sounds like the screen is refreshing after the new slide has been processed. You need to refresh the screen before rendering the new slide view.
a few things:
it's probably best to have a property in beforeViewController so it can set its own title on load (instead of setting it from the parent class).
second, why are you setting the back button for the current class? youre also leaking that (you alloc the UIBarButtonItem but dont release it).
NewViewController *newViewController = [[[NewViewController alloc]
initWithNibName:#"New" bundle:nil] autorelease];
newViewController.name = [self.listData objectAtIndex:indexPath.row];
[self.navigationController pushViewController:beforeAfterViewController animated:YES];
then in NewViewController, you have
- (void) viewDidLoad{
self.title = self.name;
}
re: your secondary question: if you pop the child controller using [self.navigationController popViewControllerAnimated:YES], the parent view should auto-deselect the row that was previously selected. it shoulnt stay selected unless you are forcing it to stay that way.
you don't need to do anything like [self.tableView deselectRowAtIndexPath:] unless you are not pushing child views (and doing something like checkmarking a cell that the user tapped).
You may put these 2 lines of code at the beginning of the didSelectRowAtIndexPath method:
UITableViewCell *cell = [tableView cellForRowAtIndexPath:indexPath];
[cell setSelected:YES animated:YES];
it should first highlight the cell before processing other program logic.
I'm trying to animate an image in a UITableViewCell subclass. It works when the tap on the cell is about 1/2 second or more in duration. For shorter taps, the cell gets selected, but my animation doesn't run.
In my view controller, I have:
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
ImageCell *cell = (ImageCell*)[tableView cellForRowAtIndexPath:indexPath];
cell.imageFlashDuration = 5.0;
cell.imageFlashNumberOfFrames = 25;
NSLog(#"Flash image...");
[cell flashImage];
[self performSelector:#selector(doSomething:) withObject:video afterDelay:5.0];
}
In my ImageCell:
-(void)flashImage {
UIImage *image = imageView.image;
if(imageView.isAnimating) {
NSLog(#"Stop animating");
[imageView stopAnimating];
}
NSMutableArray *animationArray = [NSMutableArray arrayWithCapacity:imageFlashNumberOfFrames];
for(int i=0; i<imageFlashNumberOfFrames; i++) {
[animationArray addObject:(i % 2 == 0? black60x60Image : image)];
}
imageView.animationImages = animationArray;
imageView.animationDuration = imageFlashDuration;
NSLog(#"> Start animating");
[imageView startAnimating];
}
In my log, I see
2009-07-02 22:02:55.907 MyProg[1797:20b] Flash image...
2009-07-02 22:02:55.912 MyProg[1797:20b] > Start animating
2009-07-02 22:02:59.455 MyProg[1797:20b] Flash image...
2009-07-02 22:02:59.460 MyProg[1797:20b] > Start animating
2009-07-02 22:03:02.463 MyProg[1797:20b] Flash image...
2009-07-02 22:03:02.468 MyProg[1797:20b] > Start animating
2009-07-02 22:03:05.009 MyProg[1797:20b] Flash image...
2009-07-02 22:03:05.014 MyProg[1797:20b] > Start animating
The above was from a mixture of 'short' and 'long' touches. The long touches resulted in cell selection and image animation and the short ones resulted in cell selection without animation.
Additionally, if a short tap is followed by another short tap on the cell, the animation begins.
I think the problem lies in the didSelectRowAtIndexPath method. There you call the cellForRowAtIndexPath method, which will probably NOT give back the same cell that was clicked! The cell returned probably is a new cell, not visible on screen, with an animating image-thing on it. But... It's not visible.
I assume you handle the longer clicks in a different method (not didSelectRowAtIndexPath)?
You should probably keep a reference to the cell at the point the cell gets requested by the tableview itself (or assign a tag to it or something) in the cellForRowAtIndexPath method. Then when the didSelectRowAtIndexPath method, look up that specific instance of the cell (by the reference, or by the tag, ..)
Hope this helps :) Cheers
I have a UITableView with reorderable rows and I'm using the standard UITableViewCell.text property to display text. When I tap Edit, move a row, tap Done, then tap the row, the built-in UILabel turns completely white (text and background) and opaque, and the blue shade to the cell doesn't show behind it. What gives? Is there something I should be doing that I'm not? I have a hacky fix, but I want the real McCoy.
Here is how to reproduce it:
Starting with the standard "Navigation-Based Application" template in the iPhone OS 2.2.1 SDK:
Open RootViewController.m
Uncomment viewDidLoad, and enable the Edit button:
- (void)viewDidLoad {
[super viewDidLoad];
// Uncomment the following line to display an Edit button in the navigation bar for this view controller.
self.navigationItem.rightBarButtonItem = self.editButtonItem;
}
Specify that the table has a few cells:
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
return 4;
}
In tableView:cellForRowAtIndexPath:, add a line to set the text property of a cell, and therefore to use the built-in UILabel subview:
// Set up the cell...
cell.text = #"Test";
To enable reordering, uncomment tableView:moveRowAtIndexPath:toIndexPath:. The default implementation is blank, which is fine in this case since the template doesn't include a data model.
Configure the project for the Simulator, OS 2.2.1, Build and Go. When the app comes up, tap Edit, then slide any row to a new position, tap Done, and then tap each row one at a time. Usually a tap will select a row, turn it blue, and turn its text white. But a tap on the row that you just moved does that and leaves the UILabel's background color as white. The result is a confusing white open space with blue strips on the edges. Oddly enough, after the first bogus tap, another tap appears to correct the problem.
So far I have found a hack that fixes it, but I'm not happy with it. It works by ensuring that the built-in UILabel is non-opaque and that it has no background color, immediately upon selection.
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
// hacky bugfix: when a row is reordered and then selected, the UILabel displays all crappy
UITableViewCell *cell = [tableView cellForRowAtIndexPath:indexPath];
for (UIView *view in cell.contentView.subviews) {
if ([[view class] isSubclassOfClass:[UILabel class]]) {
((UILabel *) view).backgroundColor = nil;
view.opaque = NO;
}
}
// regular stuff: only flash the selection, don't leave it blue forever
[tableView deselectRowAtIndexPath:indexPath animated:YES];
}
This appears to work, but I don't expect it to be a good idea forever. What is the Right Way to fix this?
This looks like a bug in UITableView's rendering, and you should file a Radar bug report on it. It's like the cells don't get refreshed properly after the move.
One way to work around this for now is to not use the built-in label, but roll your own in the cell:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
static NSString *CellIdentifier = #"Cell";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
if (cell == nil) {
cell = [[[UITableViewCell alloc] initWithFrame:CGRectZero reuseIdentifier:CellIdentifier] autorelease];
CGRect frame = cell.contentView.bounds;
frame.origin.x = frame.origin.x + 10.0f;
UILabel *textLabel = [[UILabel alloc] initWithFrame:frame];
[textLabel setAutoresizingMask:UIViewAutoresizingFlexibleRightMargin];
textLabel.tag = 1;
textLabel.textAlignment = UITextAlignmentLeft;
textLabel.backgroundColor = [UIColor clearColor];
textLabel.textColor = [UIColor blackColor];
textLabel.font = [UIFont boldSystemFontOfSize:20.0];
textLabel.numberOfLines = 1;
textLabel.highlightedTextColor = [UIColor whiteColor];
[cell.contentView addSubview:textLabel];
[textLabel release];
}
UILabel *textLabel = (UILabel *)[cell viewWithTag:1];
textLabel.text = #"Test";
return cell;
}
I tried this, and it doesn't exhibit the same sort of white blank rectangle you see with the built-in label. However, adding another non-opaque view to the table cell might not be the best for overall rendering performance.
I don't know how major of a glitch this is, because Apple doesn't want you to persist a selection highlight on a table row (they've been enforcing this lately during the review process). You're supposed to place a checkmark or move on to the next level in the navigation hierarchy with a selection, at which point this white box would only be on the screen for a fraction of a second.
The trick in the solution from Brad appears to be:
textLabel.backgroundColor = [UIColor clearColor];
If you leave the background as the default you still get the problem even when you roll your own cells UITableViewCells.
The reason I left it as the default is because the documentation says it is less computationally costly to use opaque backgrounds. Ideally I wouldn't want to use [UIColor clearColor] to fix this bug.
Maybe a completely custom painted cell would somehow fix it. I haven't tried those before though.
Does anyone else have a solution for this?
Thanks for the info, I was searching how to erase the background color from a UILabel.
I used the following line:
textLabel.backgroundColor = [UIColor clearColor];
and worked perfectly!!!
thanks
Alejandra :)
Selections aren't meant to be shown for extended periods! (We got knocked on this for several of our apps)
??? That means Apple would not approve their own Calendar app on iPhone! When you go to edit the start and end times of the event, the start time is selected indefinitely, it only changes once the user taps to the next field.