2012-04-04 8 views
15

Ho notato che the UIDatePicker doesn't work with NSHebrewCalendar in iOS 5.0 o 5.1. Ho deciso di provare a scrivere il mio. Sono confuso su come popolare i dati e su come mantenere le etichette per le date in modo efficiente e intelligente.Come si riscrive il componente UIDatePicker?

Quante righe ci sono effettivamente in ogni componente? Quando le file vengono "ricaricate" con nuove etichette?

Ho intenzione di fare un tentativo, e posterò come ho scoperto, ma per favore postare se sapete qualcosa.

risposta

35

Prima di tutto, grazie per aver archiviato l'errore UIDatePicker e il calendario ebraico. :)


EDIT Ora che iOS 6 è stato rilasciato, troverete che UIDatePicker ora funziona correttamente con il calendario ebraico, rendendo il codice qui sotto non necessario. Tuttavia, lo lascerò ai posteri.


Come hai scoperto, la creazione di una data funzionamento Picker è un problema difficile, perché ci sono bajillions di casi limite strani per coprire. Il calendario ebraico è particolarmente strano a questo riguardo, avendo uno intercalary month (Adar I), mentre la maggior parte della civiltà occidentale è usata per un calendario che aggiunge solo un giorno in più circa una volta ogni 4 anni.

Detto questo, la creazione di un raccoglitore di date ebraico minimo non è troppo complessa, presupponendo che siate disposti a rinunciare ad alcune delle sottigliezze offerte da UIDatePicker. Quindi cerchiamo di fare le cose semplici:

@interface HPDatePicker : UIPickerView 

@property (nonatomic, retain) NSDate *date; 

- (void)setDate:(NSDate *)date animated:(BOOL)animated; 

@end 

Stiamo semplicemente andando a creare una sottoclasse UIPickerView e aggiungere il supporto per una proprietà date.Ignoreremo minimumDate, maximumDate, locale, calendar, timeZone e tutte le altre proprietà divertenti fornite da UIDatePicker. Questo renderà il nostro lavoro molto più semplice.

L'implementazione sta per iniziare con un'estensione di classe:

@interface HPDatePicker() <UIPickerViewDelegate, UIPickerViewDataSource> 

@end 

semplicemente per nascondere che il HPDatePicker è proprio delegato e origine dati.

successiva definiremo un paio di costanti a portata di mano:

#define LARGE_NUMBER_OF_ROWS 10000 

#define MONTH_COMPONENT 0 
#define DAY_COMPONENT 1 
#define YEAR_COMPONENT 2 

Si può vedere qui che stiamo andando a codificare l'ordine delle unità di calendario. In altre parole, questo selettore di date mostrerà sempre le cose come mese-giorno-anno, indipendentemente da eventuali personalizzazioni o impostazioni locali che l'utente potrebbe avere. Quindi, se stai usando questo in un locale in cui il formato predefinito vorrebbe "Day-Month-Year", allora troppo male. Per questo semplice esempio, questo sarà sufficiente.

Ora avviare l'attuazione:

@implementation HPDatePicker { 
    NSCalendar *hebrewCalendar; 
    NSDateFormatter *formatter; 

    NSRange maxDayRange; 
    NSRange maxMonthRange; 
} 

- (id)initWithFrame:(CGRect)frame 
{ 
    self = [super initWithFrame:frame]; 
    if (self) { 
     // Initialization code 
     [self setDelegate:self]; 
     [self setDataSource:self]; 

     [self setShowsSelectionIndicator:YES]; 

     hebrewCalendar = [[NSCalendar alloc] initWithCalendarIdentifier:NSHebrewCalendar]; 
     formatter = [[NSDateFormatter alloc] init]; 
     [formatter setCalendar:hebrewCalendar]; 

     maxDayRange = [hebrewCalendar maximumRangeOfUnit:NSDayCalendarUnit]; 
     maxMonthRange = [hebrewCalendar maximumRangeOfUnit:NSMonthCalendarUnit]; 

     [self setDate:[NSDate date]]; 
    } 
    return self; 
} 

- (void)dealloc { 
    [hebrewCalendar release]; 
    [formatter release]; 
    [super dealloc]; 
} 

Stiamo ignorando l'inizializzatore designato per fare qualche messa a punto per noi. Impostiamo il delegato e l'origine dati per essere noi stessi, mostriamo l'indicatore di selezione e creiamo un oggetto calendario ebraico. Creiamo anche un NSDateFormatter e diciamo che dovrebbe formattare NSDates secondo il calendario ebraico. Estraggiamo anche un paio di oggetti NSRange e li memorizziamo nella cache come ivars, quindi non dobbiamo cercare costantemente le cose. Infine, inizializziamo con la data corrente.

Ecco le implementazioni dei metodi esposti:

- (void)setDate:(NSDate *)date { 
    [self setDate:date animated:NO]; 
} 

-setDate: appena avanti al altro metodo

- (NSDate *)date { 
    NSDateComponents *c = [self selectedDateComponents]; 
    return [hebrewCalendar dateFromComponents:c]; 
} 

recuperare un NSDateComponents rappresenta indipendentemente selezionato al momento, trasformarlo in un NSDate e restituiscilo.

- (void)setDate:(NSDate *)date animated:(BOOL)animated { 
    NSInteger units = NSYearCalendarUnit | NSMonthCalendarUnit | NSDayCalendarUnit; 
    NSDateComponents *components = [hebrewCalendar components:units fromDate:date]; 

    { 
     NSInteger yearRow = [components year] - 1; 
     [self selectRow:yearRow inComponent:YEAR_COMPONENT animated:animated]; 
    } 

    { 
     NSInteger middle = floor([self pickerView:self numberOfRowsInComponent:MONTH_COMPONENT]/2); 
     NSInteger startOfPhase = middle - (middle % maxMonthRange.length) - maxMonthRange.location; 
     NSInteger monthRow = startOfPhase + [components month]; 
     [self selectRow:monthRow inComponent:MONTH_COMPONENT animated:animated]; 
    } 

    { 
     NSInteger middle = floor([self pickerView:self numberOfRowsInComponent:DAY_COMPONENT]/2); 
     NSInteger startOfPhase = middle - (middle % maxDayRange.length) - maxDayRange.location; 
     NSInteger dayRow = startOfPhase + [components day]; 
     [self selectRow:dayRow inComponent:DAY_COMPONENT animated:animated]; 
    } 
} 

Ed è qui che iniziano le cose divertenti.

In primo luogo, prendiamo la data che ci è stata data e chiediamo al calendario ebraico di suddividerlo in un oggetto di componenti data. Se mi danno un NSDate che corrisponde alla data gregoriano di 4 Apr 2012, poi il calendario ebraico sta per darmi un oggetto NSDateComponents che corrisponde al 12 Nisan 5772, che è lo stesso giorno del 4 Apr 2012.

Sulla base di queste informazioni, ho individuato quale riga selezionare in ciascuna unità e selezionarla. Il caso dell'anno è semplice. Ho semplicemente sottratto uno (le righe sono basate su zero, ma gli anni sono basati su 1).

Per mesi, prendo il mezzo della colonna delle righe, capisco da dove inizia quella sequenza e aggiungo il numero del mese. Lo stesso con i giorni.

Le implementazioni dei metodi base <UIPickerViewDataSource> sono abbastanza banali. Stiamo visualizzando 3 componenti e ognuno ha 10.000 righe.

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

- (NSInteger)pickerView:(UIPickerView *)pickerView numberOfRowsInComponent:(NSInteger)component { 
    return LARGE_NUMBER_OF_ROWS; 
} 

Ottenere ciò che viene selezionato al momento attuale è abbastanza semplice. Ottengo la riga selezionata in ciascun componente e ne aggiungo 1 (nel caso di NSYearCalendarUnit) o eseguo una piccola operazione mod per tenere conto della natura ripetitiva delle altre unità del calendario.

- (NSDateComponents *)selectedDateComponents { 
    NSDateComponents *c = [[NSDateComponents alloc] init]; 

    [c setYear:[self selectedRowInComponent:YEAR_COMPONENT] + 1]; 

    NSInteger monthRow = [self selectedRowInComponent:MONTH_COMPONENT]; 
    [c setMonth:(monthRow % maxMonthRange.length) + maxMonthRange.location]; 

    NSInteger dayRow = [self selectedRowInComponent:DAY_COMPONENT]; 
    [c setDay:(dayRow % maxDayRange.length) + maxDayRange.location]; 

    return [c autorelease]; 
} 

Infine, ho bisogno di alcune stringhe di mostrare nell'interfaccia utente:

- (NSString *)pickerView:(UIPickerView *)pickerView titleForRow:(NSInteger)row forComponent:(NSInteger)component { 
    NSString *format = nil; 
    NSDateComponents *c = [[NSDateComponents alloc] init]; 

    if (component == YEAR_COMPONENT) { 
     format = @"y"; 
     [c setYear:row+1]; 
     [c setMonth:1]; 
     [c setDay:1];   
    } else if (component == MONTH_COMPONENT) { 
     format = @"MMMM"; 
     [c setYear:5774]; 
     [c setMonth:(row % maxMonthRange.length) + maxMonthRange.location]; 
     [c setDay:1]; 
    } else if (component == DAY_COMPONENT) { 
     format = @"d"; 
     [c setYear:5774]; 
     [c setMonth:1]; 
     [c setDay:(row % maxDayRange.length) + maxDayRange.location]; 
    } 

    NSDate *d = [hebrewCalendar dateFromComponents:c]; 
    [c release]; 

    [formatter setDateFormat:format]; 

    NSString *title = [formatter stringFromDate:d]; 

    return title; 
} 

@end 

Questo è dove le cose sono un po 'più complicato. Sfortunatamente per noi, NSDateFormatter è in grado di formattare solo le cose quando viene dato un effettivo NSDate. Non posso semplicemente dire "ecco un 6" e spero di tornare "Adar I". Quindi, devo costruire una data artificiale che abbia il valore che voglio nell'unità a cui tengo.

Nel caso di anni, è piuttosto semplice. Basta creare un componente per la data per quell'anno su Tishri 1 e sto bene.

Per mesi, devo assicurarmi che l'anno sia bisestile. Così facendo, posso garantire che i nomi dei mesi saranno sempre "Adar I" e "Adar II", indipendentemente dal fatto che l'anno in corso sia o meno un anno bisestile.

Per giorni, ho scelto un anno arbitrario, perché ogni Tishri ha 30 giorni (e nessun mese nel calendario ebraico ha più di 30 giorni).

Una volta che abbiamo costruito la data appropriata componenti oggetto, si può rapidamente trasformarsi in un NSDate con il nostro hebrewCalendar Ivar, impostare la stringa di formato della data formattatore solo essere la produzione di corde per l'unità ci sta a cuore, e generare una stringa.

Supponendo che hai fatto tutto questo correttamente, vi ritroverete con questo:

custom hebrew date picker


Alcune note:

  • ho lasciato fuori l'attuazione di -pickerView:didSelectRow:inComponent:. Spetta a te capire come si desidera notificare che la data selezionata è cambiata.

  • Questo non gestisce le date non valide non valide. Ad esempio, potresti voler considerare l'annullamento di "Adar I" se l'anno selezionato non è un anno bisestile. Ciò richiederebbe l'utilizzo di -pickerView:viewForRow:inComponent:reusingView: invece del metodo titleForRow:.

  • UIDatePicker evidenzierà la data corrente in blu. Ancora una volta, dovresti restituire una vista personalizzata anziché una stringa per farlo.

  • Il datario avrà una cornice nerastra, perché è un UIPickerView. Solo lo UIDatePickers ottiene quello bluastro.

  • I componenti della vista di selezione copriranno l'intera larghezza. Se si desidera che le cose si adattino in modo più naturale, sarà necessario eseguire l'override di -pickerView:widthForComponent: per restituire un valore corretto per il componente appropriato. Ciò potrebbe comportare la codifica di un valore o la generazione di tutte le stringhe, il ridimensionamento di ciascuna di esse e la selezione della più grande (più una spaziatura interna).

  • Come indicato in precedenza, questo visualizza sempre le cose nell'ordine Mese-Giorno-Anno. Rendere questa dinamica al locale corrente sarebbe un po 'più complicato. Dovresti ottenere una stringa @"d MMMM y" localizzata nelle impostazioni internazionali correnti (suggerimento: guarda i metodi di classe su NSDateFormatter per quello), quindi analizzala per capire qual è l'ordine.

+10

Questa è la migliore risposta che abbia mai visto sullo stack overflow. +1 –

+0

Questo codice diventa ancora più fantastico con ARC, btw. ;-) Jus 'dicendo. – Moshe

+3

@Dave DeLong è il mio eroe. –

1

Suppongo tu stia utilizzando UIPickerView?

UIPickerView funziona come un UITableView a che si specifica un'altra classe ad essere il dataSource/delegato, e si ottiene è di dati chiamando i metodi su quella classe (che dovrebbe essere conforme al UIPickerViewDelegate/protocollo DataSource).

Quindi, ciò che si dovrebbe fare è creare una nuova classe che sia una sottoclasse di UIPickerView, quindi nel metodo initWithFrame, impostarla come propria origine dati/delegato e quindi implementare i metodi.

Se non hai mai utilizzato un UITableView, dovresti familiarizzare con lo schema dell'origine dati/delegati perché è un po 'complicato capirlo, ma una volta capito, è molto facile da usare.

Se è utile, ho scritto un UIPicker personalizzato per la selezione dei paesi. Questo funziona più o meno nello stesso modo in cui si vorrà fare la classe, se non che sto utilizzando una matrice di nomi di paesi, invece di date:

https://github.com/nicklockwood/CountryPicker

Per quanto riguarda la domanda di memoria, l'UIPickerView carica solo le etichette visibili sullo schermo e le ricicla mentre scorrono dal basso verso l'alto e torna in alto, chiamando di nuovo il delegato ogni volta che è necessario visualizzare una nuova riga. Questo è molto efficiente in termini di memoria poiché ci saranno solo poche etichette sullo schermo alla volta.

+0

Ci sono date più possibili di possibili paesi, penso che sia la vera domanda qui: quante righe includi nel tuo programma di selezione delle date? – jrturton

+1

Hai diviso il tuo raccoglitore in tre colonne, quindi aggiungi giorni (31 righe) mesi (12 righe) e anni (un intervallo adatto, in genere 1900 - presente). UIPickerViewDelegate ha un callback numberofColumns che ti permette di impostare quante colonne vuoi. –

+0

In alternativa, se preferisci avere una colonna, imposta semplicemente una proprietà data min/max sul tuo selettore personalizzato e quindi genera una riga per ogni data tra min e max. –