不格好エンジニア (引っ越しました)

wordpress.comから引っ越しました。

【iOS】複数のtableviewとCoreDataを用いて、APIから取得した記事をカテゴリごとに分類する【SmartNews風】

現在、多くのモバイルアプリがAPIから取得したデータをユーザに表示しています。ここでは、最近のニュースアプリで多用される、分類された記事を複数のtableviewに表示する実装をご紹介します。

サンプル

f:id:tjnet555:20141130150023p:plain

GitHub上でニュースアプリのサンプルを開発しています。どこかで見たことのあるUIでは。。。と思うかもしれませんが、気のせいです。

はじめに

まずはCocoaPodsを用いて、AFNetworkingとMagicalRecordを導入します。AFNetworkingはHTTPリクエストを行う為のライブラリであり、MagicalRecordはCoreDataを簡単に扱うためのライブラリです。

Podfileは、このような形で記述しています。

platform :ios, '7.0'
pod 'JASidePanels'
pod 'RPSlidingMenu'
pod 'AFNetworking'
pod 'MagicalRecord'

CoreData

次に、必要なCoreDataモデルを作成します。

作成方法は、NewFile -> Core Data -> Data Modelと選択して、好きな名前を潰えます。
ここではArticleという名前をつけて、作成しています。

f:id:tjnet555:20141130151947p:plain

作成したEntityとやり取りするために、
New File -> Core Data -> NSManagedObject Subclassと選択して、NSManagedObjectのサブクラスを作成します。

必要なものをimport

#import "MainViewController.h"
#import "PagingScrollView.h"
#import "CoreData+MagicalRecord.h"
#import "AFNetworking.h"
#import "UIImageView+AFNetworking.h"
#import "Article.h"

MainViewController.hで必要に応じてimportしています。

AppDelegate

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    // Override point for customization after application launch.
    
    //CoreData setup
    [MagicalRecord setupAutoMigratingCoreDataStack];
    
    return YES;
}

AppDelegateではCoreDataのセットアップを行っています。

APIからデータ取得

@implementation MainViewController{
    PagingScrollView *scrollView;
    
    //To interact with the API, create an instance variable
    AFHTTPRequestOperationManager *_operationManager;
    
    NSMutableArray *_categorizedArticlesArray;
}

MainViewController.mに主要なロジックを記述しています。
プロダクトコードでは、APIとの通信やCoreDataとのやり取りは、ViewControllerと分離した方が良いかもしれません。
AFHTTPRequestOperationManagerは、APIとの通信に用います。

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view, typically from a nib.
    
    //Initialize AFHTTPRequestOperationManager with API Base URL
    _operationManager = [[AFHTTPRequestOperationManager alloc] initWithBaseURL:[NSURL URLWithString:@"http://geeknews.herokuapp.com/"]];
    
    [self refreshData];
       
}

ここでは、APIのベースURLを指定しています。

取得した記事を分類して保存する

#pragma mark - API
- (void)refreshData
{
    //Fetch articles from API
    [_operationManager GET:@"api/v1/article" parameters:nil success:^(AFHTTPRequestOperation *operation, id responseObject) {
    
        //AFNetworking parses the JSON response, which can now be used like a NSDictionary
        id articles = operation.responseObject;

        //for debug
        [Article MR_truncateAll];
        
        //Loop through articles from API
        for(id article in articles){

            //Take some values we'll need
            NSString *title = [article objectForKey:@"title"];
            NSString *link = [article objectForKey:@"link"];
            NSString *imageUrl = @"";
            NSString *body = NULL_TO_NIL([article objectForKey:@"description"]);
            NSInteger categoryId = [[article objectForKey:@"category_id"] integerValue];
            
            //Check if we already saved this articles...
            Article *existingEntity = [Article MR_findFirstByAttribute:@"link" withValue:link];

            //... if not, create a new entity
            if(!existingEntity)
            {
                Article *articleEntity = [Article MR_createEntity];
                articleEntity.categoryId = categoryId;
                articleEntity.title = title;
                articleEntity.imageUrl = imageUrl;
                articleEntity.body = body;
                articleEntity.link = link;
            }
        }
        
        //Persist created entities to storage
        [MagicalRecord saveUsingCurrentThreadContextWithBlock:^(NSManagedObjectContext *localContext) {
            
            //Fetch categorized articles
            _categorizedArticlesArray = [self newCategorizedArticlesArray];

            //reload table view
            for(UIView *view in [scrollView subviews]){
                if([view isKindOfClass:[UITableView class]]){
                    UITableView *tableView = (UITableView*)view;
                    tableView.delegate = self;
                    tableView.dataSource = self;
                    [tableView registerClass:[UITableViewCell class] forCellReuseIdentifier:@"Cell"];
                    [tableView reloadData];
                }
            }
            
        } completion:^(BOOL success, NSError *error) {
            nil;
        }];
        
        
    } failure:^(AFHTTPRequestOperation *operation, NSError *error) {
        NSLog(@"Failed to fetch articles from API");
    }];
}


処理全体を見てみると、HTTPリクエストの成功/失敗によって処理を分岐しています。
もう少し細かく見ていきます。

        //Loop through articles from API
        for(id article in articles){

            //Take some values we'll need
            NSString *title = [article objectForKey:@"title"];
            NSString *link = [article objectForKey:@"link"];
            NSString *imageUrl = @"";
            NSString *body = NULL_TO_NIL([article objectForKey:@"description"]);
            NSInteger categoryId = [[article objectForKey:@"category_id"] integerValue];

        }

リクエストが成功すれば、AFNetworkingが返却されたJSONをパースしてNSDictionayに変換してくれます。
このあと、必要な項目を取得しています。

            //Check if we already saved this articles...
            Article *existingEntity = [Article MR_findFirstByAttribute:@"link" withValue:link];

            //... if not, create a new entity
            if(!existingEntity)
            {
                Article *articleEntity = [Article MR_createEntity];
                articleEntity.categoryId = categoryId;
                articleEntity.title = title;
                articleEntity.imageUrl = imageUrl;
                articleEntity.body = body;
                articleEntity.link = link;
            }

取得した記事が、既にCoreDataに格納されているかどうか、記事URLを用いてチェックしています。
CoreDataに格納されていなければ、記事を保存します。

URLで記事を検索するのは微妙ですね。。。
本来はArticle.articleIdをプロパティに追加して、自動採番したIDをサーバレスポンスから取得した方が良いと思います。

分類した記事を、各tableViewに表示する

        
        //Persist created entities to storage
        [MagicalRecord saveUsingCurrentThreadContextWithBlock:^(NSManagedObjectContext *localContext) {
            
            //Fetch categorized articles
            _categorizedArticlesArray = [self newCategorizedArticlesArray];

            //reload table view
            for(UIView *view in [scrollView subviews]){
                if([view isKindOfClass:[UITableView class]]){
                    UITableView *tableView = (UITableView*)view;
                    tableView.delegate = self;
                    tableView.dataSource = self;
                    [tableView registerClass:[UITableViewCell class] forCellReuseIdentifier:@"Cell"];
                    [tableView reloadData];
                }
            }
            
        } completion:^(BOOL success, NSError *error) {
            nil;
        }];

分類した記事を、CoreDataから取得して、_categorizedArticlesArrayに保存します。
そして、ここで複数あるUITableViewのdelegateとdataSourceを指定します。
その後、[tableView reloadData]を実行して、記事を読み込みます。

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"Cell"
                                                            forIndexPath:indexPath];

    NSInteger categoryId = [self toCategoryId:tableView.tag];
    [self configureCell:cell atIndex:indexPath categoryId:categoryId];
    
    return cell;
}

-(void)configureCell:(UITableViewCell*)cell atIndex:(NSIndexPath*)indexPath categoryId:(NSInteger)categoryId{
    
    NSArray *categorizedArticles = [self lookupArticlesByCategoryId:_categorizedArticlesArray categoryId:categoryId];
    Article *article  = categorizedArticles[indexPath.row];
    
    cell.textLabel.text = article.title;
    
}

セルの内容は、indexPathとcategoryIdを用いて決定します。

こんな感じでしょうか。。。お役に立てば幸いです。
上級者の方は、アドバイスやマサカリコメントもお待ちしています。