ПРОГРАММИРОВАНИЕ ПОД IPHONE, IPAD OBJECTIVE-C часть 3

14. XML & JSON

Создаем свою читалку RSS

Перед изучением этого примера советую ознакомиться со следующими примерами:

 

 

В этом примере мы создадим читалку RSS-ленты для этого сайта.

 

 

В первую очередь создадим новый проект. Поскольку отображать ленту новостей мы будем в таблице — создадим проект с шаблона, который уже содержит таблицу (Navigation-Based Application). Я назвал этот проект ReadRSS.

 

Перед тем как парсить данные нам нужно их получить (закачать с интернета). Как это делать я описывал в примере Закачка данных. Давайте реализуем процесс закачки. Создадим переменную rssData типа NSMutableData и пропишем для нее свойства. В реализации метода синтезируйте методы доступа и реализуйте очистку памяти для rssData. Теперь добавьте методы для закачки данных. Если все сделано правильно — интерфейс класса RootViewController должен выглядеть так:

 

<code data-result="[object Object]">#import &lt;UIKit/UIKit.h&gt;

@interface RootViewController : UITableViewController {
    NSMutableData *rssData;
}

@property (nonatomic, retain) NSMutableData *rssData;

@end</code>

 

А реализации должны присутствовать следующие методы:

 

<code data-result="[object Object]">- (void)viewDidLoad
{
    [super viewDidLoad];

    self.title = @"imaladec.com";

    [[UIApplication sharedApplication] setNetworkActivityIndicatorVisible:YES];
    NSURL *url = [NSURL URLWithString:@"http://imaladec.com/rss.php"];
    NSURLRequest *theRequest=[NSURLRequest requestWithURL:url
                                              cachePolicy:NSURLRequestUseProtocolCachePolicy
                                          timeoutInterval:60.0];

    NSURLConnection *theConnection=[[NSURLConnection alloc] initWithRequest:theRequest delegate:self];
    if (theConnection) {
        self.rssData = [NSMutableData data];
    } else {
        NSLog(@"Connection failed");
    }

    [theConnection release];  
}

- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data {
    [rssData appendData:data];
}

- (void)connectionDidFinishLoading:(NSURLConnection *)connection {
    NSString *result = [[NSString alloc] initWithData:rssData encoding:NSUTF8StringEncoding];
    NSLog(@"%@",result);
    [result release];
}

- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error {
    NSLog(@"%@", error);
}</code>

 

Как видите, код не сильно отличается от кода с примера Закачка данных. Сама закачка начинается после загрузки интерфейса. Когда все данные получены, в методе connectionDidFinishLoading мы преобразуем полученные данные в строку, выводим эту строку в консоль и очищаем память, которую занимала эта строка. Таким образом — мы получили в консоли структуру нашего файла (это и есть нужный нам XML-файл).

Теперь полученные данные следует распарсить, для этого дополним интерфейс нашего класса новыми переменными:

 

<code data-result="[object Object]">#import &lt;UIKit/UIKit.h&gt;

@interface RootViewController : UITableViewController &lt;NSXMLParserDelegate&gt; {
    NSMutableData *rssData;
    NSMutableArray *news;
    NSString * currentElement;
    NSMutableString *currentTitle;
    NSMutableString *pubDate;
}

@property (nonatomic, retain) NSMutableData *rssData;
@property (nonatomic, retain) NSMutableArray *news;
@property (nonatomic, retain) NSString * currentElement;
@property (nonatomic, retain) NSMutableString *currentTitle;
@property (nonatomic, retain) NSMutableString *pubDate;

@end</code>

 

В первую очередь хочу обратить ваше внимание на подключенный протокол NSXMLParserDelegate, без него методы парсера работать не будут. Затем я добавил изменяемый массив news. Заголовки новостей, которые мы будем получать хранить будем в этом массиве, а потом выводить данные с этого массива в таблицу. В переменной currentElement будем хранить текущий элемент, который парсим. А в переменных currentTitle и pubDate — заголовок и дату новости, которые парим в данный момент. Важным моментом последних двух переменных является их тип, это должны быть именно изменяемые строки, почему так вы сами поймете позже. И в конце для всех переменных следует добавить свойства.

 

Теперь перейдем к реализации класса RootViewController. В первую очередь синтезируйте методы доступа и организуйте очистку памяти для только что объявленых переменных. Изменим метод connectionDidFinishLoading (который вызывается в момент окончания получения данных).

 

<code data-result="[object Object]">- (void)connectionDidFinishLoading:(NSURLConnection *)connection {
    NSString *result = [[NSString alloc] initWithData:rssData encoding:NSUTF8StringEncoding];
    NSLog(@"%@",result);
    [result release];    

    self.news = [NSMutableArray array];
    NSXMLParser *rssParser = [[NSXMLParser alloc] initWithData:rssData];
    rssParser.delegate = self;
    [rssParser parse];
    [rssParser release];
}</code>

 

Теперь кроме вывода полученных данных в консоль мы создаем массив новостей (если данные по какой-то причине не получены — массив нет смыла создавать). После чего на основании полученных данных, создаем экземпляр класса NSXMLParser (rssParser) и даем ему команду парсить данные, которые мы передали в момент его инициализации. Завершается метод очисткой памяти, которую занимала переменная rssParser.

Как только парсер получил команду парсить — вызывается метод didStartElement:

 

<code data-result="[object Object]">- (void)parser:(NSXMLParser *)parser 
didStartElement:(NSString *)elementName 
  namespaceURI:(NSString *)namespaceURI 
 qualifiedName:(NSString *)qualifiedName 
    attributes:(NSDictionary *)attributeDict  {

    self.currentElement = elementName;
    if ([elementName isEqualToString:@"item"]) {
        self.currentTitle = [NSMutableString string];
        self.pubDate = [NSMutableString string];
    }
}</code>

 

Данный метод начинает анализировать каждый элемент. В нем мы нашей переменной currentElement присваиваем имя элемента (оно нам пригодится в следующих методах). Затем мы ищем элемент с именем item, если находим такой — инициализируем  переменные currentTitle и pubDate. Дело в том, что если мы посмотрим в структуру rss-файла то увидим, что элементы с именем titleи pubDate являются вложенными в элемент item. То есть, item является индикатором того, что скоро будут следовать его внутренние элементы, а поскольку значения этих внутренних элементов нас как раз и интересуют, то для них мы и инициализируем переменные. На скриншоте ниже вы сами можете проследить структуру подчиненности.

 

 

Теперь перейдем к методу foundCharacters. Как и в случае с NSURLConnection, этот метод не возвращает сразу всю строку, которую хранит элемент, поэтому мы складываем полную строку с кусочками, которые возвращает нам этот метод.

 

<code data-result="[object Object]">- (void)parser:(NSXMLParser *)parser foundCharacters:(NSString *)string {
    if ([currentElement isEqualToString:@"title"]) {
		[currentTitle appendString:string];
	} else if ([currentElement isEqualToString:@"pubDate"]) {
		[pubDate appendString:string];
    }
}</code>

 

Он вызывается в момент получения данных, которые находятся в каждом элементе. Чтобы не читать данные всех элементов — я добавил условие, то есть имена тех элементов, значения которых я хочу читать. Как вы помните в методе didStartElementпеременной currentElement мы присваивали имя каждого элемента, который читаем, вот теперь по этому имени мы и делаем отбор.

В методе didEndElement мы тоже делаем отбор по конкретному элементу (item). Этот метод вызывается когда на пути парсера встречается имя элемента с закрывающимся тегом. Это значит, что все данные с этого элемента прочитаны.

 

<code data-result="[object Object]">- (void)parser:(NSXMLParser *)parser 
 didEndElement:(NSString *)elementName 
  namespaceURI:(NSString *)namespaceURI 
 qualifiedName:(NSString *)qName {
    if ([elementName isEqualToString:@"item"]) {
        NSDictionary *newsItem = [NSDictionary dictionaryWithObjectsAndKeys:
                                  currentTitle, @"title", 
                                  pubDate, @"pubDate", nil];
        [news addObject:newsItem];
        self.currentTitle = nil;
        self.pubDate = nil;
        self.currentElement = nil;
    }
}</code>

 

Когда все данные получены — мы сохраняем их в словарь (каждый под своим ключом), а словарь добавляем в массив, который будет выведен в таблицу. После добавления данных значения переменных currentTitlepubDate и currentElement следует очистить, это нужно для очистки памяти и для того, чтобы не смешивались данные.

Когда парсер прошелся по всему документу — вызывается метод parserDidEndDocument.

 

<code data-result="[object Object]">- (void)parserDidEndDocument:(NSXMLParser *)parser {
    [[UIApplication sharedApplication] setNetworkActivityIndicatorVisible:NO];
    [self.tableView reloadData];
}</code>

 

В нем мы убираем индикатор активности и перезагружаем данные таблицы.

В случае, если парсер не сможет распарсить данные будет вызван метод parseErrorOccurred. В этом методе я вывожу ошибку в консоль.

 

<code data-result="[object Object]">- (void)parser:(NSXMLParser *)parser parseErrorOccurred:(NSError *)parseError {
    NSLog(@"%@", parseError);
}</code>

 

Теперь организуем правильную работы нашей таблицы. В методе numberOfRowsInSection указываем количество элементов массива.

 

<code data-result="[object Object]">- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
    return [news count];
}</code>

 

В методе  укажем количество секций таблицы.

 

<code data-result="[object Object]">- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
{
    return 1;
}</code>

 

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

 

<code data-result="[object Object]">- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    static NSString *CellIdentifier = @"Cell";

    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
    if (cell == nil) {
        cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleSubtitle 
                                       reuseIdentifier:CellIdentifier] autorelease];
    }

    NSDictionary *newsItem = [news objectAtIndex:indexPath.row];

    cell.textLabel.text = [newsItem objectForKey:@"title"];
    cell.detailTextLabel.text = [newsItem objectForKey:@"pubDate"];

    return cell;
}</code>

 

Наша читалка rss-лент готова, можете смело запускать ее.

 

В конце хочу еще раз уточнить принцип работы парсера. Парсер читает весь XML файл. Чтение происходит сверху вниз строго по порядку. То есть, если в процессе парсинга ему встречается клю, которые имеет вложенные ключи — парсер не перейдет к следующему ключу, пока не прочитает всю структуру вложенности данного ключа. Это похоже на то, как если бы вы искали какой-то файл на вашем компьютере и просматривали бы каждую папку. Вы бы зашли в папку документы, а в ней еще три папки, открыли бы первую папку, и пока не просмотрели бы все ее содержимое — не перешли бы ко второй папке. Именно это свойство я использовал в реализации этого примера. Если в методе didEndElement парсер дошел до закрывающегося ключа item — значит все вложенные ключи и их значения он прочитал полностью.

 

Исходный код можно скачать здесь.

Comments are closed.