MVC на iPhone: «The Model» (Часть 1)

Original author: Keith Peters
  • Translation
CocoaTouch с самого начала создавалась с прицелом на парадигму MVC. Практически все шаблоны, представления и их контроллеры для пользователя уже готовы. Ключевые классы — "UIView" и "UIViewController". Во многих случаях метод "UIView" применим сам по себе — с добавлением элементов пользовательского интерфейса в общий "UIView" в редакторе IB. Для создания собственных функций добавляем подклассы к "UIViewController". Спецификаторы "IBOutlet" позволяют связывать элементы пользовательского интерфейса с представлением, обеспечивая к ним доступ.

А как быть с понятием «Model»? О нем информации я практически не нашел. В уроках по программированию с моделью предпочитают не работать, набирая код непосредственно в контроллерах.

Добившись, как мне показалось, неплохих результатов с реализацией, я предлагаю их здесь для обсуждения и оценки. Изложу вкратце. Я создаю класс "Singleton", расширяющий "NSObject" для моей модели. Потом посредством наблюдения за ключами/переменными узнаю об обновлениях. Это во многом напоминает "ModelLocator" из "Cairngorm", если кому-то приходилось работать с ним во "Flex".

Для начала создадим проект с парой представлений. Одно из них позволит пользователю менять значение. Это значение задается для модели, которая, в свою очередь, запускает изменение в другом представлении. Для этой цели вполне подойдет шаблон "Utility Application". Итак, создаем на его основе проект и присваиваем ему имя "MVC". (В принципе, имя может быть любым, но для удобства работы с уроком лучше его продублировать.)

Результат должен быть примерно таким:

mvc_01

Как видите, перед нами объекты "Main View" и "Flipside View", причем для обоих есть контроллеры и nib-файлы. Запустите проект и предварительно ознакомьтесь с кодом. Я буду концентрироваться в основном на моментах, касающихся модели, полагая, что остальное и так ясно.

Теперь добавим к представлениям метку и текстовое поле. Метка — для "Main View", поле, соответственно, для "Flipside View". Для начала займемся переменными типа outlet.

Файл "MainViewController.h" должен выглядеть так, как показано ниже.

#import

@interface MainViewController : UIViewController {
UILabel *label;
}

@property (nonatomic, retain) IBOutlet UILabel *label;
@end
[/cc]

а файл "<strong>MainViewController.m</strong>" — так:

[cc lang="objc"]
#import "MainViewController.h"
#import "MainView.h"

@implementation MainViewController
@synthesize label;

- (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil {
if (self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil]) {
// Пользовательская инициализация
}
return self;
}

/*
// Внедряем viewDidLoad для дополнительной настройки после загрузки изображения, как правило, из nib-файла.
- (void)viewDidLoad {
[super viewDidLoad];
}
*/

/*
// Отменяем, разрешая другие ориентации помимо заданной по умолчанию книжной.
- (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation {
// Return YES for supported orientations
return (interfaceOrientation == UIInterfaceOrientationPortrait);
}
*/

- (void)didReceiveMemoryWarning {
[super didReceiveMemoryWarning]; // Освобождаем представление, если у него нет superview
// Освобождаем все лишнее, например данные из кэша
}

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

@end


* This source code was highlighted with Source Code Highlighter.


Точно так же в файле "FlipsideViewController.h" добавляем переменную "textField", а также "IBAction", который даст нам знать об изменении текста в поле

#import

@interface FlipsideViewController : UIViewController {
UITextField *textField;
}

@property (nonatomic, retain) IBOutlet UITextField *textField;

- (IBAction)textChanged:(id)sender;
@end


* This source code was highlighted with Source Code Highlighter.


и FlipsideViewController.m:

#import "FlipsideViewController.h"

@implementation FlipsideViewController
@synthesize textField;

- (void)viewDidLoad {
[super viewDidLoad];
self.view.backgroundColor = [UIColor viewFlipsideBackgroundColor];
}

/*
// Override to allow orientations other than the default portrait orientation.
- (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation {
// Return YES for supported orientations
return (interfaceOrientation == UIInterfaceOrientationPortrait);
}
*/

- (void)didReceiveMemoryWarning {
[super didReceiveMemoryWarning]; // Releases the view if it doesn't have a superview
// Release anything that's not essential, such as cached data
}

- (IBAction)textChanged:(id)sender
{

}

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

@end


* This source code was highlighted with Source Code Highlighter.


Теперь можно открыть "MainView.xib" и добавить к представлению "UILabel". Перетащите от объекта «File's Owner» (класс "MainViewController") соединительную линию к метке, связав ее с соответствующей переменной.

mvc_02mvc_03

То же самое проделайте в файле "FlipsideView.xib", добавив элемент "UITextField" и связав его с переменной "textField" для "File's Owner" ("FlipsideViewController"). Подключите IBAction "textChanged" к событию "editingChanged" текстового поля.

mvc_04mvc_05

К этому времени, наверное, уже стало понятно, что наша цель — ввод пользователем некоего текста в поле, который затем появляется на метке. Но ведь это два отдельных представления со своими контроллерами? Как наладить взаимодействие между ними? С помощью модели, конечно. Сохранив оба nib-файла, закройте редактор IB и вернитесь в XCode.

Модель.


Настало время создать модель. Добавьте к проекту новый файл — подкласс для "NSObject". Дайте классу имя "Model". Не то, чтобы я был ярым поклонником шаблонов "Singleton". Я прекрасно осведомлен об их недостатках и считаю, что порой их задействуют неоправданно, но как прагматик, уверен, что в данном случае это приемлемый вариант.

Те, кому невыносима сама мысль о синглетонах, могут создать модель в "RootViewController" и передать по экземпляру каждому из контроллеров представления. Это должно сработать. Я же выберу другой способ, воспользовавшись остроумным файлом "SynthesizeSingleton" с [CocoaWithLove.com]. Кстати, там же можно высказать свое отношение к синглетонам.

Теперь добавьте к проекту файл "SynthesizeSingleton.h" и вызовите макрос "SYNTHESIZE_SINGLETON_FOR_CLASS" в реализации класса, который хотите сделать синглетоном.

Я заметил, что работа с данным макросом всегда вызывает предупреждение, если только не объявить статичный метод "sharedModel" в файле интерфейса.

Нашей модели также понадобятся одно единственное свойство, текст и пользовательские средства доступа. Вот файл "Model.h":

#import <Foundation/Foundation.h>

@interface Model : NSObject {
NSString *text;
}

@property (nonatomic, retain) NSString *text;

+ (Model *)sharedModel;
@end


* This source code was highlighted with Source Code Highlighter.


А вот и "Model.m":

#import "Model.h"
#import "SynthesizeSingleton.h"

@implementation Model

SYNTHESIZE_SINGLETON_FOR_CLASS(Model);

@synthesize text;

- (id) init
{
self = [super init];
if (self != nil) {
text = @"";
}
return self;
}

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

@end


* This source code was highlighted with Source Code Highlighter.


Настройка данных для модели.


При изменении текста в текстовом поле будет вызываться метод "textChanged" в контроллере "FlipsideViewController". Здесь можно присвоить новое значение свойству текста модели.

- (IBAction)textChanged:(id)sender
{
Model *model = [Model sharedModel];
model.text = textField.text;
}


* This source code was highlighted with Source Code Highlighter.


Обязательно импортируйте "Model.h" в "FlipsideViewController.m".

Фиксация изменений.


Основному представлению достаточно сообщить, когда будет меняться модель. Это можно сделать путем наблюдения за ключом/значением. Соответственно, нужно настроить наблюдателя на определенное свойство объекта, чтобы он сразу же вызывал метод в случае его изменения. Здесь нам нужно узнать, когда поменяется текстовое свойство для класса модели синглетона. Воспользуемся для этой цели методом "initWithNibName" класса "MainViewController". Вот как он выглядит:

- (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil {
if (self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil]) {
// Пользовательская инициализация
Model *model = [Model sharedModel];
[model addObserver:self forKeyPath:@"text" options:NSKeyValueObservingOptionNew context:nil];
}
return self;
}


* This source code was highlighted with Source Code Highlighter.


Для начала получаем ссылку на синглетон "Model", после чего вызываем метод "addObserver:forKeyPath:options:context". Обозревателем является "self", класс "MainViewController". «keyPath» — свойство, за которым будет вестись наблюдение, текстовая строка символов. Эта опции позволяют нам указать, какие именно данные будут меняться. Здесь это новое значение для свойства. При желании можно запросить начальное, устаревшее или предыдущее значение, а также любую их комбинацию. В качестве контекста можно добавить ноль.

Перемена, за которой следит обозреватель, должна внедрять особый метод, который будет вызываться лишь в данном случае. Во т его сигнатура:

— (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context

"keyPath" — имя свойства как строки, "object" — объект, являющийся владельцем данного свойства (в данном случае, модели), "change" — словарь, хранящий указанные старое, новое, предыдущее и начальное значения, "context" — любой соответствующий случаю контекст (у нас — ноль).

Если стоит цель наблюдать сразу за несколькими свойствами модели, потребуется оператор выбора с "keyPath", сообщающий о том, какое именно свойство поменялось. Пока мы просто просматриваем текст, поэтому и так это знаем. Можно получить новое значение свойства, отправив запрос на изменение параметров словаря. Еще один вариант — напрямую добраться к свойству текста модели, но мы воспользуемся приведенными ниже данными:

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
label.text = [change valueForKey:@"new"];
}


* This source code was highlighted with Source Code Highlighter.


Здесь мы спрашиваем у объекта изменения его «новое» значение (которым будет все его содержимое, поскольку именно такой вариант нами задан). Присваиваем результат "label.text" — и все готово.

Ну вот и все. У нас есть модель, хранящая данные и сообщающая о событиях при их изменении. Обратите внимание: в парадигме Cocoa MVC представления обычно не следят непосредственно за моделью. Этим занимаются контроллеры представлений, и они же сообщают представлениям, что делать. Такой подход не имеет статуса закона, но является общепринятым.

Я хотел разобраться еще с одной концепцией — с использованием "NSNotification" вместо наблюдения за ключом/значением. Не уверен, что это действительно достойная альтернатива, и какие у нее могут быть плюсы и минусы.

Рассмотренный выше пример практической ценности не имеет — это всего лишь иллюстрация. В реальном приложении многое можно было бы сделать по-другому, например, не фиксировать каждое событие редактирования, переходя к нему только после закрытия представления "Flipside". Но цель нашего урока была совсем не в этом, а в наглядном внедрении модели (по крайней мере, ОДНИМ из способов).

Спасибо и успехов!

Исходный код к уроку скачать можно здесь.
Ads
AdBlock has stolen the banner, but banners are not teeth — they will be back

More

Comments 3

    +3
    Весьма любопытно, спасибо. У самого опыт работы с фреймворком не слишком большой, заметил несколько моментов для обсуждения, и буду рад комментариям.
    На сколько я помню, при выбранном подходе NSNotification было бы использовать удобнее в случае нескольких получателей уведомления об изменении состояния модели.
    Общеприянтой практикой, как я понял, является объявление подобных Model сущностей в Application Delegate, с дальнейшим осуществлением доступа через sharedApplication. Т.е. одного синглтона (предоставляемого приложением) казалось бы достаточно, и вводить новые — избыточно.
    Такое ощущение, что доставка подобного рода уведомлений обычно осуществляется несколько по-другому: к сеттеру совйства прикручивается вызов метода, описываемого в протоколе соответствующего делегата. Все заинтересованные в уведомлениях сущности (обычно контроллеры) после получения доступа к разделяемой сущности (свойства sharedApplication) добавляют себя в список делегатов (тоже ничего писать не нужно, какой-то стандартный метод типа addDelegate есть). Реализуя необходимое подмножество протокольных методов получаем необходимые нотификации. В качестве бонуса: общая логика связанная с уведомлениями может быть релизована на стороне источника, а в методы делегата передается уже результат. Такой механизм представляется более очевидным, чем KVO или использование NSNotification.
      +2
      Есть несколько замечаний:
      1. В Вашем случае @property (nonatomic, retain) NSString *text; не подходит. Обязательно надо использовать (nonatomic, copy). Если этого не сделать, то с легкостью можно случайно установить в модель NSMutableString и изменять ее. Ни модель ни кто-либо другой не смогут отследить такие изменения.

      2. SYNTHESIZE_SINGLETON_FOR_CLASS(Model); — пердложил бы заменить на код синглтона предложенный в документации от Apple.

      3.Инициализация переменных в init не самый лучший способ, лучше использовать lazy способ инициализации, он поможет экономно использовать ресурсы устройства. Плюс в примере инициализация переменной text бессмысленна.

      4. Не вижу никакой выгоды от Вашей реализации MVC. MVC подразумевает не только наличие Модели, Контролера и Отображения но и связь между ними, которая позволила бы повторно использовать уже имеющийся код. В примере MainViewController всего лишь показывает строку и позволяет ее изменить и даже это мелочное поведение не можно использовать повторно так как он намертво привязан к синглтону модели. То есть если у Вас завтра в модели добавится еще одно строковое поле то Вы будете для него делать отдельный контроллер для показа второй строки по такому принципу? По моему мнению MVC ни на каплю не раскрыт.
        0
        По поводу lazy так это тоже не самый лучший способ и применять его, ИМХО, лучше когда очень прижмет. Иначе потом отследить когда объект инициализирован, а когда нет, будет сложно по коду.

      Only users with full accounts can post comments. Log in, please.