24

I am making a timer app and needed to display hour, min and seconds to the user. I tried using UIDatePicker but it shows only hours and mins as selections. Not seconds. After doing a bit of research online I found that there is no way to get seconds in UIDatePicker and I had to write my own UIPickerView from scratch.

So my question is, is there sample code for such i.e. someone wrote a CustomUIPickerView for hours, min and secs that I can incorporate in my project? UIDatePicker has a nice overlay of Hours & Mins text that stays put while the user rotate the dials. It would be nice if someone added that in their custom picker too. I prefer not to write a custom UIPickerView from scratch if I don't have to. Thank you.

Atulkumar V. Jain
  • 5,102
  • 9
  • 44
  • 61
Sam B
  • 27,273
  • 15
  • 84
  • 121
  • use uiPickerView with 3 components with each having array. HourArray from (1 to 12). MinutesArray with ( 1 to 60). secondsArray ( 1 to 60) – Paresh Navadiya Jun 12 '12 at 16:02
  • @safecase. I understand technically how it needs to be done. All I am asking is if someone would like to share this piece of code with me instead of I writing it from scratch – Sam B Jun 12 '12 at 16:12
  • 3
    @EmilioPelaez - please refrain from using "us". You don't speak for the whole iOS development community here. There are a lot of good developers that happily share knowledge and code. If I end up writing the code, I will share it here with the rest of the community and anyone that may be interested in future and reads my post – Sam B Jun 12 '12 at 16:33
  • 3
    I said "us" because Stack Overflow has a pretty clear purpose, and requesting code is not it (if somebody has that code and wants to share it, there are a lot of searchable ways to do it). "We" are here with that premise. – EmilioPelaez Jun 12 '12 at 17:38

8 Answers8

43

Alrighty folks, here is the code to get hours/mins/secs in your UIPickerView. You can add 3 labels an strategically place them on the picker. I have attached a picture as well.

enter image description here

In your header .h file put this

@interface v1AddTableViewController : UITableViewController
{

    IBOutlet UIPickerView *pickerView;    
    NSMutableArray *hoursArray;
    NSMutableArray *minsArray;
    NSMutableArray *secsArray;
    
    NSTimeInterval interval;
    
}

@property(retain, nonatomic) UIPickerView *pickerView;
@property(retain, nonatomic) NSMutableArray *hoursArray;
@property(retain, nonatomic) NSMutableArray *minsArray;
@property(retain, nonatomic) NSMutableArray *secsArray;

in your .m file put this

@synthesize pickerView;
@synthesize hoursArray;
@synthesize minsArray;
@synthesize secsArray;
@synthesize interval;

- (void)viewDidLoad
{
    [super viewDidLoad];
    
    
    //initialize arrays
    hoursArray = [[NSMutableArray alloc] init];
    minsArray = [[NSMutableArray alloc] init];
    secsArray = [[NSMutableArray alloc] init];
    NSString *strVal = [[NSString alloc] init];
    
    for(int i=0; i<61; i++)
    {
        strVal = [NSString stringWithFormat:@"%d", i];
        
        //NSLog(@"strVal: %@", strVal);
        
        //Create array with 0-12 hours
        if (i < 13)
        {
            [hoursArray addObject:strVal];
        }
        
        //create arrays with 0-60 secs/mins
        [minsArray addObject:strVal];
        [secsArray addObject:strVal];
    }
    
    
    NSLog(@"[hoursArray count]: %d", [hoursArray count]);
    NSLog(@"[minsArray count]: %d", [minsArray count]);
    NSLog(@"[secsArray count]: %d", [secsArray count]);
    
}


//Method to define how many columns/dials to show
- (NSInteger)numberOfComponentsInPickerView:(UIPickerView *)pickerView
{
    return 3;
}


// Method to define the numberOfRows in a component using the array.
- (NSInteger)pickerView:(UIPickerView *)pickerView numberOfRowsInComponent :(NSInteger)component 
{ 
    if (component==0)
    {
        return [hoursArray count];
    }
    else if (component==1)
    {
        return [minsArray count];
    }
    else
    {
        return [secsArray count];
    }
    
}


// Method to show the title of row for a component.
- (NSString *)pickerView:(UIPickerView *)pickerView titleForRow:(NSInteger)row forComponent:(NSInteger)component
{
    
    switch (component) 
    {
        case 0:
            return [hoursArray objectAtIndex:row];
            break;
        case 1:
            return [minsArray objectAtIndex:row];
            break;
        case 2:
            return [secsArray objectAtIndex:row];
            break;    
    }
    return nil;
}


-(IBAction)calculateTimeFromPicker
{
    
    NSString *hoursStr = [NSString stringWithFormat:@"%@",[hoursArray objectAtIndex:[pickerView selectedRowInComponent:0]]];
    
    NSString *minsStr = [NSString stringWithFormat:@"%@",[minsArray objectAtIndex:[pickerView selectedRowInComponent:1]]];
    
    NSString *secsStr = [NSString stringWithFormat:@"%@",[secsArray objectAtIndex:[pickerView selectedRowInComponent:2]]];
    
    int hoursInt = [hoursStr intValue];
    int minsInt = [minsStr intValue];
    int secsInt = [secsStr intValue];
    
    
    interval = secsInt + (minsInt*60) + (hoursInt*3600);
    
    NSLog(@"hours: %d ... mins: %d .... sec: %d .... interval: %f", hoursInt, minsInt, secsInt, interval);
    
    NSString *totalTimeStr = [NSString stringWithFormat:@"%f",interval];

}
maxmitz
  • 258
  • 1
  • 4
  • 17
Sam B
  • 27,273
  • 15
  • 84
  • 121
  • 5
    How can we display "Hours", "Min", "Sec" in picker? – Nam Vu Jun 26 '13 at 08:10
  • 14
    This got to be one of the most terrible way of doing things... Why would you want to pre-allocate all strings !!?? it also doesn't set the required labels to display "hour" , "min", "secs" as shown on the screen capture – jyavenard Sep 11 '13 at 08:48
31

Unlike the accepted solution above, I came up with something a little less terrible. Definitely not perfect, but it has the desired effect (I've only tested in iOS 7 on iPhone, only works in portrait as written). Edits to make better are welcome. Relevant display code below:

// assumes you conform to UIPickerViewDelegate and UIPickerViewDataSource in your .h
- (void)viewDidLoad
{
    [super viewDidLoad];

    // assumes global UIPickerView declared. Move the frame to wherever you want it
    picker = [[UIPickerView alloc] initWithFrame:CGRectMake(0, 100, self.view.frame.size.width, 200)];
    picker.dataSource = self;
    picker.delegate = self;

    UILabel *hourLabel = [[UILabel alloc] initWithFrame:CGRectMake(42, picker.frame.size.height / 2 - 15, 75, 30)];
    hourLabel.text = @"hour";
    [picker addSubview:hourLabel];

    UILabel *minsLabel = [[UILabel alloc] initWithFrame:CGRectMake(42 + (picker.frame.size.width / 3), picker.frame.size.height / 2 - 15, 75, 30)];
    minsLabel.text = @"min";
    [picker addSubview:minsLabel];

    UILabel *secsLabel = [[UILabel alloc] initWithFrame:CGRectMake(42 + ((picker.frame.size.width / 3) * 2), picker.frame.size.height / 2 - 15, 75, 30)];
    secsLabel.text = @"sec";
    [picker addSubview:secsLabel];

    [self.view addSubview:picker];
}

- (NSInteger)numberOfComponentsInPickerView:(UIPickerView *)pickerView
{
    return 3;
}

- (NSInteger)pickerView:(UIPickerView *)pickerView numberOfRowsInComponent:(NSInteger)component
{
    if(component == 0)
        return 24;

    return 60;
}

- (CGFloat)pickerView:(UIPickerView *)pickerView rowHeightForComponent:(NSInteger)component
{
    return 30;
}

- (UIView *)pickerView:(UIPickerView *)pickerView viewForRow:(NSInteger)row forComponent:(NSInteger)component reusingView:(UIView *)view
{
    UILabel *columnView = [[UILabel alloc] initWithFrame:CGRectMake(35, 0, self.view.frame.size.width/3 - 35, 30)];
    columnView.text = [NSString stringWithFormat:@"%lu", (long) row];
    columnView.textAlignment = NSTextAlignmentLeft;

    return columnView;
}

And the result:

H/M/S Picker

Oren
  • 5,055
  • 3
  • 34
  • 52
Stonz2
  • 6,306
  • 4
  • 44
  • 64
  • 3
    Have you tried this with Auto Layout? I'm wondering how it would account for device rotation, different screen sizes, etc. Nice work! – Rod Mar 27 '14 at 22:24
  • 2
    I have not. As it's written now, it's pretty much just for iPhone portrait view, so there's definitely some limitations. – Stonz2 Mar 28 '14 at 14:36
  • Looks very pretty. Could this possibly be packaged and shared? – fatuhoku Apr 09 '14 at 10:01
  • 1
    I would, but I'm trying to work out the kinks of getting it to look right in landscape mode. I've got it down when using the picker as an `inputView` of a `UITextField` (so the phone handles updating the frame) but not as a standalone view. I'm definitely not the best when it comes to handling orientation changes. – Stonz2 Apr 11 '14 at 15:08
  • This should definitely be above the top answer, 32 for that is ridiculous! I think it would be better to use autolayout though as Rod suggests, you could just use the multiplier and center attributes then. Nice work though! – Rich Apr 15 '14 at 22:40
  • settings it to 2 columns instead of 3 messes it out obviously because x y numbers are fixed, is there a simple calculation for two columns ? i cant figure. – Osa Feb 14 '15 at 04:46
  • @Osa The question was specifically about a date picker with hours, mins, and seconds. If you have a new question, please use the appropriate action. Include a link to this question in your new question for reference. – Stonz2 Feb 15 '15 at 14:47
  • i was only trying to make it minutes and seconds :-) – Osa Feb 15 '15 at 18:20
8

swift implementation

class TimePickerView: UIPickerView, UIPickerViewDataSource, UIPickerViewDelegate {
    var hour:Int = 0
    var minute:Int = 0


    override init() {
        super.init()
        self.setup()
    }

    required internal init(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
        self.setup()
    }

    override init(frame: CGRect) {
        super.init(frame: frame)
        self.setup()
    }

    func setup(){
        self.delegate = self
        self.dataSource = self

        /*let height = CGFloat(20)
        let offsetX = self.frame.size.width / 3
        let offsetY = self.frame.size.height/2 - height/2
        let marginX = CGFloat(42)
        let width = offsetX - marginX

        let hourLabel = UILabel(frame: CGRectMake(marginX, offsetY, width, height))
        hourLabel.text = "hour"
        self.addSubview(hourLabel)

        let minsLabel = UILabel(frame: CGRectMake(marginX + offsetX, offsetY, width, height))
        minsLabel.text = "min"
        self.addSubview(minsLabel)*/
    }

    func getDate() -> NSDate{
        let dateFormatter = NSDateFormatter()
        dateFormatter.dateFormat = "HH:mm"
        let date = dateFormatter.dateFromString(String(format: "%02d", self.hour) + ":" + String(format: "%02d", self.minute))
        return date!
    }

    func numberOfComponentsInPickerView(pickerView: UIPickerView) -> Int {
        return 2
    }

    func pickerView(pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) {
        switch component {
        case 0:
            self.hour = row
        case 1:
            self.minute = row
        default:
            println("No component with number \(component)")
        }
    }

    func pickerView(pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int {
        if component == 0 {
            return 24
        }

        return 60
    }

    func pickerView(pickerView: UIPickerView, rowHeightForComponent component: Int) -> CGFloat {
        return 30
    }

    func pickerView(pickerView: UIPickerView, viewForRow row: Int, forComponent component: Int, reusingView view: UIView!) -> UIView {
        if (view != nil) {
            (view as UILabel).text = String(format:"%02lu", row)
            return view
        }
        let columnView = UILabel(frame: CGRectMake(35, 0, self.frame.size.width/3 - 35, 30))
        columnView.text = String(format:"%02lu", row)
        columnView.textAlignment = NSTextAlignment.Center

        return columnView
    }

}
Mikael
  • 2,355
  • 1
  • 21
  • 45
7

I did not like the answers that didn't rely on adding UILabel as subviews to UIPickerView, because hardcoding the frames will break if iOS decides to change the appearance someday.

A simple trick is to have the hour/min/sec labels as components with 1 row.

The hour/min/sec labels will be further from their values, but that's okay. Most users will not even notice that. See for yourself:

enter image description here

samwize
  • 25,675
  • 15
  • 141
  • 186
4

I made some slight improvements to @Stonz2's version. There was also an error with the calculation of the y position of the labels.

Here the .h file

@interface CustomUIDatePicker : UIPickerView <UIPickerViewDataSource, UIPickerViewDelegate>

@property NSInteger hours;
@property NSInteger mins;
@property NSInteger secs;

-(NSInteger) getPickerTimeInMS;
-(void) initialize;

@end

And here the .m file with the improvements

@implementation CustomUIDatePicker
-(instancetype)init {
 self = [super init];
 [self initialize];
 return self;
}

-(id)initWithCoder:(NSCoder *)aDecoder {
 self = [super initWithCoder:aDecoder];
 [self initialize];
 return self;
}

-(instancetype)initWithFrame:(CGRect)frame {
 self = [super initWithFrame:frame];
 [self initialize];
 return self;
}

-(void) initialize {
self.delegate = self;
self.dataSource = self;

int height = 20;
int offsetX = self.frame.size.width / 3;
int offsetY = self.frame.size.height / 2 - height / 2;
int marginX = 42;
int width = offsetX - marginX;

UILabel *hourLabel = [[UILabel alloc] initWithFrame:CGRectMake(marginX, offsetY, width, height)];
hourLabel.text = @"hour";
[self addSubview:hourLabel];

UILabel *minsLabel = [[UILabel alloc] initWithFrame:CGRectMake(marginX + offsetX, offsetY, width, height)];
minsLabel.text = @"min";
[self addSubview:minsLabel];

UILabel *secsLabel = [[UILabel alloc] initWithFrame:CGRectMake(marginX + offsetX * 2, offsetY, width, height)];
secsLabel.text = @"sec";
[self addSubview:secsLabel];
}

-(void)pickerView:(UIPickerView *)pickerView didSelectRow:(NSInteger)row inComponent:(NSInteger)component {
if (component == 0) {
    self.hours = row;
} else if (component == 1) {
    self.mins = row;
} else if (component == 2) {
    self.secs = row;
}
}

-(NSInteger)getPickerTimeInMS {
return (self.hours * 60 * 60 + self.mins * 60 + self.secs) * 1000;
}

-(NSInteger)numberOfComponentsInPickerView:(UIPickerView *)pickerView {
return 3;
}
-(NSInteger)pickerView:(UIPickerView *)pickerView numberOfRowsInComponent:(NSInteger)component
{
 if(component == 0)
     return 24;

 return 60;
}

- (CGFloat)pickerView:(UIPickerView *)pickerView rowHeightForComponent:(NSInteger)component
{
return 30;
}

-(UIView *)pickerView:(UIPickerView *)pickerView viewForRow:(NSInteger)row forComponent:(NSInteger)component reusingView:(UIView *)view
{
if (view != nil) {
    ((UILabel*)view).text = [NSString stringWithFormat:@"%lu", row];
    return view;
}
UILabel *columnView = [[UILabel alloc] initWithFrame:CGRectMake(35, 0, self.frame.size.width/3 - 35, 30)];
columnView.text = [NSString stringWithFormat:@"%lu", row];
columnView.textAlignment = NSTextAlignmentLeft;

return columnView;
}

@end
ph1lb4
  • 1,982
  • 17
  • 24
  • ty! may I ask you if I only want 2 columns (minutes, seconds) what should I change? I tried to edit the code a little but couldn't get the result I wanted – LS_ Mar 02 '15 at 15:49
  • 1
    `numberOfComponentsInPickerView` has to return 2 instead. and in `initialize` you should only create two subviews. also the offsetX should only be something like `self.frame.size.width / 2;` – ph1lb4 Mar 02 '15 at 17:56
2

One option is to use ActionSheetPicker

https://github.com/skywinder/ActionSheetPicker-3.0

and utilize the ActionSheetMultipleStringPicker. You can follow the example code in the ActionSheetPicker documents.

1

I have a simple solution here, you can implement 3 method of UIPickerViewDataSource for UIPickerView

    - (NSInteger)numberOfComponentsInPickerView:(UIPickerView *)pickerView{
    return 5;
}
- (NSInteger)pickerView:(UIPickerView *)pickerView numberOfRowsInComponent:(NSInteger)component{
    switch (component) {
        case 0:
            return 24;
            break;
        case 1:
        case 3:
            return 1;
            break;
        case 2:
        case 4:
            return 60;
            break;
        default:
            return 1;
            break;
    }
}

- (NSString*)pickerView:(UIPickerView *)pickerView titleForRow:(NSInteger)row forComponent:(NSInteger)component
{
    switch (component) {
        case 1:
        case 3:
            return @":";
            break;
        case 0:
        case 2:
        case 4:
            return [NSString stringWithFormat:@"%lu", (long) row];
            break;
        default:
            return @"";
            break;
    }
}

enter image description here

Hien.Nguyen
  • 282
  • 5
  • 3
0

Here's another solution for the "hour/min/sec" overlay. It uses the pickerView(UIPickerView, didSelectRow: Int, inComponent: Int) method from the picker view's delegate to update the view's label.

This way, it will work no matter the screen orientation. It still isn't optimal but it's quite straightforward.

UIPickerView example

func pickerView(_ pickerView: UIPickerView, viewForRow row: Int, forComponent component: Int, reusing view: UIView?) -> UIView {
    let label = UILabel()
    label.text = String(row)
    label.textAlignment = .center
    return label
}

func pickerView(_ pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) {

    if let label = pickerView.view(forRow: row, forComponent: component) as? UILabel {

        if component == 0, row > 1 {
            label.text = String(row) + " hours"
        }
        else if component == 0 {
            label.text = String(row) + " hour"
        }
        else if component == 1 {
            label.text = String(row) + " min"
        }
        else if component == 2 {
            label.text = String(row) + " sec"
        }
    }
}

To have the overlay appear when the picker view is displayed, rows must be programmatically selected...

func selectPickerViewRows() {

    pickerView.selectRow(0, inComponent: 0, animated: false)
    pickerView.selectRow(0, inComponent: 1, animated: false)
    pickerView.selectRow(30, inComponent: 2, animated: false)

    pickerView(pickerView, didSelectRow: 0, inComponent: 0)
    pickerView(pickerView, didSelectRow: 0, inComponent: 1)
    pickerView(pickerView, didSelectRow: 30, inComponent: 2)
}
nyg
  • 2,380
  • 3
  • 25
  • 40
  • Good answer @nyg but you shouldn't use label.text?.append(" min") as if you select twice the row, you got the word twice. Much better to use label.text = String(row)+" min".. But Thanks works great – Pierre Jul 30 '17 at 17:00