Pull to refresh

Three20 demystifying: TTModel

Reading time10 min
Views2.2K
Те, кому по причине необходимости пришлось столкнуться с Three20, в процессе разработки iOS приложений, знают, что данная библиотека более-менее решать некоторый круг задач, связанный с быстрым прототипированием, использованием глобальной навигации, MVC подходом для разработки приложений.

Основным минусом библиотеки является то, что в ней отсутствует нормальная документация, а та, что есть недостаточна для нормального понимания того, что происходит внутри этой библиотеки.

Это не единственный минус библиотеки, и в интернете можно найти много обсуждений относительно за и против Three20.

Необходимо сказать, что главный разработчик остановил разработку над Three20, и решил создать новую библиотеку Nimbus, основной идеей которой будет простота и высокая документированность:
«Nimbus is an iOS framework whose feature set grows only as fast as its documentation.»

Тем не менее, проект существует и по сей день, и некоторые части этого проекта заслуживают внимания, и достаточны интересны с точки зрения проектирования.

В данном посте будут подробно рассмотрены вопросы, связанные с работой TTModel.

Для тех, кто не собирается использовать Three20 у себя в проектах, будет полезно подсмотреть на одну из возможных, заслуживающий внимания, реализаций модели в шаблоне MVC.

Всех, кому еще интересно, прошу .

TTModel



Те, кто все еще не знаком с шаблоном проектирования MVC(Model-View-Controller), либо забыли о чем это все, могут ознакомиться с ним на Wiki. («Повторение — мать учения» как никак)

Архитектура



Итак, TTModel в библиотеке Three20 представляет собой классическую модель из MVC. То есть это просто абстрактный набор данных, который не зависит от представления либо контроллера.

Архитектура TTModel изначально подразумевает, что модель может находится в разных состояниях:
  • -(BOOL)isLoaded — Основной флаг, который указывает на то, что модель загружена, и содержит актуальные данные
  • -(BOOL)isLoading
  • -(BOOL)isLoadingMore — состояние модели, когда основные данные загружены, но был осуществлен запрос за дополнительными данными
  • -(BOOL)isOutdated — флаг, указывающий на то, что данные в модели устарели, и необходима перезагрузка данных для восстановления модели в актуальное состояние

Кроме этого TTModel предоставляет несколько базовых методов для работы с ней.
  • -(void)load:(TTURLRequestCachePolicy)cachePolicy more:(BOOL)more — Метод, который вызывает загрузку актуальных данных в модель
  • -(void)cancel — Отменяет загрузку данных в модель
  • -(void)invalidate:(BOOL)erase — метод, который переводит модель в состояние «неактуальности». Параметр erase указывает на необходимость удаления данных из кэша


Данное архитектурное решение вполне оправдано, и позволяет решить задачу с сокрытием реализации модели. По сути, нам не важно, с помощью чего осуществляется загрузка модели — нужен ли ей запрос в базу данных для получения актуальных данных, запрос на сервер, вызов 10 последовательных запросов, или осуществление какой-то сложной вычислительной задачи. Для нас важно знать ее состояние. И как только состояние сменилось на isLoaded, мы можем забирать из этой модели актуальные данные.

   // Создаем запрос в базу данных
   DBRequestModel * someModel = 
   [[DBRequestModel alloc] initWithQuery:@"SELECT * FROM TABLE USERS"];
   
   // Выполняем запрос асинхронно
   [someModel load:TTURLRequestCachePolicyNone more:NO];
   
   // Ждем его завершения
   while ([someModel isLoading]) {
      [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
   }
   
   if ([someModel isLoaded]) {
      // Доступаемся к полю result DBRequestModel
      // Получаем данные
      NSArray * users = [someModel result];
   } else {
      // Ошибка загрузки?
   }

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

Код приведенный выше, мог бы существовать в какой-нибудь вселенной, но я бы не советовал его использовать в нашей.

Слежение за изменением состояния модели


Зачем нам следить за изменением состояния модели? Для того, чтобы знать, когда в ней будут актуальные данные.


Для того, чтобы следить за состоянием модели, необходимо добавить объект(обычно контроллер), в массив delegates модели.

[[someModel delegates] addObject:self];


В свою очередь, модель, при изменении своего состояния будет сообщать об этом всем своим делегатам посредством протокола TTModelDelegate

Протокол TTModelDelegate предоставляет набор методов, из которых необходимо выделить несколько базовых:
  • (void)modelDidStartLoad:(id<TTModel>)model — Оповещает о том, что модель начала загрузку
  • (void)modelDidFinishLoad:(id<TTModel>)model — Оповещает о том, что модель успешно загрузила данные
  • (void)model:(id<TTModel>)model didFailLoadWithError:(NSError*)error — Вызывается при ошибке загрузки модели. Причина ошибки указана в переменной error
  • (void)modelDidCancelLoad:(id<TTModel>)model — Вызывается, в случае если загрузка модели была отменена путем вызова метода cancel


Итого, подписавшись на изменения модели, мы реализуем методы, которые нам необходимы. Все вышеперечисленные методы являются опциональными, и позволяют сконцентрироваться исключительно на той функциональности, которая нам нужна.

- (void)showModelDataIfPossible {
   // Создаем запрос в базу данных
   self.model = [[DBRequestModel alloc] initWithQuery:@"SELECT * FROM TABLE USERS"];
   
   // Подписываемся на сообщения об изменениях состояния модели
   [[[self model] delegates] addObject:self];
   
   // Проверяем, была ли модель загружена
   // И находятся ли в ней актуальные данные
   if (! [[self model] isLoaded]  && [[self model] isOutdated]) {
      // Выполняем запрос асинхронно
      [someModel load:TTURLRequestCachePolicyNone more:NO];
   } else {
      // В этом случае, у нас уже загруженная модель в который есть актуальные данные
      // Просто показываем ее содержимое
      [self showModelData:[self model]];
   }
}

#pragma mark - TTModelDelegate

- (void)modelDidStartLoad:(id<TTModel>)model {
   // Модель начала загрузку..
   // Здесь можно показать пользователю экран загрузки
   [self showLoading];
}

- (void)modelDidFinishLoad:(id<TTModel>)model {
   // Не забываем убрать экран загрузки
   [self hideLoading];
   
   // Модель закончила загружаться
   // В ней теперь актуальные данные
   [self showModelData:[self model]];
   
}

- (void)model:(id<TTModel>)model didFailLoadWithError:(NSError*)error {
   // Не забываем убрать экран загрузки
   [self hideLoading];
   
   // Самый простой и совсем не User-Friendly вариант
   TTAlertNoTitle(@"Error code #10");
   
   // Немного более User-Friendly вариант
   [self showReloadScreen];
   self.reloadLabel.title = @"Something went wrong"
   "But you could try to reload data";
}

- (void)modelDidCancelLoad:(id<TTModel>)model {
   // Не забываем убрать экран загрузки
   [self hideLoading];
   
   // Чаще всего этот метод обусловлен тем, 
   // что нам не нужна больше модель
   // и вообще, контроллер сейчас будет скрыт с экрана
   // По разному бывает

}

- (void)showModelData:(id<TTModel>)model {
  // Доступаемся к полю result DBRequestModel
   // Получаем данные
   NSArray * users = [someModel result];
   
   // Показываем пользователю результат
   [self showUsers:users];
}



Надеюсь, стало более-менее понятно, что представляет собой TTModel и как с ней работать. Теперь поговорим немного о delegates у модели.

delegates vs listeners


Массив delegates в TTModel представляет собой особый массив, который не retain'ит объекты при их добавлении. Создается он при помощи функции TTCreateNonRetainingArray.
Сделано это для избежания циклических ссылок, и для соответствия конвенции о том, что delegate не должен быть retaine'd.


На самом деле имя для свойства выбрано не совсем правильно. Делегат по своей функциональности должен помогать основному объекту(в данном случае модели) решать задачу, а в текущей реализации, массив делегатов на самом деле является массивом слушателей(listeners).

Основное отличие слушателя(listener) и делегата(delegate) в том, что слушателю сообщают об изменениях в базовом объекте, а у делегата, чаще всего, запрашивают некоторые данные, которые необходимы для функционирования базового объекта.

Так что, на самом деле, в TTModel массив делегатов исходя из функциональности является массивом слушателей.

TTURLRequestModel


Для работы с моделями, которые забирают данные из сети, в Three20 используется класс TTURLRequestModel. Он завязан на TTURLRequest и предоставляет собой наглядный пример того, что можно достаточно просто скрыть конкретную реализацию модели.

TTURLRequestModel является по сути абстрактным, классом, и не умеет особо ничего, разве что добавляет в модель несколько дополнительных полей, связанных с сетевой составляющей.

Что еще интересно в этой модели, из-за отсутствия нормальной документации, не сразу понятно, как она работает, и как правильно ее использовать :)

Создадим свою модель?



Предположим, модель должна предоставлять нам список некоторых сущностей, назовем их для простоты пользователями.

Сервер предоставляет данные постранично. На каждой странице N штук пользователей.

В путь!

Объявляем интерфейс

@interface ItemsListModel : TTURLRequestModel {
   /* 
    Массив для хранения списка сущностей
    */
   NSMutableArray * _items;
   /*
    Базовый URL модели 
    */
   NSString * _baseURLString;
   /*
    Текущая страница
    */
   int _page;
   /*
    количество объектов на странице 
    */
   int _itemsOnPage;
   
}

@property(nonatomic, readonly) NSMutableArray * users;
@property(nonatomic, readonly) NSString * baseURLString;
@property(nonatomic, assign) int page;
@property(nonatomic, readonly) int itemsOnPage;

/* 
Создание модели c базовым URL'ом и количеством элементов на странице
 */
- (id)initWithBaseURLString:(NSString *)baseURLString itemsOnPage:(int)itemsOnPage;
/*
 Метод, который должен возвратить TTURLRequest по текущим параметрам модели
 Может быть переопределен в потомках
 */
- (TTURLRequest *)requestForDataWithCachePolicy:(TTURLRequestCachePolicy)cachePolicy more:(BOOL) more;
/* 
 Метод, который из NSData * data создает список элементов списка
 Может быть переопределен в потомках
*/
- (NSArray *)parseDataToItems:(NSData *)data error:(NSError **)error;

@end



И, собственно, реализуем нашу модель

@implementation ItemsListModel

@synthesize items = _items;
@synthesize itemsOnPage = _itemsOnPage;
@synthesize baseURLString = _baseURLString;
@synthesize page = _page;

/*
 Базовый метод инициализации. Ничего особо сложного.
 Инициируем текущую страницу со значением 0
 */
- (id)initWithBaseURLString:(NSString *)baseURLString itemsOnPage:(int)itemsOnPage {
   self = [super init];
   if (self) {
      _baseURLString = [baseURLString copy];
      _itemsOnPage = itemsOnPage;
      _page = 0;
      _items = [[NSMutableArray array] retain];
      _itemsOnPage = 20;
   }
   return self;
}

/*
 Метод загрузки модели
 */
- (void)load:(TTURLRequestCachePolicy)cachePolicy more:(BOOL)more {
   // Стартуем новое, только в том случае, если 
   // Модель еще не загружена
   if ( ! [self isLoading]) {
      
      // Сохраняем флаг дозагрузки
      _isLoadingMore = more;
      
      // Если мы не догружаем данные
      // сбрасываем предыдущий запрос и время его загрузки
      if ( ! more) {
        [self reset];
      }

      // Создаем запрос для получения новой порции данных
      TT_RELEASE_SAFELY(_loadingRequest);
      _loadingRequest = [[self requestForDataWithCachePolicy:cachePolicy more:more] retain];
      
      // Стартуем асинхронный запрос
      [_loadingRequest send];
   }
}

/*
 Метод, который по данным модели создаст правильный запрос
 */
- (TTURLRequest *)requestForDataWithCachePolicy:(TTURLRequestCachePolicy)cachePolicy more:(BOOL) more {
   // Получаем полную строку запроса
   // Добавляем к базовой строке параметр page
   // hhtp://baseURL/sdfsd?page=0
   //
   // Здесь на самом деле может быть достаточно много параметров
   // Все зависит от модели и ее состояния
   NSString * fullRequestURL = self.baseURLString;
   fullRequestURL = [fullRequestURL stringByAddingQueryDictionary:
                     [NSDictionary dictionaryWithObject:
                      [NSString stringWithFormat:@"%d", _page]
                                                 forKey:@"page"
                      ]
                     ];
 
   // Создаем базовый запрос
   TTURLRequest * req = [TTURLRequest requestWithURL:fullRequestURL delegate:self];
   req.cachePolicy = cachePolicy;
   req.response = [[TTURLDataResponse new] autorelease];
   
   return req;
}

/*
 Преобразуем данные, которые были получены из сети в данные приложения
 */
- (NSArray *)parseDataToItems:(NSData *)data error:(NSError **)error {
   NSString * dataString = [[[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding] autorelease];
   
   // Здесь должна быть подключена библиотека SBJSON
   NSArray * tweets = [dataString JSONValue];
   return tweets;
}

- (void)requestDidFinishLoad:(TTURLRequest *)request {
   TTURLDataResponse * response = nil;

   // Убеждаемся, что ответ от сервера нужного на типа
   if ([[request response] isKindOfClass:[TTURLDataResponse class]]) {
      response = (TTURLDataResponse *)request.response;
   }
   
   if ( ! response) {
      // Нет ответа - валимся с ошибкой
      [self request:request didFailLoadWithError:nil/* Создайте здесь нормальную ошибку*/];
      _isLoadingMore = NO;
      return;
   }
   
   NSError * error = nil;
   
   // Парсим данные, полученные с сервера
   NSArray * loadedItems = [self parseDataToItems:[response data] error:&error];
   
   // проверяем, не было ли ошибки при парсинге
   if (error) {
      // Была ошибка? Валимся с ошибкой
      [self request:request didFailLoadWithError:error];
      return;
   }
   
   // если мы догружаем данные, то 
   // Добавляем их в существующий массив
   if ([self isLoadingMore]) {
      [_items addObjectsFromArray:loadedItems];
   } else {
      TT_RELEASE_SAFELY(_items);
      _items = [[NSMutableArray arrayWithArray:loadedItems] retain];
   }
   
   // Увеличиваем счетчик страниц
   _page++;
  
   if ([loadedItems count] < _itemsOnPage) {
      /*
       Дополнительно, в зависимости от количества полученных данных
       Можно судить о том, что данных больше нету
       */
      
      // _hasMoreItems = NO
   }

   _isLoadingMore = NO;
   
   [super requestDidFinishLoad:request];
}

/*
 Сбрасывание модели в начальное состояние
 */
- (void)reset {
   _page = 0;
   TT_RELEASE_SAFELY(_items);
   [super reset];
}

/*
 Не забываем про освобождение ресурсов
 */
- (void)dealloc {
   TT_RELEASE_SAFELY(_baseURLString);
   TT_RELEASE_SAFELY(_items);
   [super dealloc];
}

@end


Ну, и, наконец, пример использования нашей модели в реальных условиях

@implementation MyModelViewController 

#pragma mark - View LifeCycle

- (void)viewWillAppear:(BOOL)animated {
   [super viewWillAppear:animated];
   [self createAndStartModelIfNeeded];   
}

//...

#pragma mark - Model Showing

- (void)showModelData:(id<TTModel>)model {
   // Доступаемся к полю result ItemsListModel
   // Получаем данные
   NSArray * tweets = [(ItemsListModel *)model items];
      
   // Выводим их "на экран"
   NSLog(@"tweets : %{собачка} ", tweets);

   // У Хабра небольшие проблемы с подсветкой синтаксиса :(
   // Он не понимает строки вида @"tweets : %@"
   
   // И первый твит - в status Label
   if ([tweets count]) {
      NSDictionary * firstTweet = [tweets objectAtIndex:0];
      NSString * tweetText = [firstTweet objectForKey:@"text"];
      _statusLabel.text = [[_statusLabel text] stringByAppendingFormat:@"\n%@", tweetText];
   }
   
}

#pragma mark - Model Creationg

- (void)createAndStartModelIfNeeded {
   // Создаем модель
   if ( ! _model) {
      _model = [[ItemsListModel alloc] 
                initWithBaseURLString:@"http://api.twitter.com/1/statuses/user_timeline.json?user_id=18191307" itemsOnPage:20];
      [[_model delegates] addObject:self];
   }
   
   // И если она еще не загружена - стартуем ее загрузку
   if ( ! [_model isLoaded] ) {
      [_model load:TTURLRequestCachePolicyNone more:NO];
   }
}

       
#pragma mark - TTModel Delegate

- (void)modelDidStartLoad:(id<TTModel>)model {
   _statusLabel.text = @"Загружаем";
}

- (void)modelDidFinishLoad:(id<TTModel>)model {
   _statusLabel.text = @"Ура! Данные успешно загружены";
   [self showModelData:model];
}

- (void)model:(id<TTModel>)model didFailLoadWithError:(NSError*)error {
   _statusLabel.text = [NSString stringWithFormat:@":(( %@", [error localizedDescription]];
}

- (void)modelDidCancelLoad:(id<TTModel>)model {
   _statusLabel.text = @"Отменено пользователем";
}

@end


Кому особо интересно, исходники можно скачать отсюда. Я не решился избавить пользователей Хабра от наслаждения создания проекта с подключением Three20, так что в архиве только файлы с моделью и ViewController'ом.

Резюме



TTModel в библиотеке Three20 представляет собой достаточно хорошую абстракцию которую можно использовать для организации MVC подхода при программировании под iOS. Даже если вы не собираетесь использовать Three20, стоит, все же, посмотреть на реализацию TTModel, и, при желании, сделать свою модель, на основе представленной.
Tags:
Hubs:
Total votes 20: ↑19 and ↓1+18
Comments4

Articles