Как стать автором
Поиск
Написать публикацию
Обновить

Habra RSS reader для iPhone

Без долгих прелюдий начну. Довелось мне писать тестовое задание на мою потенциально новую работу, которое звучало примерно так:



«Сделать iPhone приложение, которое будет выводить данные из rss ленты (habrahabr.ru/rss). Данные должны быть выведены в таблицу, в каждой ячейке содержится одна новость (картинка, название, дата, описание на 2 строки). При клике на ячейку должен открываться внутренний браузер с новостью.»



С виду ничего сложного, на самом же деле… так и есть- ничего сложного по сути. На задание дается примерно 2 часа (сразу скажу- я не очень уложился, не работал до этого с xml и возникли некоторые другие сложности, завалил немного срок и оставил memory leaks).



Так как код не несет никакой коммерческой или другой тайны, был создан лично мной и думаю кому-то будет полезно почитать, как создать простенький rss reader — милости прошу под кат.



Итак


Нам необходимо создать table view приложение, в каждой строке которого выводятся некоторые данные из ленты, в частности нас будут интересовать элементы item из xml, которые имею формат:



<item>
<title>
<![CDATA[
Ненормальное программирование / [Из песочницы] Программирование без использования условных конструкций
]]>
</title>
<guid isPermaLink="true">http://habrahabr.ru/blogs/crazydev/124878/</guid>
<link>http://habrahabr.ru/blogs/crazydev/124878/</link>
<description>
<![CDATA[
Один знакомый заявил мне, что любая программа может быть написана без использования if/else. Я, конечно, тут же возмутился и сформулировал ему (а заодно и себе) простейшую задачу: написать программу, которая будет радоваться, если на вход ей подать, например, слово «печенька», и огорчаться в противном случае; но при этом нельзя использовать никаких конструкций, изменяющих направление программы — то есть она должна быть строго линейной. Решение под катом.<br/>
 <div class="habracut"> <a class="habracut" href="http://habrahabr.ru/blogs/crazydev/124878/#habracut">Читать дальше →</a> </div>
]]>
</description>
<pubDate>Mon, 25 Jul 2011 11:48:58 GMT</pubDate>
<author>Moonrise</author>
<category>ненормальное программирование</category>
<category>оператор ветвления</category>
<category>Java</category>
</item>


Из всего этого разнообразия информации нас интересуют title, link, description, pubDate. Ситуация немного осложняется тем, что изображение к статье хранится в самом теле статьи и может иметь разный набор тегов (alt, align etc), это ничего- справимся, но все по порядку.



XML парсер



Опустим описание создания Navigation-based проекта, это- довольно тривиальная задача. Первое, что нас интересует,- непосредственно парсер xml. В оbjective-c существует для этой цели класс NSXMLParser и необходимо, чтобы класс реализовывал протокол NSXMLParserDelegate. Изменим RootViewController.h до вида:



@interface RootViewController : UITableViewController {

//непосредственно парсер
NSXMLParser * _rssParser;

//текущая статья, из которых состоит массив статей
HabraItem* _currentItem;
//массив всех статей из ленты
NSMutableArray* _habraItems;

//название текущего элемента xml
NSString* _currentProperty;
//значение текущего элемента
NSMutableString* _currentValue;
}

//метод для получения данных из rss
-(void) getDataFromRSS;

@end

Класс HabraItem будет в себе содержать данные о каждой статье, в частности: название, описание, ссылку на статью на хабр, изображение, дату публикации. Дальше нам необходимо получить xml и реализовать методы из протокола NSXMLParserDelegate для парсинга xml.



Получаем xml вызовом из viewDidLoad:



-(void) getDataFromRSS {
_rssParser = [[NSXMLParser alloc] initWithContentsOfURL:[NSURL URLWithString:@"http:/habrahabr.ru/rss"]];
[_rssParser setDelegate:self];
[_rssParser parse];

}

Метод крайне прост- создаем экземпляр класса NSXMLParser, указываем откуда взять данные, указываем, что делегатом для данного парсера будет текущий класс и непосредственно парсим xml.



Дальше нас будут интересовать три метода из NSXMLParserDelegate, без которых не выйдет мапить данные:



//нашли начало нового элемента
- (void)parser:(NSXMLParser *)parser didStartElement:(NSString *)elementName namespaceURI:(NSString *)namespaceURI qualifiedName:(NSString *)qName attributes:(NSDictionary *)attributeDict {

//если это новая статья - создадим экземпляр HabraItem для сохранения в нем данных
if ( [elementName isEqualToString:@"item"] ) {
_currentItem = [[HabraItem alloc] init];
return;

}

//если это title, link, description или pubDate - будем дальше работать с этими элементами, остальные элементы нас в данном случае не интересуют, сетим в _currentProperty название элемента
else if ([elementName isEqualToString:@"title"] || [elementName isEqualToString:@"link"] || [elementName isEqualToString:@"description"] || [elementName isEqualToString:@"pubDate"]) {
_currentProperty = elementName;
return;
}

}

//непосредственно содержание элементов- добавляем в _currentValue, в этот метод может передаваться не весь элемент а только часть, по этому создаем NSMutableString и каждый раз добавляем туда строки
- (void)parser:(NSXMLParser *)parser foundCharacters:(NSString *)string {
if (!_currentValue) {
_currentValue = [[NSMutableString alloc] initWithCapacity:50];
}
[_currentValue appendString:string];

//так как в xml много служебной информации(табуляции и новые строки), которая нам не нужна - избавляемся от нее
NSString *trimmedString = [_currentValue stringByTrimmingCharactersInSet:[NSCharacterSet characterSetWithCharactersInString:@"\t\n"]];
[_currentValue setString:trimmedString];
}

//дошли до конца элемента
-(void)parser:(NSXMLParser *)parser didEndElement:(NSString *)elementName namespaceURI:(NSString *)namespaceURI qualifiedName:(NSString *)qName{

//если это была статья - добавляем ее в массив всех статей и освобождаем память
if ([elementName isEqualToString:@"item"]) {
[_habraItems addObject:_currentItem];
[_currentItem release];
return;
}
//если это был элемент, который мы до этого сетили в _currentProperty, вызываем метод из HabraItem, который будет заниматься тем, что сетит свой проперти в зависимости от ключа. После этого освобождаем память от _currentValue
else if ([elementName isEqualToString:_currentProperty]){
[_currentItem setValue:_currentValue forProperty:_currentProperty];
}

[_currentValue release];
_currentValue = nil;

}

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



Маппинг статьей



Дальше рассмотрим класс, в который будут мапиться статьи- HabraItem. Хедер будет выглядеть так:



@interface HabraItem : NSObject {
NSString* _title;
NSURL* _link;
NSString* _description;
UIImage* _img;
NSDate* _pubDate;
}

@property (nonatomic, retain) NSString* title;
@property (nonatomic, retain) NSURL* link;
@property (nonatomic, retain) NSString* description;
@property (nonatomic, retain) UIImage* img;
@property (nonatomic, retain) NSDate* pubDate;

//метод который мапит все проперти класса
-(void) setValue:(NSString*)value forProperty:(NSString*)property;

@end


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



-(void) setValue:(NSString*)value forProperty:(NSString*)property {

//если это ссылка- создаем экземпляр класса NSURL по строке
//тут и далее элементам, на которые я сам не выделяю память, необходимо увеличивать счетчик ссылок иначе они поместятся в autorelease pool и будут удалены на следующем цикле сообщений, что в свою очередь будет крэшить приложение
if ([property isEqualToString:@"link"]) {
_link = [[NSURL URLWithString:value] retain];
return;

} else if ([property isEqualToString:@"pubDate"]) {
// если это дата- создаем экземпляр даты по формату указанному в NSDateFormatter, это не обязательно, в принципе, для текущей задачи можно было и строку хранить
NSDateFormatter *dateFormat = [[NSDateFormatter alloc] init];
[dateFormat setDateFormat:@"EE, d LLLL yyyy HH:mm:ss Z"];
_pubDate = [dateFormat dateFromString:value];
[dateFormat release];
[_pubDate retain];
return;

} else if ([property isEqualToString:@"description"]) {

//далее самое, наверное, сложное в данном классе. Так как изображение для статьи храниться просто в html- нужно его парсить отдельно. Способ, которым делал это я- наверняка не очень правильный, но время уже поджимало, не ругайте сильно и, скорее всего, не стоит так делать в продакшн версиях продукта

NSRange range;
//первая регулярка вытаскивает все, что идет в кавычках после src и заканчивается на .**g (png\jpg), не оставил просто src, потому что в теле статьи иногда встречаются видео
range = [value rangeOfString:@"src=\".*\\..{2}g\"" options:NSRegularExpressionSearch];

if (range.location != NSNotFound){
NSString* tempStr = [value substringWithRange:range];

NSRange newRange;
//механизм для регулярных выражений, который использую я, вытягивает совпадения вместе с регулякой (не нашел как сделать аналог круглых скобок в POSIX), по этому сделал вторую регулярку чтобы вытащить именно урл
newRange = [tempStr rangeOfString:@"h.*g" options:NSRegularExpressionSearch];

if (newRange.location != NSNotFound) {

NSString* urlStr = [tempStr substringWithRange:newRange];

//создаем из стринга урл и получаем избражение по урлу
NSURL* url = [NSURL URLWithString:urlStr];
NSData* imgData = [NSData dataWithContentsOfURL:url];
UIImage* tempImg = [[UIImage alloc] initWithData:imgData];

//resize изображения
CGRect rect = CGRectMake(0.0, 0.0, 40, 40);
UIGraphicsBeginImageContext(rect.size);
[tempImg drawInRect:rect];
_img = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();

[tempImg release];

[_img retain];
}
}
}

//если это title или description мапим просто в проперти
[self setValue:value forKey:property];

}


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



Исходный код проекта



Полезности для данного проекта:


Теги:
Хабы:
Данная статья не подлежит комментированию, поскольку её автор ещё не является полноправным участником сообщества. Вы сможете связаться с автором только после того, как он получит приглашение от кого-либо из участников сообщества. До этого момента его username будет скрыт псевдонимом.