dimanche 19 février 2012

À la recherche d'une meilleure façon: Vues de détail modifiable

Une partie du code le plus laid iPhone que j'ai écrit à ce jour - peut-être la plupart du code laide que j'ai écrit à ce jour - a été dans les contrôleurs de vue de table agissant comme vitres détails de montage. Normalement, lorsque vous utilisez une table de vue de l'utiliser pour présenter une liste de données à l'utilisateur d'un tableau ou fetch demande, et pour faire cela, l'architecture de tableau est beau. Mais, quand vous voulez l'utiliser pour présenter - et permettre à l'utilisateur de modifier - les propriétés d'un objet unique, les choses deviennent un peu gnarlier.

Bien sûr, vous pouvez utiliser Interface Builder pour construire votre vitres édition. Mais, la plupart des gens prennent leurs repères d'Apple sur la façon de faire les choses, et les vues d'édition détail sont généralement mis en œuvre sous forme de tableaux. Jetez un oeil à l'application Contacts, par exemple:



Il utilise des vues de table. Il en va de l'application Réglages. Ne nous leurrons pas, la plupart d'entre nous vont vouloir utiliser la même approche.

Mais, il est difficile d'écrire du code propre bonne pour gérer ces types de vues de montage détaillée. Parce que le montage de tout bien particulier pourrait être manipulé par une classe contrôleur différent, il est très difficile d'écrire du code pour appliquer ces élégantes. Vous vous retrouvez avec beaucoup de code similaires mais pas-facile-à-refactoriser.

La façon la plus évidente d'écrire ces contrôleurs est d'utiliser des énumérations de définir des sections de votre vue de table, et les lignes au sein de chaque section, un peu comme ceci:

typedef enum  
{
ProjectTableSectionNameSection,
ProjectTableSectionTasksAndExpensesSection,
ProjectTableSectionPrimaryContactSection,
ProjectTableSectionDateSection,
ProjectTableSectionBudgetSection,
ProjectTableSectionLocationSection,
ProjectTableSectionReportSection,
ProjectTableSectionCompleteButtonSection,
ProjectTableSectionDeleteButtonSection,

ProjectTableSectionNumberOfSections
}
ProjectTableSection;

typedef enum
{
ExpenseEditSectionName,
ExpenseEditSectionGeneral,
ExpenseEditSectionDate,
ExpenseEditSectionDeleteButton,

ExpenseEditSectionCount
}
ExpenseEditSection;
...

Ensuite, dans la mise en œuvre de votre classe, vous écrivez des déclarations switch pour gérer la logique de chaque section et la combinaison consécutive. L'avantage de cette approche est que vous pouvez réorganiser les lignes et sections juste en changeant les énumérations. Parce que chaque valeur dans l'énumération est un plus élevé que celui d'avant, il vous suffit de changer l'ordre des constantes, et les lignes ou les ordres de modification des sections dans la table réelle.

But...

Si vous avez un objet avec de nombreuses propriétés, divisé en plusieurs sections, vous vous retrouvez avec désagréables, difficiles à maintenir le code faisant cela. Bien sûr, vous n'avez pas à réorganiser ou changer de gros morceaux de code pour réorganiser l'aspect visuel, mais il est encore difficile de trouver le code que vous recherchez quand vous allez faire des changements ou corriger des bugs et il est toujours mêler la vue et pièces de contrôleur de MVC ensemble dans une situation de vie inconfortable. Vous êtes violer MVC, et la conception du contrôleur de tableau est en fait sorte de vous encourager à le faire.

Pour vous donner un exemple, voici un extrait de l'un des premiers (et absolument horrible) complexes vues d'édition basée sur des tables que j'ai écrit en utilisant cette technique:

- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
switch ([indexPath section])
{
case ProjectTableSectionNameSection:
{
TextFieldEditingViewController *controller = [[TextFieldEditingViewController alloc] initWithStyle:UITableViewStyleGrouped];
controller.fieldNames = [NSArray arrayWithObject:NSLocalizedString(@"Project Name", @"Project Name")];
controller.fieldKeys = [NSArray arrayWithObject:@"name"];
controller.fieldValues = [NSArray arrayWithObject:project.name];
controller.shouldClearOnEditing = [project.name isEqualToString:NSLocalizedString(@"Untitled Project", @"Untitled Project - name given to new projects")];

controller.delegate = self;
[self.navigationController pushViewController:controller animated:YES];
[controller release];
break;
}

case ProjectTableSectionPrimaryContactSection:
{
if ([indexPath row] == [project.contacts count])
{
ABPeoplePickerNavigationController *peoplePickerNavigationController = [[ABPeoplePickerNavigationController alloc] init];
peoplePickerNavigationController.peoplePickerDelegate = self;

[self presentModalViewController:peoplePickerNavigationController animated:YES];
[peoplePickerNavigationController release];
}

else
{
ABPersonViewController *controller = [[ABPersonViewController alloc] init];
controller.personViewDelegate = self;
controller.allowsEditing = YES;
controller.addressBook = project.addressBook;
ABRecordRef theRef = [project recordRefForContactAtIndex:[indexPath row]];
controller.displayedPerson = theRef;
[self.navigationController pushViewController:controller animated:YES];
[controller release];

}

break;
}

case ProjectTableSectionReportSection:
{
ReportViewController *controller = [[ReportViewController alloc] initWithNibName:@"ReportView" bundle:nil];
controller.reportHTML = [project projectReportAsHTML];
controller.title = NSLocalizedString(@"Project Report", @"Project Report");
[self.navigationController pushViewController:controller animated:YES];
[controller release];
break;
}

case ProjectTableSectionDateSection:
{
dateBeingEdited = ([indexPath row] == 0) ? project.startDate : project.expectedCompletionDate;
DateViewController *controller = [[DateViewController alloc] init];
controller.delegate = self;
controller.date = dateBeingEdited;
controller.title = ([indexPath row] == 0) ? NSLocalizedString(@"Start Date", @"Start Date") : NSLocalizedString(@"Exp. Completion", @"Expected Completion Date (abbreviated to fit in title bar)");
[self.navigationController pushViewController:controller animated:YES];
[controller release];
break;
}

case ProjectTableSectionBudgetSection:
{
TextFieldEditingViewController *controller = [[TextFieldEditingViewController alloc] initWithStyle:UITableViewStyleGrouped];
controller.fieldNames = [NSArray arrayWithObject:NSLocalizedString(@"Budget", @"Project Budget")];
controller.fieldKeys = [NSArray arrayWithObject:@"budget"];
controller.fieldValues = [NSArray arrayWithObject:[project.budget stringValue]];
controller.shouldClearOnEditing = ([project.budget doubleValue] == 0.0);
[controller setKeyboardType:UIKeyboardTypeNumberPad forIndex:0];

controller.delegate = self;
[self.navigationController pushViewController:controller animated:YES];
[controller release];
break;
}

case ProjectTableSectionTasksAndExpensesSection:
{
switch ([indexPath row])
{
case 0: // Tasks
{
TaskCategoryViewController *controller = [[TaskCategoryViewController alloc] initWithNibName:@"TaskCategoryView" bundle:nil];
controller.project = project;
[self.navigationController pushViewController:controller animated:YES];
[controller release];
break;
}

case 1: // Expenses
{
ExpenseListViewController *controller = [[ExpenseListViewController alloc] initWithStyle:UITableViewStylePlain];
controller.project = project;
[self.navigationController pushViewController:controller animated:YES];
[controller release];
break;
}

case 2: // Change Requests
{
ChangeRequestCategoryViewController *controller = [[ChangeRequestCategoryViewController alloc] initWithNibName:@"ChangeRequestCategoryView" bundle:nil];
controller.project = project;
[self.navigationController pushViewController:controller animated:YES];
[controller release];
break;
}

case 3: // Budget Metrics
{
BudgetMetricsViewController *controller = [[BudgetMetricsViewController alloc] initWithStyle:UITableViewStyleGrouped];
controller.project = project;
[self.navigationController pushViewController:controller animated:YES];
[controller release];
break;
}

default:
break;
}

break;
}

case ProjectTableSectionLocationSection:
{
LocationViewController *controller = [[LocationViewController alloc] initWithStyle:UITableViewStyleGrouped];
controller.project = project;
[self.navigationController pushViewController:controller animated:YES];
[controller release];
break;
}

case ProjectTableSectionDeleteButtonSection:
break;
default:
break;
}

[tableView deselectRowAtIndexPath:indexPath animated:YES];
}


Assez horrible, hein? Ouais. Ne fais pas ça, les enfants. Et ce qui est pire, c'est tout ce que le code était juste de traiter l'interaction de l'utilisateur à partir peut-être dix lignes de données divisées en une poignée de sections. Et c'est juste une méthode dans la classe contrôleur. Il ya plus longs, en fait. C'est laid, le code impurs quelque manière que vous le regardez.

Donc, depuis que j'ai écrit cette monstruosité qui précède, j'ai été à l'affût des moyens de rendre le processus d'utilisation de vues tableau pour afficher et modifier les propriétés d'un objet unique de données en utilisant une table plus gérable. Utilisation des classes de contrôleur générique comme le celles que j'ai écrit un certain temps aidé certains, mais pas assez et ne fixe pas la répartition de la paroi entre le C et V de MVC. Malheureusement, je me suis occupé, et j'ai mis la quête d'une meilleure solution sur un brûleur arrière.

Toutefois, l'une des applications que nous avons écrit pour les Plus l'iPhone 3 de développement (Sur les données fondamentales) utilise un de ces panneaux d'édition détail, et je voulais vraiment trouver quelque chose de plus élégant que j'ai commis, avant le code à imprimer pour le monde de voir. Alors, j'ai plongé de nouveau dans ce problème récemment et est venu avec quelque chose, je suis vraiment heureux avec, mais il a encore besoin de raffinement. C'est la preuve aa-of-concept scène maintenant, mais c'est une preuve de concept très prometteur.

Au lieu d'écrire une classe contrôleur personnalisé pour chaque vue d'édition détails dont j'ai besoin, je peux maintenant il suffit de créer une instance d'une classe contrôleur générique conçu pour ces types de vues de montage détaillée. Je passe l'emplacement d'un fichier de liste des biens à la méthode d'initialisation de ce contrôleur, et que la liste des biens définit la structure et l'apparence de la vue de détail basée sur des tables de montage. Il supporte les sections et les différents types d'éditeurs. Vous pouvez avoir l'utilisateur d'éditer un attribut dans un champ de texte, ou vous pouvez présenter une liste déroulante sans écrire de code, et vous pouvez ajouter des éditeurs supplémentaires simplement en dérivant une classe existante. Juste assembler une liste des biens en utilisant Xcode éditeur intégré la liste des biens et de transmettre la liste des biens qui dans la classe générique.

Vous voulez réorganiser les lignes ou sections? Il suffit de glisser les entrées correspondantes dans la liste des biens à leur nouvel emplacement. Voulez-vous ajouter une nouvelle section ou une nouvelle ligne dans une section? Il suffit d'ajouter une nouvelle entrée dans la liste des biens. Vous voulez supprimer une ligne ou section? Il suffit de supprimer l'entrée de la liste des biens.

Voici un exemple tiré de ma preuve de concept d'application. J'ai défini une liste de propriétés comme ceci:


Cliquez ici pour agrandir


Si vous regardez la liste des biens, vous voyez qu'il ya deux sections définies, et un total de trois rangées. L'application résultante ressemble à ceci:



Vous voyez? Deux sections, trois rangées, pas de code. Je viens d'instancier le contrôleur générique avec le chemin de la liste des biens:

    NSString *layoutPath = [[NSBundle mainBundle] pathForResource:@"HeroLayout" ofType:@"plist"];
ManagedObjectDetailEditor *controller = [[ManagedObjectDetailEditor alloc] initWithLayoutFile:layoutPath];
controller.managedObject = newManagedObject;
[self.navigationController pushViewController:controller animated:YES];
[controller release];

Vous avez sans doute remarqué que le dernier dictionnaire dans la liste des biens (représentant le sexe de la super-héros) a une valeur de clé supplémentaires appelées arguments. Cela donne la souplesse nécessaire pour passer des données supplémentaires à la classe contrôleur, vous pouvez donc faire des choses comme présenter une liste de valeurs que l'utilisateur peut choisir. Dans ce cas, nous les laissons choisir le sexe d'une liste qui comprend masculin et féminin, plutôt que de les rendre tapez texte de forme libre.



Ce code est encore à ses balbutiements avec des éditeurs de nombreux types de données de gauche à être développé, mais je pense qu'il ya beaucoup de potentiel ici pour gagner du temps aux développeurs. Je suis même de penser à développer un petit outil pour vous permettre de concevoir visuellement la table basée sur Core Data modéliser les données - mais ce serait tout à fait un des moyens sur toute la ligne. Même sans cela, et tout l'artisanat des listes de propriétés plutôt que des classes personnalisées, il ya un gain de temps énorme.

Le code source sera disponible dans le cadre du Plus l'iPhone 3 de développement archive du code source, et une section de l'ouvrage comprendra un tutoriel sur comment créer une liste de propriétés de définir une présentation vue de détail d'édition.

Aucun commentaire: