swift初心者がSmartNews風ニュースアプリを作ってみる過程を晒す(6) - Alamofire + Object Mapper + Realm + SDWebImageで最低限動くニュースアプリを作る


## <i class="fa fa-cube" style="font-size:1em;"></i> はじめに

swiftはほとんど未経験ですが、SmartNews風ニュースアプリを作ってみて、その過程をさらしています。

前回は、こんな記事を書きました。

[swift初心者がSmartNews風ニュースアプリを作ってみる過程を晒す(5) - ニュースアプリにおけるApp Transport Securityについて考える - Qiita](http://qiita.com/tjnet/items/d2ea5dd425bc51f92996)


今回は、ニュース記事としての最低限の機能を実装して、一通り動くものを作成します。


## <i class="fa fa-cube" style="font-size:1em;"></i>現在の進捗

前回は、ニュースアプリにおけるApp Transport Securityについて考えました。

前回までのソースコードは下記コマンドで取得できます。

``git clone --branch v1.4 https://github.com/tjnet/NewsAppWithSwift.git``

## <i class="fa fa-cube" style="font-size:1em;"></i>今回、作ったもの

今回は、ニュースアプリとしての最低限の動作を実現するため、以下の機能を実装しました。

- Web APIから記事の取得
- Realmに記事を保存
- 記事タイトルの一覧表示
- 記事のサムネイルを表示
- 記事の詳細を表示

こんな感じで動作します。

![20160111_23_35](https://cloud.githubusercontent.com/assets/901084/12236377/b2c84b28-b8bc-11e5-815f-863fdc4f687a.gif)

開発環境はXCode7.2を使用しています。


ソースコードは下記コマンドで取得できます。

``git clone --branch v1.5 https://github.com/tjnet/NewsAppWithSwift.git``

## <i class="fa fa-cube" style="font-size:1em;"></i>記事はここから取得します

```ViewController.swift
        var feeds: [Dictionary<String, String>] =
        [
            [
                "link": "https://ajax.googleapis.com/ajax/services/feed/load?v=1.0&q=http://menthas.com/top/rss",
                "title": "top"
            ],
            [
                "link": "https://ajax.googleapis.com/ajax/services/feed/load?v=1.0&q=http://menthas.com/ruby/rss",
                "title": "ruby"
            ],
            [
                "link": "https://ajax.googleapis.com/ajax/services/feed/load?v=1.0&q=http://menthas.com/ios/rss",
                "title": "ios"
            ],
            [
                "link": "https://ajax.googleapis.com/ajax/services/feed/load?v=1.0&q=http://menthas.com/infrastructure/rss",
                "title": "infrastructure"
            ],
        ]
```
記事は、今、話題の[Menthas.com](http://menthas.com/)さんから取得させて頂きます :bow:
URLが重複していて冗長な記述なので、本当はどこかにまとめた方が良いですね.

Google Ajax Feed APIを使用しているのは、RSSフィードを直接JSONとして取得できて便利だからです。
[RSSフィードをJSONとして受け取る方法～Google Feed APIの応用 | 56docブログ](http://blog.56doc.net/Entry/472/)

## <i class="fa fa-cube" style="font-size:1em;"></i>ニュース記事を取得してみます

ネットワーク通信にはAlamofireを使用しています。
ニュース記事の取得元となるURLはfetchFromに代入しています。

JSONはこんな感じで返ってきます。

```.json
{
  responseData: {
  feed: {
    feedUrl: "http://menthas.com/top/rss",
    title: "Menthas[top]",
    link: "http://menthas.com",
    author: "",
    description: "プログラマ向けのニュースキュレーションサービスです。",
    type: "rss20",
    entries: [
     {

```
responseObjectメソッドの第1引数には、オブジェクトマッピング(オブジェクトとJSONレスポンスの紐付け)を行うためのkey path(ここではresponseData)を指定しています。第2引数にはクロージャを指定します。そのクロージャでは、Alamofire.Response<T, NSError>型の引数を扱うことができます。

記事データを取得するのは、response.result.valueを取得すれば良さそうです。
Alamofire.Responseの定義や[AlamofireとSwiftyJSONでAPIを叩くチュートリアル - Qiita](http://qiita.com/yuta-t/items/1b6dfe34fa8537cf3329)を読んで頂けると、理解できるかと、、、:bow:

```TableViewController.swift

Alamofire.request(.GET, fetchFrom).responseObject("responseData") { (response: Alamofire.Response<FeedResponse, NSError>) in
            
  guard let entries = response.result.value?.feed?.entries else {
        return
  }
            
  print(entries)
  print("Our default realm is located at \(RLMRealm.defaultRealm().path)")

  //この後、取得した記事をRealmに保存
                        
}
```

上記のコードについて、少し補足します。

```
  guard let entries = response.result.value?.feed?.entries else {
        return
  }
```

ここは、早めにreturnする方が、if letで書くよりもネストが深くならずにすみそうなので、こう書いています。

``print("Our default realm is located at \(RLMRealm.defaultRealm().path)")``

ここで表示するpathを[Realm Browser](https://itunes.apple.com/app/realm-browser/id1007457278)で開くと、Realmに保存したデータの内容を確認できます




- [Alamofire Tutorial Part 2: Progress and Caching - Ray Wenderlich](http://www.raywenderlich.com/87595/intermediate-alamofire-tutorial)
- [swift - Understanding Alamofire's Response Object Serialization use of Closures - Stack Overflow](http://stackoverflow.com/questions/30995538/understanding-alamofires-response-object-serialization-use-of-closures)
- [AlamofireとSwiftyJSONでAPIを叩くチュートリアル - Qiita](http://qiita.com/yuta-t/items/1b6dfe34fa8537cf3329)
- [antitypical/Result: Swift type modelling the success/failure of arbitrary operations.](https://github.com/antitypical/Result)


## <i class="fa fa-cube" style="font-size:1em;"></i>JSONレスポンスをSwiftのオブジェクトにマッピングします

JSONは、こんな形式で返ってきます。

```
{
  responseData: {
  feed: {
    feedUrl: "http://menthas.com/top/rss",
    title: "Menthas[top]",
    link: "http://menthas.com",
    author: "",
    description: "プログラマ向けのニュースキュレーションサービスです。",
    type: "rss20",
    entries: [
     {
       title: "Windows 7／8サポート、Skylake搭載端末上の場合は2017年7月17日までに - ITmedia ニュース",
       link: "http://www.itmedia.co.jp/news/articles/1601/17/news009.html",
       author: "",
       publishedDate: "Sat, 16 Jan 2016 19:13:11 -0800",
       contentSnippet: "MicrosoftがWindowsのサポートポリシーを変更し、プロセッサがIntelの第6世代Coreシリーズ（コードネーム：Skylake）の端末にインストールしたWindows 7およびWindows ...",
       content: "MicrosoftがWindowsのサポートポリシーを変更し、プロセッサがIntelの第6世代Coreシリーズ（コードネーム：Skylake）の端末にインストールしたWindows 7およびWindows 8.1については、サポートを2017年7月17日までとした。",
       categories: [ ]
      ...(略)...

},
```


このJSONレスポンスを、AlamofireObjectMapperを用いてFeedResponseオブジェクト、Feedオブジェクト、Entryオブジェクトにマッピングしています。

```FeedResponse.swift
import Foundation
import ObjectMapper

class FeedResponse: Mappable {
    var feed: Feed?
    
    required init?(_ map: Map){
        
    }
    
    func mapping(map: Map) {
        feed <- map["feed"]
    }
    
}
```
```Feed.swift
import Foundation
import ObjectMapper

class Feed: Mappable {
    var entries: [Entry]?
    
    required init?(_ map: Map){
        
    }
    func mapping(map: Map) {
        entries <- map["entries"]
    }
}
```

``dynamic var`` は、propertyがrealmのデータのアクセサになっていることを意味しています。
initializerの宣言の仕方については、　[The Swift Programming Language (Swift 2.1): Initialization](https://developer.apple.com/library/ios/documentation/Swift/Conceptual/Swift_Programming_Language/Initialization.html) が参考になりそうです。

```Entry.swift
import Foundation
import ObjectMapper
import RealmSwift

class Entry: Object, Mappable {
    
    dynamic var title = ""
    dynamic var link = ""
    dynamic var contentSnippet = ""
    dynamic var category = ""
    
    required convenience init?(_ map: ObjectMapper.Map) {
        self.init()
    }
    func mapping(map: Map) {
        title <- map["title"]
        link <- map["link"]
        contentSnippet <- map["contentSnippet"]
    }    
    override class func primaryKey() -> String {
        return "link"
    }
}
```

## <i class="fa fa-cube" style="font-size:1em;"></i>次はデータを保存します

```AppDelegate.swift

let uiRealm = try! Realm()

```

プロジェクトのすべてのswiftファイルでRealm objectをシェアするため、これをAppDelegate.swiftの冒頭に書きます。

```TableViewController.swift

      try! uiRealm.write { () -> Void in
          
          for entry in entries {
              entry.category = self.title!
              
              //see https://github.com/realm/realm-cocoa/issues/2149
              //see https://realm.io/docs/swift/latest/api/Classes/Realm.html#/s:FC10RealmSwift5Realm3adduRq_Ss12SequenceTypedqqq_S1_9GeneratorSs13GeneratorType7ElementCS_6Object_FS0_FTq_6updateSb_T_
              
              uiRealm.add(entry, update: true)
              
          }
      }
      self.readEntriesAndUpdateUI()

```

ぐるぐるループして、entryをRealmに保存するためにwriteメソッドをcallしています。

``uiRealm.add(entry, update: true)`` の第２引数に指定しているtrueは、既に保存されているオブジェクトであれば、同じprimary keyでupdateすることを意味しています。詳細は[If true will try to update existing objects with the same primary key.](https://realm.io/docs/swift/latest/api/Classes/Realm.html#/s:FC10RealmSwift5Realm3adduRq_Ss12SequenceTypedqqq_S1_9GeneratorSs13GeneratorType7ElementCS_6Object_FS0_FTq_6updateSb_T_)をご参照ください。

## <i class="fa fa-cube" style="font-size:1em;"></i>記事のUIを作成

記事一覧の一つ一つの記事のUIは、カスタムセルにAuto Layoutを適用して作っていきます。

カスタムセルの作り方は、このあたりの記事が、参考になりそうです。
[Storyboards Tutorial in iOS 9: Part 1 - Ray Wenderlich](http://www.raywenderlich.com/113388/storyboards-tutorial-in-ios-9-part-1)

Auto Layoutについては、以下の記事が参考になりそうです。

[Auto Layout Tutorial in iOS 9 Part 1: Getting Started - Ray Wenderlich](http://www.raywenderlich.com/115440/auto-layout-tutorial-in-ios-9-part-1-getting-started-2)
[Auto Layout Tutorial in iOS 9 Part 2: Constraints - Ray Wenderlich](http://www.raywenderlich.com/115444/auto-layout-tutorial-in-ios-9-part-2-constraints)

## <i class="fa fa-cube" style="font-size:1em;"></i>記事の一覧を表示

```TableViewController.swift

    func readEntriesAndUpdateUI(){
        
        let predicate = NSPredicate(format: "category = %@", self.title!)
        lists = uiRealm.objects(Entry).filter(predicate)

        self.tableView.reloadData()
    }

```

JSONパースとRealmへのデータ保存が終了したら、tableViewを更新します。

tableViewの表示部分は、こんな感じで実装しています。

```TableViewController.swift
    override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {

        let cell : FeedTableViewCell = tableView.dequeueReusableCellWithIdentifier("FeedTableViewCell") as! FeedTableViewCell
    
        // Configure the cell...
        let entry = lists[indexPath.row] as Entry
        
        cell.configure(entry)
        
    
        return cell
    }

```

```FeedTableViewCell.swift
    func configure(entry: Entry){
        titleLabel.text = entry.title
        descriptionLabel.text = entry.contentSnippet
        self.link = entry.link
    }
```

![スクリーンショット 2016-01-20 0.02.03.png](https://qiita-image-store.s3.amazonaws.com/0/34321/f96554e7-67f1-581a-6cec-efd08e8ce741.png)


## <i class="fa fa-cube" style="font-size:1em;"></i>ニュース記事のサムネイルの表示

画像の取得処理を非同期処理で行いたいので、SDWebImageを使用した実装にしています。


```TJExtensions.swift
import Foundation
import UIKit
import WebImage

extension UIImageView {
    func loadWebImage(url:NSURL!){
        self.sd_setImageWithURL(url)
    }
    
    func loadWebImage(url:NSURL!, placeholderImage:UIImage!) {
        self.sd_setImageWithURL(url, placeholderImage: placeholderImage)
    }
    
    func loadWebImage(url:NSURL!, placeholderImage:UIImage!,completeion:SDWebImageCompletionBlock){
        self.sd_setImageWithURL(url, placeholderImage: placeholderImage, completed: completeion)
    }
}
```

```FeedTableViewCell.swift
class FeedTableViewCell: UITableViewCell {
    
    @IBOutlet weak var thumbnailImageView: UIImageView!
    @IBOutlet weak var titleLabel: UILabel!
    @IBOutlet weak var descriptionLabel: UILabel!
    
    var link: String! {
        didSet {
            Alamofire.request(.GET, ArticleAPI.ogpImage + link).responseObject("") { (response: Response<OGPResponse, NSError>) in
                var imageUrl: NSURL?
                let ogpResponse = response.result.value
                if let ogpImgSrc = ogpResponse?.image {
                        imageUrl = NSURL(string: ogpImgSrc)
//                        print(imageUrl)
                        self.setThumbnailWithFadeInAnimation(imageUrl)
                }
            }
        }
    }

    func setThumbnailWithFadeInAnimation(imageUrl: NSURL!){
        self.thumbnailImageView.loadWebImage(imageUrl, placeholderImage: nil, completeion: {
            (image, error, cacheType, url) ->Void in
            self.thumbnailImageView.alpha = 0
            
            UIView.animateWithDuration(0.25,
                animations: {
                    self.thumbnailImageView.alpha = 1
            })
        })
    }
}
```

```API.swift
import UIKit

class ArticleAPI {
    static let ogpImage = "http://api.hitonobetsu.com/ogp/analysis?url="
}
```

サムネイルの生成に関しては、[OGP（Open Graph Protocol）データ取得API](http://140note.hitonobetsu.com/apipage/ogp)を使用させて頂きました。:bow:
画像がOGPに設定されていなくとも、可能であれば代替の画像を返してくれるので、非常に便利です。

## <i class="fa fa-cube" style="font-size:1em;"></i>一覧画面から記事タイトルをタップすると、ニュース記事の内容を表示

最後に、記事一覧画面から記事をタップした時の画面遷移です。

```
  override func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
        let entry = lists[indexPath.row] as Entry
        
        let svc = SFSafariViewController(URL: NSURL(string: entry.link)!)
        self.presentViewController(svc, animated: true, completion: nil)

        
        reloadRowsAtIndexPath(indexPath)
    }
```

[SFSafariViewController Class Reference](https://developer.apple.com/library/ios/documentation/SafariServices/Reference/SFSafariViewController_Ref/)を使用して、記事の内容を表示しています。これはiOS9で導入された新しいcontrollerです。

UIWebViewで実装することもできますが、Back/Forwardボタン等を自分で実装するのもシンドイので、今回はSFSafariViewControllerを使用しました。

アプリから離脱させることなくブラウザとしての機能を提供でき、わずか数行のコードでwebコンテンツを表示できます。



## <i class="fa fa-cube" style="font-size:1em;"></i>感想

...ひとまず、最低限の動きはできるようになりました。

何だかViewControllerが肥大化して、今後の仕様変更がツラくなりそうです。
このままではテストも書きにくそうですね。

次回以降は、今後の拡張を踏まえてアーキテクチャの変更(MVVMへ移行、ReactiveCocoaの導入)やテストコードの追加(Quickの導入)を行います。

ここまでのソースコードは下記コマンドで取得できます。

``git clone --branch v1.5 https://github.com/tjnet/NewsAppWithSwift.git``


## <i class="fa fa-cube" style="font-size:1em;"></i>参考

[SwiftだけでRSSリーダーを作ってみた - 株式会社エウレカ](http://eure.jp/blog/swift_rss_kim/)