Динамические древовидные меню на iPhone
написал Владимир Пузанов - October 17, 2008 – 14:01Эта статья по идее должна была бы идти перед “Динамической подгрузкой xib интерфейсов на iPhone“, но вышло так, что я забыл описать эту методику. Что сейчас и исправляю.
В описанном ниже коде содержится методика загрузки меню по XML-описанию, и обработка вызовов элементов.И так, для начала опишем формат меню. Для этого мы будем использовать Apple Property Lists, т.к. они загружаются в дерево объектов буквально одной строкой.
Пункты меню могут быть двух типов: либо они вызывают какое-то действие, либо содержат подменю. Необходимые поля хранятся в структуре NSDictionary: это обязательное поле title типа string (название пункта меню или заголовок всего меню в корневом элементе), поле items типа array (содержит массив подэлементов) или поля action и params, оба типа string (описывают имя вызываемого метода и передаваемый аргумент). Вот пример:
title
Test Menu
items
title
Item 1
items
title
Item 1.1
action
doMenu
params
1.1
title
Item 1.2
action
doMenu
params
1.2
title
Item 1.3
items
title
Item 1.3.1
action
doMenu
params
My sub-sub-menu
title
Item 1.3.2
action
doMenu
params
1.3.2
title
Item 2
action
doItem2
params
Корневой элемент меню не может содержать action/params, и обязан содержать items.
Код проекта основан на стандартном шаблоне с таблицей. По сути вся логика меню завязана на контроллере таблицы, а бизнес-логика – на отдельном внешнем классе. Не знаю, насколько это похоже на традиционный MVC, но в коде выглядит достаточно логично.
Для выполнения функций меню сделаем отдельный класс MenuController:
@interface MenuController : NSObject {
}
- (void) doItem2:(NSString *)attribs;
- (void) doMenu:(NSString *)attribs;
@end
@implementation MenuController
- (void) doItem2:(NSString *)attribs
{
NSLog(@"called item 2 with string '%@'", attribs);
}
- (void) doMenu:(NSString *)attribs
{
NSLog(@"called menu with string '%@'", attribs);
}
@end
В нем реализованы два метода, которые вызываются в вышеописанном меню. Что бы не усложнять пример, оповещения об обработке событий выводятся на консоль.
Интерфейс контроллера расширяем следующим образом:
@class MenuController;
@interface RootViewController : UITableViewController
{
IBOutlet UINavigationController *navController;
IBOutlet MenuController *menuController;
NSArray *items;
}
@property (readwrite,retain) NSArray *items;
- (void)setNavController:(UINavigationController *)nc menuController:(MenuController *)mc;
@end
navController в IB завязываем на корневой контроллер навигации, menuController – на экземпляр MenuController. В items хранятся элементы меню текущего контроллера.
В реализации мы для начала добавим статическую переменную menu:
static NSDictionary *menu = nil;
И напишем код, который будет загружать в нее наше дерево:
- (void)awakeFromNib
{
NSData *menuData = [NSData dataWithContentsOfFile:[[NSBundle mainBundle] pathForResource:@"menu" ofType:@"plist"]];
menu = [NSPropertyListSerialization
propertyListFromData:menuData
mutabilityOption:NSPropertyListImmutable
format:NULL
errorDescription:nil];
self.navigationItem.title = [menu objectForKey:@"title"];
items = [[menu objectForKey:@"items"] retain];
}
awakeFromNib будет вызван только один раз для контроллера, который находится в xib файле. Тут мы загружает menu.plist, инициализируем заголовок меню и корневые элементы.
Код протокола для datasource выглядит так:
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
{
return 1;
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
return [items count];
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"cell"];
if (cell == nil) {
cell = [[[UITableViewCell alloc] initWithFrame:CGRectZero reuseIdentifier:@"cell"] autorelease];
}
NSDictionary *i = [items objectAtIndex:indexPath.row];
cell.text = [i objectForKey:@"title"];
if([i objectForKey:@"items"]) {
cell.accessoryType = UITableViewCellAccessoryDisclosureIndicator;
} else {
cell.accessoryType = UITableViewCellAccessoryNone;
}
return cell;
}
У нас всегда одна секция, а количество элементов мы выбираем из нашего списка items. В зависимости от того, есть ли в словаре элемента подполе items или нет мы показываем на элементе меню стрелочку (тем самым давая понять пользователю, что у пункта есть подменю). Обращаю ваше внимание на то, что ветка else обязательна, т.к. мы можем получить ячейку из кеша, где она ранее могла содержать пункт меню другого типа.
Самый интересный момент – протокол для delegate. Тут то и происходит магия рекурсивных контроллеров:
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
NSDictionary *i = [items objectAtIndex:indexPath.row];
id subItems;
if( (subItems = [i objectForKey:@"items"]) ) {
RootViewController *subc = [[[RootViewController alloc] initWithNibName:@"RootViewController" bundle:nil] autorelease];
[subc setNavController:navController menuController:menuController];
subc.items = subItems;
subc.navigationItem.title = [i objectForKey:@"title"];
[navController pushViewController:subc animated:YES];
} else {
SEL s = NSSelectorFromString([[i objectForKey:@"action"] stringByAppendingString:@":"]);
[menuController performSelector:s
withObject:[i objectForKey:@"params"]];
}
}
Тут мы получаем словарь выбранного пункта и проверяем его тип (есть items – подменю, нет – меню с действием). В первом случае мы создаем новый конторллер загружая его таблицу из соответствующего xib, устанавливаем его поля navController и menuController (у корневого они привязаны еще в IB, а дочерним надо делать самому), задаем ему список элементов и заголовок, после чего кидаем на стек контроллеров над нами.
Если же у нас тут просто вызов действия – то получаем название метода (я напоминаю, что “:”, это тоже часть имени селектора, потому @selector(abc) не то же самое что и @selector(abc:)), и вызываем его над нашим MenuController.
Вот так, просто и эффективо:

2 Ответов к “Динамические древовидные меню на iPhone”
Исходнички выложите тоже ;)
написал Сергей на Nov 1, 2008
Собственно все необходимые куски кода есть в статье, но так уж и быть:
http://dl.getdropbox.com/u/248886/ByteFlow/MenuTreeNavigation.zip
написал Владимир Пузанов на Nov 1, 2008