In Keynote (and other apps), I've noticed the "standard" interface of doing Undo/Redo is by providing an Undo button on the tool bar.
Clicking the button (that is always enabled) Undos the recent operation.
(If there is not recent operation to undo, it will show the Undo/Redo menu).
Long-clicking the Undo button opens an Undo/Redo menu.
I searched for methods of implementing this, and the best answer I found so far is at the following link.
I wonder if anyone knows of a simpler way?
Thanks!
After reviewing all methods and discussing with friends, below is the solution I used, for a UIBarButtonItem the responds to both taps and long-press (TapOrLongPressBarButtonItem).
It is based on the following principals:
Subclass UIBarButtonItem
Use a custom view (so it's really trivial to handle the long-press - since our custom view has no problem responding to a long-press gesture handler...)
... So far - this approach was in the other SO thread - and I didn't like this approach since I couldn't find and easy enough way of making the custom view appear like an iPad navigation bar button... Soooo...
Use UIGlossyButton by Water Lou (thanks water!). This use is encapsulated within the subclass...
The resulting code is as follows:
#protocol TapOrPressButtonDelegate;
#interface TapOrPressBarButtonItem : UIBarButtonItem {
UIGlossyButton* _tapOrPressButton;
__weak id<TapOrPressButtonDelegate> _delegate;
}
- (id)initWithTitle:(NSString*)title andDelegate:(id<TapOrPressButtonDelegate>)delegate;
#end
#protocol TapOrPressButtonDelegate<NSObject>
- (void)buttonTapped:(UIButton*)button withBarButtonItem:(UIBarButtonItem*)barButtonItem;
- (void)buttonLongPressed:(UIButton*)button withBarButtonItem:(UIBarButtonItem*)barButtonItem;
#end
#implementation TapOrPressBarButtonItem
- (void)buttonLongPressed:(UILongPressGestureRecognizer*)gesture {
if (gesture.state != UIGestureRecognizerStateBegan)
return;
if([_delegate respondsToSelector:#selector(buttonLongPressed:withBarButtonItem:)]) {
[_delegate buttonLongPressed:_tapOrPressButton withBarButtonItem:self];
}
}
- (void)buttonTapped:(id)sender {
if (sender != _tapOrPressButton) {
return;
}
if([_delegate respondsToSelector:#selector(buttonTapped:withBarButtonItem:)]) {
[_delegate buttonTapped:_tapOrPressButton withBarButtonItem:self];
}
}
- (id)initWithTitle:(NSString*)title andDelegate:(id<TapOrPressButtonDelegate>)delegate {
if (self = [super init]) {
// Store delegate reference
_delegate = delegate;
// Create the customm button that will have the iPad-nav-bar-default appearance
_tapOrPressButton = [UIGlossyButton buttonWithType:UIButtonTypeCustom];
[_tapOrPressButton setTitle:title forState:UIControlStateNormal];
[_tapOrPressButton setNavigationButtonWithColor:[UIColor colorWithRed:123.0/255 green:130.0/255 blue:139.0/255 alpha:1.0]];
// Calculate width...
CGSize labelSize = CGSizeMake(1000, 30);
labelSize = [title sizeWithFont:_tapOrPressButton.titleLabel.font constrainedToSize:labelSize lineBreakMode:UILineBreakModeMiddleTruncation];
_tapOrPressButton.frame = CGRectMake(0, 0, labelSize.width+20, 30);
// Add a handler for a tap
[_tapOrPressButton addTarget:self action:#selector(buttonTapped:) forControlEvents:UIControlEventTouchUpInside];
// Add a handler for a long-press
UILongPressGestureRecognizer* buttonLongPress_ = [[UILongPressGestureRecognizer alloc] initWithTarget:self
action:#selector(buttonLongPressed:)];
[_tapOrPressButton addGestureRecognizer:buttonLongPress_];
// Set this button as the custom view of the bar item...
self.customView = _tapOrPressButton;
}
return self;
}
// Safe guards...
- (id)initWithImage:(UIImage *)image style:(UIBarButtonItemStyle)style target:(id)target action:(SEL)action {
NSLog(#"%s not supported!", __FUNCTION__);
return nil;
}
- (id)initWithImage:(UIImage *)image landscapeImagePhone:(UIImage *)landscapeImagePhone style:(UIBarButtonItemStyle)style target:(id)target action:(SEL)action {
NSLog(#"%s not supported!", __FUNCTION__);
return nil;
}
- (id)initWithTitle:(NSString *)title style:(UIBarButtonItemStyle)style target:(id)target action:(SEL)action {
NSLog(#"%s not supported!", __FUNCTION__);
return nil;
}
- (id)initWithBarButtonSystemItem:(UIBarButtonSystemItem)systemItem target:(id)target action:(SEL)action {
NSLog(#"%s not supported!", __FUNCTION__);
return nil;
}
- (id)initWithCustomView:(UIView *)customView {
NSLog(#"%s not supported!", __FUNCTION__);
return nil;
}
#end
And all you need to do is:
1. Instantiate is as follows:
TapOrPressBarButtonItem* undoMenuButton = [[TapOrPressBarButtonItem alloc] initWithTitle:NSLocalizedString(#"Undo", #"Undo Menu Title") andDelegate:self];
2. Connect the button to the navigation bar:
[self.navigationItem setLeftBarButtonItem:undoMenuButton animated:NO];
3. Implement the TapOrPressButtonDelegate protocol, and you're done...
-(void)buttonTapped:(UIButton*)button withBarButtonItem:(UIBarButtonItem*)barButtonItem {
[self menuItemUndo:barButtonItem];
}
-(void)buttonLongPressed:(UIButton*)button withBarButtonItem:(UIBarButtonItem*)barButtonItem {
[self undoMenuClicked:barButtonItem];
}
Hope this helps anyone else...
If you are using IB (or in Xcode4 the designer...i guess it is called) then you can select "Undo" from the First responder and drag that action to a button. I can give you more specific instructions if that doesn't cover it.
Here's what it looks like
It's on the left underneath the column "Received actions" at the bottom
I believe the key is actually in the UINavigationBar itself. Unlike UIButtons or other normal touch tracking objects, I suspect UIBarItems don't handle their own touches. They don't inherit UIResponder or UIControl methods. However UINavigationBar of course does. And I've personally added gestures straight to a UINavigationBar many times.
I suggest you override touch handling in a UINavigationBar subclass and check the touches against its children. If the child is your special Undo button you can handle it accordingly.
UIButton* undoButton = [UIButton buttonWithType:UIButtonTypeCustom];
[undoButton addTarget:self action:#selector(undoPressStart:) forControlEvents:UIControlEventTouchDown];
[undoButton addTarget:self action:#selector(undoPressFinish:) forControlEvents:UIControlEventTouchUpInside];
UIBarButtonItem* navButton = [[[UIBarButtonItem alloc] initWithCustomView:undoButton] autorelease];
self.navigationItem.rightBarButtonItem = navButton;
You don't necessarily have to add the UIBarButtonItem as the rightBarButtonItem, this is just and easy way to show you how to create your UIBarButtonItem with a custom view that is the UIButton you want to handle events.
You'll need to implement the undoPressStart: and undoPressFinish: by maintaining state. I'd say on start, store the current NSDate or some granular representation of the time. On finish, if check the time elapsed and if it is beyond a certain threshold, show the menu - otherwise (as well as if the start date was never captured) perform the undo.
As an improvement, you'll likely want to observe the UIControlEventTouchDragExit event as well to cancel the long press.
Related
I am using a CCTableView to make a table with CCNodes as cells. Those CCNodes have a button each. I want to be able to detect if a user taps on a cell and if it taps on the button. But the CCTableView doesn't have a tableView:didSelectRowAtIndexPath: method so how can I do this? Do you know of any open source class that has this method?
P.S. I am using version 3 of cocos2d
I took a different approach after trying numerous things
#interface WKTableCell : CCTableViewCell
#end
#implementation WKTableCell
- (instancetype) initWithTitle: (NSString *) title
{
self = [super init];
if (!self)
return nil;
[self.button setTitle:title];
// This is a transparent png (400x200) for my needs
CCSpriteFrame * frame = [CCSpriteFrame frameWithImageNamed:#"cell.png"] ;
[self.button setPreferredSize:CGSizeMake(frame.originalSize.width, frame.originalSize.height)];
[self.button setContentSizeType:CCSizeTypePoints];
[self.button setBackgroundSpriteFrame:frame forState:CCControlStateNormal];
}
// then in your table
[table setBlock:^(id sender) {
CCLOG(#"yup, this gets called.. ");
}];
this did work for me..
Your CCTableView, responds to the CCTouchDelegate, so you can use ccTouchBegan etc
to detect the point and then calculate what cell there was in that point. Here the class reference:
http://docs.huihoo.com/doxygen/cocos2d-x/2.1.2/d0/d38/classcocos2d_1_1_c_c_touch_delegate.html
I'm attempting to trigger an event in a subview of a UITableViewCell, and let it bubble up the responder chain and be handled by a custom UITableViewCell subclass.
Basically:
SomeView.m (which is a subview of the UITableViewCell)
[self.button addTarget:nil action:#selector(someAction:) events:UIControlEventTouchUpInside]
SomeCustomCell.m
- (void)someAction:(id)sender {
NSLog(#"cool, the event bubbled up to the cell");
}
And to test why this wasn't working, I've added the someAction: method on the ViewController and the ViewController is the one that ends up handling the event that bubbles up from the table view cell subview, even though the Cell should handle it. I've checked that the Cell is on the responder chain and I've verified that any views on the responder chain both above and below the cell will respond to the event if they implement the someAction: method.
What the heck is going on here?
Here's a project that shows it https://github.com/keithnorm/ResponderChainTest Is this expected behavior somehow? I haven't found any documentation stating UITableViewCell's are treated any differently than other UIResponder's.
The cell seems to ask its table view for permission. To change that you can of course override
- (BOOL)canPerformAction:(SEL)action withSender:(id)sender
{
return [self respondsToSelector:action];
}
Swift 3, 4, 5:
override func canPerformAction(_ action: Selector, withSender sender: Any?) -> Bool {
return self.responds(to: action)
}
I've concluded that this is either a bug or undocumented intended behavior. At any rate, I ended up brute force fixing it by responding to the event in a subview and then manually propagating the message up the responder chain. Something like:
- (void)customEventFired:(id)sender {
UIResponder *nextResponder = self.nextResponder;
while (nextResponder) {
if ([nextResponder respondsToSelector:#selector(customEventFired:)]) {
[nextResponder performSelector:#selector(customEventFired:) withObject:sender];
break;
}
nextResponder = nextResponder.nextResponder;
}
}
I've also updated my demo project to show how I'm using this "fix" https://github.com/keithnorm/ResponderChainTest.
I still welcome any other ideas if anyone else figures this out, but this is the best I've got for now.
You can change the code in View.m as
[button addTarget:nil action:#selector(customEventFired:) forControlEvents:(1 << 24)];
to
[button addTarget:cell action:#selector(customEventFired:) forControlEvents:(1 << 24)];
do like this
#implementation ContentView
// uncomment this to see event caught by the cell's subview
- (id)initWithFrame:(CGRect)frame
{
self = [super initWithFrame:frame];
if(self)
{
UIButton *button = [UIButton buttonWithType:UIButtonTypeCustom];
[button setTitle:#"Click" forState:UIControlStateNormal];
[button setBackgroundColor:[UIColor blueColor]];
[button addTarget:self action:#selector(customEventFired:) forControlEvents:UIControlEventTouchUpInside];
button.frame = CGRectMake(4, 5, 100, 44);
[self addSubview:button];
}
return self;
}
- (void)customEventFired:(id)sender
{
UIAlertView *alertView = [[UIAlertView alloc] initWithTitle:#"Event Triggered in cell subview" message:#"so far so good..." delegate:nil cancelButtonTitle:#"cancel" otherButtonTitles:nil, nil];
[alertView show];
}
#end
now customEventFired: method get called
I think this one is the easiest solution. If you do not specify a target the event will automatically bubble up the responder chain.
[[UIApplication sharedApplication]sendAction:#selector(customAction:) to:nil from:self forEvent:UIEventTypeTouches];
i have a Navigation Bar, wich contains a Navigation Item, which contains 2 Bar Buttons, these are created in the Storyboard, and i wanted to change 1 of the buttons at runtime, now this works:
- (void)viewDidLoad
{
[super viewDidLoad];
// Do any additional setup after loading the view.
UINavigationItem *thisNavBar = [self myNavigationItem];
thisNavBar.rightBarButtonItem = nil; // this works, it gets removed
UIBarButtonItem *insertBtn = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemEdit target:self action:#selector(insertSkemaItem:)];
thisNavBar.rightBarButtonItem = insertBtn; // this also works, it sets the btn
}
Now, in my other method, which is called by another controller, it does not work
- (void)callChildChange {
...
// remove old btn
UINavigationItem *thisNavBar = [self skemaNavigationItem];
thisNavBar.rightBarButtonItem = nil; // does not work?
}
There is nothing wrong with the method, it runs just fine, but the nav btn item does not get removed ?
skemaNavigationItem is a Navigation item, declared in the .h file which links the navigation item i made via the storyboard.
Your UI items need to be added to your code (by ctrl-dragging) in the header file (.h) so they can be publicly accessed from other classes/view controllers.
Presuming you've done this, hiding a UI item is best done by using
relevantClass.yourViewObject.hidden = YES;
or if you really need to delete it for good,
[relevantClass.yourViewObject.view removeFromSuperView];
Edits
Options for changing target method:
Declare #property (nonatomic, assign) BOOL myButtonWasPressed; and:
- (IBAction) myButtonPressed
{
if (!self.myButtonWasPressed)
{
// This code will run the first time the button is pressed
self.myButton.text = #"New Button Text";
self.myButtonWasPressed = YES;
}
else
{
// This code will run after the first time your button is pressed
// You can even set your BOOL property back, and make it toggleable
}
}
or
- (IBAction) myButtonWasPressedFirstTime
{
// do what you need to when button is pressed then...
self.myButton.text = #"New Button Text";
[self.myButton removeTarget:self action:#selector(myButtonPressedFirstTime) forControlEvents:UIControlEventTouchUpInside];
[self.myButton addTarget:self action:#selector(myButtonPressedAgain) forControlEvents: UIControlEventTouchUpInside];
}
- (IBAction) myButtonWasPressedAgain
{
// this code will run the subsequent times your button is pressed
}
I'm working on an application in which I'm adding UIBarButtonItem to UIToolbar dynamically. When User clicks on a bar button. I'm changing it's tint color to red.
But for some bar buttons it's not working and the application is crashing.
Here is my code:
#interface myClass : UIViewController
#property (nonatomic, retain) NSMutableArray *barButtonItems;
#property (nonatomic, retain) IBOutlet UIToolbar *toolBar;
#end
#implementation myClass
#sythesize barButtonItems, toolBar;
- (void)viewDidLoad
{
[super viewDidLoad];
barButtonItems = [[NSMutableArray alloc] init];
[self initToolBar];
}
//To set the tool bar
- (void)initToolBar
{
[self addBarItem:#"PlantDetails" actionName:#"createPlantDetails:"];
[self addBarItem:#"ElectricalEquipmentInventory" actionName:#"createInventory:button:"];
toolBar.items = barButtonItems;
}
//Create bar button item
- (void)addBarItem:(NSString*)barButtonName actionName:(NSString*)methodName
{
UIBarButtonItem *plantDetails = [[UIBarButtonItem alloc] initWithTitle:barButtonName style:UIBarButtonItemStyleDone target:self action:NSSelectorFromString(methodName)];
[barButtonItems addObject:plantDetails];
[plantDetails release];
plantDetails = nil;
}
//Changes the barbutton tintcolor when user selected
-(void)changeSelection:(UIBarButtonItem *)button
{
NSArray *tempArray = toolBar.items;
for(int loop = 0; loop<[tempArray count]; loop++)
[[tempArray objectAtIndex:loop] setTintColor:[UIColor blackColor]];
[button setTintColor:[UIColor redColor]];
}
//First bar button method
- (void)createPlantDetails:(UIBarButtonItem *)button
{
[self changeSelection:button];
NSLog(#"createPlantDetails");
}
//second bar button method
- (void)createInventory:(int)selectedIndex button:(UIBarButtonItem *)button
{
[self changeSelection:button];
NSLog(#"createInventory");
}
#end
Here my issue is the bar button with only one parameter in it's selector is working perfectly (createPlantDetails) but when I click on the bar button which have two parameter in it's selector (createInventory) the application is crashing on [button setTintColor:[UIColor redColor]]; of changeSelection method.
Crash log is something like: touches event have no method like setTintColor .
I searched a lot but couldn't find a solution. Please help me.
Thanks in advance
The method for the action property has to have one of the following three forms:
- (void)methodName;
- (void)methodName:(id)sender;
- (void)methodName:(id)sender withEvent:(UIEvent *)event;
You can't use any arbitrary format or custom parameters (the button wouldn't know what to pass for them).
The createPlantDetails: method works because it matches the second form.
The createInventory:button: method fails because it doesn't match any of the expected signatures.
Since your method has two parameters, when the button calls the method, the button passes a UIEvent object in the second parameter which in your method is named button.
In changeSelection:, it crashes when it tries to call setTintColor: because button is really a UIEvent and not the UIBarButtonItem (ie. the sender).
I know this is really basic stuff but i need to understand whether my understanding of this is correct.
So what i want to do is this. I want an view with a label on which when double tapped flips and loads another view. On the second view i want a UIPickerView and above i have a button saying back. Both views will be of same size as an UIPickerView which is 320px x 216px.
What i am thinking of to do is create two UIViewclasses named labelView and pickerView. I would then create a viewController which on loadView loads labelView then when user double taps the labelView i get an event in labelView class which is sent to my viewController that then can unload loadView and load the pickerView.
Does this sound as the best way to do this ? Is there a simpler way ? I am also unsure how i route the event from the labelView class to the viewControllerclass.
I dont exactly know the most efficient way to do it(as i am also now to this language),but it is for sure that i have solved ur problem. I made a simple program for that.Three classes involved here in my eg are BaseViewController (which will show two views),LabelView and PickerView (according to ur requirement).
In LabelView.h
#protocol LabelViewDelegate
-(void)didTapTwiceLabelView;
#end
#interface LabelView : UIView {
id <LabelViewDelegate> delegate;
}
#property(nonatomic,retain)id <LabelViewDelegate> delegate;
-(void)didTouch;
#end
In LabelView.m
#synthesize delegate;
-(id)initWithFrame:(CGRect)frame {
self = [super initWithFrame:frame];
if (self)
{
UILabel* labl = [[UILabel alloc] initWithFrame:CGRectMake(10, 5, frame.size.width-20,20)];
labl.text = #"Some Text";
[self addSubview:labl];
[labl release]; labl = nil;
self.backgroundColor = [UIColor grayColor];
UITapGestureRecognizer* ges = [[[UITapGestureRecognizer alloc] initWithTarget:self action:#selector(didTouch)] autorelease];
ges.numberOfTapsRequired = 2;
[self addGestureRecognizer:ges];
}
return self;
}
-(void)didTouch
{
[delegate didTapTwiceLabelView];
}
//=============================================================
In Pickerview.h
#protocol PickerViewDelegate
-(void)didTapBackButton;
#end
#interface PickerView : UIView <UIPickerViewDelegate,UIPickerViewDataSource>{
id <PickerViewDelegate> delegate;
}
#property(nonatomic,retain)id <PickerViewDelegate> delegate;
#end
In Pickerview.m
#implementation PickerView
#synthesize delegate;
-(id)initWithFrame:(CGRect)frame {
self = [super initWithFrame:frame];
if (self)
{
UIPickerView* picker = [[UIPickerView alloc] initWithFrame:CGRectMake(0, 30, 320, 216)];
picker.delegate = self;
picker.dataSource = self;
[self addSubview:picker];
[picker release]; picker = nil;
self.frame = CGRectMake(frame.origin.x, frame.origin.y, 320, 250);
UIButton* btn = [UIButton buttonWithType:UIButtonTypeRoundedRect];
[btn setFrame:CGRectMake(10, 1, 50, 27)];
[btn setTitle:#"Back" forState:UIControlStateNormal];
[btn addTarget:self action:#selector(backButton) forControlEvents:UIControlEventTouchUpInside];
[self addSubview:btn];
}
return self;
}
-(void)backButton
{
[delegate didTapBackButton];
}
//====================================================================
in BaseViewController.h
#import "LabelView.h"
#import "PickerView.h"
#interface VarticalLabel : UIViewController<UITextFieldDelegate,PickerViewDelegate,LabelViewDelegate> {
PickerView* myPickerView;
LabelView* myLabelView;
}
#end
In BaseViewController.m
-(void)viewDidLoad
{
[super viewDidLoad];
myPickerView= [[PickerView alloc] initWithFrame:CGRectMake(0, 50, 320, 250)];
[self.view addSubview:myPickerView];
myPickerView.delegate = self;
myLabelView= [[LabelView alloc] initWithFrame:CGRectMake(0, 50, 320, 250)];
[self.view addSubview:myLabelView];
myLabelView.delegate = self;
myPickerView.hidden = YES;
}
#pragma mark PickerViewDelgate
-(void)didTapBackButton
{
myPickerView.hidden = YES;
myLabelView.hidden = NO;
}
#pragma mark LabelViewDelegate
-(void)didTapTwiceLabelView
{
myPickerView.hidden = NO;
myLabelView.hidden = YES;
}
To get events from a button to the view controller, just hook up the button's event, e.g. touch up inside, to a method in the view controller, using interface builder. (Double tapping is probably more complicated though.)
When you say 'flips', do you mean it actually shows an animation of flipping over a view to show a 'reverse' side? Like in the weather app when you hit the 'i' button? I'm assuming this is what you mean.
Perhaps check TheElements sample example on the iPhone Reference Library, it has an example of flip animation.
Btw, it's not strictly necessary to unload the loadView that is being 'hidden' when you flip -- it saves you having to construct it again when you flip back -- but it may be pertinent if you have memory use concerns, and/or the system warns you about memory being low.
Also, what do you mean by "create a UIView"? Do you mean subclass UIView, or just instantiate a UIVIew and add children view objects to it? The latter is the usual strategy. Don't subclass UIView just because you want to add some things to a UIView.
If you've got one screen of information that gives way to another screen of information, you'd normally make them separate view controllers. So in your case you'd have one view controller with the label and upon receiving the input you want, you'd switch to the view controller composed of the UIPickerView and the button.
Supposing you use Interface Builder, you would probably have a top level XIB (which the normal project templates will have provided) that defines the app delegate and contains a reference to the initial view controller in a separate XIB (also supplied). In the separate XIB you'd probably want to add another view controller by reference (so, put it in, give it the class name but indicate that its description is contained in another file) and in that view controller put in the picker view and the button.
The point of loadView, as separate from the normal class init, is to facilitate naming and linking to an instance in one XIB while having the layout defined in another. View controllers are alloced and inited when something that has a reference to them is alloced and inited. But the view is only loaded when it is going to be presented, and may be unloaded and reloaded while the app is running (though not while it is showing). Generally speaking, views will be loaded when needed and unnecessary views will be unloaded upon a low memory warning. That's all automatic, even if you don't put anything in the XIBs and just create a view programmatically within loadView or as a result of viewDidLoad.
I've made that all sound more complicated than your solution, but it's actually simpler because of the amount you can do in Interface Builder, once you're past the curve of learning it. It may actually be worth jumping straight to the Xcode 4 beta, as it shakes things up quite a lot in this area and sites have reported that a gold master was seeded at one point, so is likely to become the official thing very soon.
With respect to catching the double tap, the easiest thing is a UITapGestureRecognizer (see here). You'd do something like:
// create a tap gesture recogniser, tell it to send events to this instance
// of this class, and to send them via the 'handleGesture:' message, which
// we'll implement below...
UITapGestureRecognizer *tapGestureRecognizer = [[UITapGestureRecognizer alloc]
initWithTarget:self action:#selector(handleGesture:)];
// we want double taps
tapGestureRecognizer.numberOfTapsRequired = 2;
// attach the gesture recogniser to the view we want to catch taps on
[labelView addGestureRecognizer:tapGestureRecognizer];
// we have an owning reference to the recogniser but have now given it to
// the label. We don't intend to talk to it again without being prompted,
// so should relinquish ownership
[tapGestureRecognizer release];
/* ... elsewhere ... */
// the method we've nominated to receive gesture events
- (void)handleGesture:(UIGestureRecognizer *)gestureRecognizer
{
// could check 'gestureRecognizer' against tapGestureRecognizer above if
// we set the same message for multiple recognisers
// just make sure we're getting this because the gesture occurred
if(gestureRecognizer.state == UIGestureRecognizerStateRecognized)
{
// do something to present the other view
}
}
Gesture recognisers are available as of iOS 3.2 (which was for iPad only; so iOS 4.0 on iPhone and iPod Touch).