Comment réécrire le composant UIDatePicker?

J'ai remarqué que le UIDatePicker ne fonctionne pas avec NSHebrewCalendar dans iOS 5.0 ou 5.1. J'ai décidé d'essayer d'écrire le mien. Je suis confus quant à la façon de remplir datatables et comment maintenir les labels pour les dates d'une manière saine et efficace.

Combien y a-t-il de lignes dans chaque composant? Quand les lignes sont-elles "rechargées" avec de nouvelles labels?

Je vais donner un coup de feu, et je postrai comme je le découvre, mais s'il vous plaît postz si vous savez quelque chose.

Tout d'abord, merci de déposer le bug sur UIDatePicker et le calendar hébreu. 🙂


EDIT Maintenant que iOS 6 a été publié, vous findez que UIDatePicker fonctionne maintenant correctement avec le calendar hébreu, rendant le code ci-dessous inutile. Cependant, je vais le laisser pour la postérité.


Comme vous l'avez découvert, créer un sélecteur de date fonctionnel est un problème difficile, car il y a des bajillions de cas bizarres à couvrir. Le calendar hébreu est particulièrement étrange à cet égard, ayant un mois intercalaire (Adar I), alors que la plupart de la civilisation occidentale est habituée à un calendar qui ajoute seulement un jour supplémentaire environ tous les 4 ans.

Cela étant dit, créer un sélecteur de date hébreu minimal n'est pas trop complexe, en supposant que vous êtes prêt à renoncer à certaines des UIDatePicker qu'offre UIDatePicker . Alors faisons simple:

 @interface HPDatePicker : UIPickerView @property (nonatomic, retain) NSDate *date; - (void)setDate:(NSDate *)date animated:(BOOL)animated; @end 

Nous allons simplement sous- UIPickerView et append le support pour une propriété de date . Nous allons ignorer minimumDate , maximumDate , locale , calendar , timeZone , et toutes les autres propriétés amusantes que UIDatePicker fournit. Cela rendra notre travail beaucoup plus simple.

L'implémentation va commencer avec une extension de class:

 @interface HPDatePicker () <UIPickerViewDelegate, UIPickerViewDataSource> @end 

Simplement pour cacher que HPDatePicker est son propre délégué et source de données.

Ensuite, nous allons définir quelques constantes maniables:

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

Vous pouvez voir ici que nous allons coder en dur l'ordre des unités de calendar. En d'autres termes, ce sélecteur de date affiche toujours les éléments en tant que mois-jour-année, quelles que soient les personnalisations ou les parameters régionaux que l'user peut avoir. Donc, si vous utilisez ceci dans un environnement où le format par défaut voudrait "Day-Month-Year", alors c'est dommage. Pour cet exemple simple, cela suffira.

Maintenant, nous commençons la mise en œuvre:

 @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]; } 

Nous remplaçons l'initialiseur désigné pour effectuer une configuration pour nous. Nous définissons le délégué et la source de données comme étant nous-même, montrons l'indicateur de sélection et créons un object de calendar hébreu. Nous créons également un NSDateFormatter et lui disons qu'il doit formater NSDates selon le calendar hébreu. Nous NSRange objects NSRange et les mettons en cache sous forme d'ivars afin que nous n'ayons pas à chercher constamment des choses. Enfin, nous l'initialisons avec la date actuelle.

Voici les implémentations des methods exposées:

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

-setDate: juste à l'autre méthode

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

Récupérez un NSDateComponents représentant tout ce qui est sélectionné en ce moment, transformez-le en NSDate et renvoyez-le.

 - (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]; } } 

Et c'est là que des choses amusantes commencent à se produire.

D'abord, nous prendrons la date qui nous a été donnée et requestrons au calendar hébreu de la décomposer en un object de composants de date. Si je lui donne un NSDate correspondant à la date grégorienne du 4 avril 2012, alors le calendar hébreu va me donner un object NSDateComponents correspondant à 12 Nisan 5772, soit le même jour que le 4 avril 2012.

En fonction de ces informations, je détermine quelle ligne sélectionner dans chaque unité et la sélectionne. Le cas de l'année est simple. Je soustrais simplement un (les lignes sont basées sur zéro, mais les années sont basées sur 1).

Pendant des mois, je choisis la colonne du milieu de la rangée, détermine où commence cette séquence et ajoute le numéro du mois. La même chose avec les jours.

Les implémentations des methods de base <UIPickerViewDataSource> sont assez sortingviales. Nous affichons 3 composants, et chacun a 10 000 lignes.

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

Obtenir ce qui est sélectionné au moment présent est assez simple. Je reçois la ligne sélectionnée dans chaque composant et ajoute 1 (dans le cas de NSYearCalendarUnit ), ou effectue une petite opération mod pour tenir count de la nature répétitive des autres unités du calendar.

 - (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]; } 

Enfin, j'ai besoin de certaines strings pour montrer dans l'interface user:

 - (NSSsortingng *)pickerView:(UIPickerView *)pickerView titleForRow:(NSInteger)row forComponent:(NSInteger)component { NSSsortingng *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]; NSSsortingng *title = [formatter ssortingngFromDate:d]; return title; } @end 

C'est là que les choses sont un peu plus compliquées. Malheureusement pour nous, NSDateFormatter ne peut formater les choses que lorsqu'on lui donne un NSDate réel. Je ne peux pas juste dire "voici un 6" et j'espère revenir "Adar I". Ainsi, je dois build une date artificielle qui a la valeur que je veux dans l'unité qui m'intéresse.

En ce qui concerne les années, c'est plutôt simple. Il suffit de créer un composant date pour cette année sur Tishri 1, et je vais bien.

Pendant des mois, je dois m'assurer que l'année est une année bissextile. Ce faisant, je peux garantir que les noms des mois seront toujours "Adar I" et "Adar II", que l'année en cours soit ou non une année bissextile.

Pendant des jours, j'ai choisi une année arbitraire, parce que chaque Tishri a 30 jours (et aucun mois dans le calendar hébreu n'a plus de 30 jours).

Une fois que nous avons construit l'object de composants de date approprié, nous pouvons rapidement le transformer en NSDate avec notre hebrewCalendar ivar, définir la string de format sur le formateur de date pour produire uniquement des strings pour l'unité qui nous intéresse et générer une string.

En supposant que vous avez fait tout cela correctement, vous finirez avec ceci:

sélecteur de date hébreu personnalisé


Quelques notes:

  • J'ai -pickerView:didSelectRow:inComponent: l'implémentation de -pickerView:didSelectRow:inComponent: C'est à vous de déterminer comment vous voulez avertir que la date sélectionnée a changé.

  • Cela ne gère pas les dates invalides. Par exemple, vous pouvez envisager de supprimer "Adar I" si l'année sélectionnée n'est pas une année bissextile. Cela nécessiterait l'utilisation de -pickerView:viewForRow:inComponent:reusingView: place de la méthode -pickerView:viewForRow:inComponent:reusingView:

  • UIDatePicker mettra en évidence la date actuelle en bleu. Encore une fois, vous devrez returnner une vue personnalisée au lieu d'une string pour le faire.

  • Votre sélecteur de date aura une lunette noirâtre, car il s'agit d'un UIPickerView . Seulement UIDatePickers obtient le bleu.

  • Les composants de la vue du sélecteur s'étendront sur toute sa largeur. Si vous voulez que les choses s'adaptent plus naturellement, vous devrez surcharger -pickerView:widthForComponent: pour renvoyer une valeur saine pour le composant approprié. Cela peut impliquer de coder en dur une valeur ou de générer toutes les strings, de les dimensionner chacune et de choisir la plus grande (plus un peu de remplissage).

  • Comme indiqué précédemment, cela affiche toujours les choses dans l'ordre Mois-Jour-Année. Rendre cette dynamic aux parameters régionaux actuels serait un peu plus compliqué. Vous devez get une string @"d MMMM y" localisée à l'environnement local actuel (indice: regardez les methods de class sur NSDateFormatter pour cela), puis l'parsingr pour déterminer quel est l'ordre.

Je suppose que vous utilisez un UIPickerView?

UIPickerView fonctionne comme un UITableView en spécifiant une autre class à dataSource / delegate, et il obtient ses données en appelant des methods sur cette class (qui doit être conforme au protocole UIPickerViewDelegate / DataSource).

Donc, ce que vous devriez faire est de créer une nouvelle class qui est une sous-class de UIPickerView, puis dans la méthode initWithFrame, définissez-la comme étant sa propre source de données / délégué, puis implémentez les methods.

Si vous n'avez jamais utilisé UITableView auparavant, vous devez vous familiariser avec le model datasource / delegate, car il est un peu difficile de comprendre, mais une fois que vous l'avez compris, il est très facile à utiliser.

Si cela vous aide, j'ai écrit un UIPicker personnalisé pour sélectionner les pays. Cela fonctionne à peu près de la même façon que vous voulez faire votre class, sauf que j'utilise un tableau de noms de pays au lieu de dates:

https://github.com/nicklockwood/CountryPicker

En ce qui concerne la question de la memory, UIPickerView ne charge que les labels visibles à l'écran et les recycle lorsqu'elles défilent vers le bas et vers le haut, en appelant à nouveau votre délégué chaque fois qu'il doit afficher une nouvelle ligne. Ceci est très efficace car il n'y aura que quelques labels à la fois.