iOSのWidgetをAPIからデータを取得して表示させてみる

![Screen Shot 2015-03-02 at 1.20.49 PM.png](https://qiita-image-store.s3.amazonaws.com/0/18766/a810d3da-92db-6545-732d-087255125fa9.png "Screen Shot 2015-03-02 at 1.20.49 PM.png")
な感じで表示されるものを作ってみる。

[準備に関してはこちら](http://qiita.com/kiyotaman/items/5b37d99a6fd89a3bb005)

開発する前にざっと[ドキュメント](https://developer.apple.com/library/prerelease/ios/documentation/General/Conceptual/ExtensibilityPG/NotificationCenter.html)を読んでみる。

冒頭のWidgetの振る舞いについて意訳をしてみる。

Ensure that content always looks up to date
**コンテツはいつも最新にしておけよ！**

Respond appropriately to user interactions
**いい感じで使いやすくしろよ！**

Perform well (in particular, iOS widgets must use memory wisely or the system may terminate them)
**ちゃんと動かせよ！**

Avoid putting a scroll view inside a Today widget.
**scroll viewは使わないで！**

これってUIScrollViewのことじゃないよね？？？
なのでpageControlとscrollViewを利用して実装してみる。


## Widget出来上がりサンプル
以下のような仮実装をしてみた。
＞ボタンを押して画面遷移をさせるようにしてみた。
![Screen Shot 2015-02-19 at 8.07.28 PM.png](https://qiita-image-store.s3.amazonaws.com/0/18766/9000ad5e-adbf-fd1e-a765-6ebbc19285a1.png "Screen Shot 2015-02-19 at 8.07.28 PM.png")
![Screen Shot 2015-02-19 at 8.08.39 PM.png](https://qiita-image-store.s3.amazonaws.com/0/18766/fef86ce6-1743-1d49-dce5-769f861745b3.png "Screen Shot 2015-02-19 at 8.08.39 PM.png")

## サンプルソース
### TodayView.swift
```swift:TodayView.swift
import UIKit
import Foundation

public class TodayView : UIView {
    var currentPage: Int?
    
    required public init(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
    }

    override init(frame : CGRect) {
        super.init(frame : frame)
    }

    convenience init(_ currentPage: Int) {
        self.init(frame: CGRectZero)
        self.setup(currentPage)
    }

    func setup(currentPage: Int) {
        self.currentPage = currentPage
    }
}
```

### TodayViewControllerサンプル
```swift:TodayViewControllerSample.swift
import UIKit
import NotificationCenter


class TodayViewController: UIViewController, NCWidgetProviding {
    @IBOutlet weak var pageControl: UIPageControl!
    @IBOutlet weak var scrollView: UIScrollView!
    @IBOutlet weak var nextButton: UIButton!
    @IBOutlet weak var backButton: UIButton!

    let numberOfPages: Int = 5 // ページ数
    let viewHeight: CGFloat = 140 // 画面の高さ

    override func viewDidLoad() {
        super.viewDidLoad()
        
        // pageControlの準備
        pageControl.numberOfPages = numberOfPages

        // scrollViewの準備
        scrollView.contentSize = CGSize(
            width: CGFloat(numberOfPages) * scrollView.frame.size.width,
            height: viewHeight
        )

        // Widgetに高さを明示的に教える
        self.preferredContentSize = CGSizeMake(0, viewHeight);
        
        self.updateScrollViewContent()
    }
    
    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
    }
    
    func widgetPerformUpdateWithCompletionHandler(completionHandler: ((NCUpdateResult) -> Void)!) {
        // Perform any setup necessary in order to update the view.

        // If an error is encountered, use NCUpdateResult.Failed
        // If there's no update required, use NCUpdateResult.NoData
        // If there's an update, use NCUpdateResult.NewData

        completionHandler(NCUpdateResult.NewData)
    }
    
    // 戻るボタンアクション
    @IBAction func backButton(sender: UIButton) {
        if pageControl.currentPage > 0 {
            pageControl.currentPage--
            nextButton.hidden = false
        }
        if pageControl.currentPage == 0 {
            backButton.hidden = true
        }
        self.updateScrollViewContentOffset()
    }

    // 進むボタンアクション
    @IBAction func nextButton(sender: UIButton) {
        if pageControl.currentPage < numberOfPages {
            pageControl.currentPage++
            backButton.hidden = false
        }
        if pageControl.currentPage == numberOfPages-1 {
            nextButton.hidden = true
        }
        self.updateScrollViewContentOffset()
    }

    // pageControlのcurretPageValueが更新された時の処理
    @IBAction func pageChanged(sender: UIPageControl) {
        self.updateScrollViewContentOffset()
    }

    // 横スクロールのオフセットをページ開始に合わせる
    func updateScrollViewContentOffset() {
        scrollView.setContentOffset(
            CGPoint(
                x: scrollView.frame.size.width * CGFloat(pageControl.currentPage),
                y: 0.0
            ),
            animated: true
        )
    }

    func updateScrollViewContent() {
        // ページ数分の背景色を準備してみる
        let colors = [
            UIColor.redColor(),
            UIColor.blackColor(),
            UIColor.yellowColor(),
            UIColor.greenColor(),
            UIColor.purpleColor()
        ]
        for var i = 0; i < numberOfPages; i++ {
            let pageView = TodayView(i)
            let w = scrollView.frame.size.width
            let x = CGFloat(i) * w
            pageView.frame = CGRectMake(x, 0, w, viewHeight)
            pageView.backgroundColor = colors[i]
            scrollView.addSubview(pageView)
        }
    }
}

```

### APIと繋いだTodayViewControllerのコード例
まだ綺麗にできると思うけど、こんな感じでAPIからデータを取得して表示させることができる。

```swift:TodayViewController.swift
import UIKit
import NotificationCenter


class TodayViewController: UIViewController, NCWidgetProviding {
    @IBOutlet weak var page1ViewButton: UIButton!
    @IBOutlet weak var page2ViewButton: UIButton!
    @IBOutlet weak var page3ViewButton: UIButton!
    @IBOutlet weak var page4ViewButton: UIButton!
    @IBOutlet weak var page5ViewButton: UIButton!
    
    @IBOutlet weak var pageControl: UIPageControl!
    @IBOutlet weak var scrollView: UIScrollView!
    @IBOutlet weak var nextButton: UIButton!
    @IBOutlet weak var backButton: UIButton!
    
    @IBOutlet weak var page1View: UIView!
    @IBOutlet weak var page2View: UIView!
    @IBOutlet weak var page3View: UIView!
    @IBOutlet weak var page4View: UIView!
    @IBOutlet weak var page5View: UIView!
    
    @IBOutlet weak var page1Label: UILabel!
    @IBOutlet weak var page2Label: UILabel!
    @IBOutlet weak var page3Label: UILabel!
    @IBOutlet weak var page4Label: UILabel!
    @IBOutlet weak var page5Label: UILabel!
    
    @IBOutlet weak var page1ImageView: UIImageView!
    @IBOutlet weak var page2ImageView: UIImageView!
    @IBOutlet weak var page3ImageView: UIImageView!
    @IBOutlet weak var page4ImageView: UIImageView!
    @IBOutlet weak var page5ImageView: UIImageView!
    
    @IBOutlet weak var activityIndicatorView: UIActivityIndicatorView!
    private var responseData: Array<Dictionary<String, AnyObject>>?
    private let numberOfPages: Int = 5 // ページ数
    private let viewHeight: CGFloat = 140 // 画面の高さ
    private let imageURLBase: String = "https://*******"
    private let imageURLSize: String = "********"
    private let endpoint : String = "https://**************"
    private var loading: Bool = false
    private var pageViews: [UIView] = []
    private var pageLabels: [UILabel] = []
    private var pageImageViews: [UIImageView] = []
    private var imageIds: [String] = []
    private var currentCountryCode: String = ""
    override func viewDidLoad() {
        super.viewDidLoad()
        self.loading = false
        var locale = NSLocale.currentLocale()
        currentCountryCode = locale.objectForKey(NSLocaleCountryCode) as String
        pageViews = [
            page1View, page2View, page3View,
            page4View, page5View
        ]
        pageLabels = [
            page1Label, page2Label, page3Label,
            page4Label, page5Label
        ]
        pageImageViews = [
            page1ImageView, page2ImageView, page3ImageView,
            page4ImageView, page5ImageView
        ]

        // pageControlの準備
        pageControl.numberOfPages = numberOfPages
        
        // scrollViewの準備
        let scrollViewWidth = scrollView.frame.size.width
        scrollView.contentSize = CGSize(
            width: CGFloat(numberOfPages) * scrollViewWidth,
            height: viewHeight
        )
        for var i = 0; i < numberOfPages; i++ {
            pageViews[i].frame = CGRectMake(
                CGFloat(i) * scrollViewWidth, 0,
                scrollViewWidth, viewHeight
            )
            scrollView.addSubview(pageViews[i])
            pageImageViews[i].layer.cornerRadius = pageImageViews[i].frame.size.width/2
            pageImageViews[i].clipsToBounds = true
        }
        
        // Widgetに高さを明示的に教える
        self.preferredContentSize = CGSizeMake(0, viewHeight)
    }
    
    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
    }
    
    func widgetPerformUpdateWithCompletionHandler(completionHandler: ((NCUpdateResult) -> Void)!) {
        if self.loading == false {
            dispatch_async(dispatch_get_main_queue(), {
                self.activityIndicatorView.startAnimating()
            })
            self.loading = true
            let config = NSURLSessionConfiguration.defaultSessionConfiguration()
            let session = NSURLSession(configuration: config)
            var url = NSURL(string: self.endpoint)
            let task = session.dataTaskWithURL(url!) {
                (data, response, error) in
                var dict = NSJSONSerialization.JSONObjectWithData(data, options: NSJSONReadingOptions.MutableContainers, error:nil) as Dictionary<String, Array<Dictionary<String, AnyObject>>>;
                if error == nil && dict["root"] != nil {
                    self.responseData = dict["root"]?
                    if self.responseData?.count > 0 {
                        self.updateScrollViewContent()
                        completionHandler(NCUpdateResult.NewData)
                    } else {
                        completionHandler(NCUpdateResult.NoData)
                    }
                } else {
                    completionHandler(NCUpdateResult.Failed)
                }
                self.loading = false
            }
            task.resume()
        }
    }
    
    // 戻るボタンアクション
    @IBAction func backButton(sender: UIButton) {
        if pageControl.currentPage > 0 {
            pageControl.currentPage--
            nextButton.hidden = false
        }
        if pageControl.currentPage == 0 {
            backButton.hidden = true
        }
        self.updateScrollViewContentOffset()
    }
    
    // 進むボタンアクション
    @IBAction func nextButton(sender: UIButton) {
        if pageControl.currentPage < numberOfPages {
            pageControl.currentPage++
            backButton.hidden = false
        }
        if pageControl.currentPage == numberOfPages-1 {
            nextButton.hidden = true
        }
        self.updateScrollViewContentOffset()
    }
    
    // pageControlのcurretPageValueが更新された時の処理
    @IBAction func pageChanged(sender: UIPageControl) {
        self.updateScrollViewContentOffset()
    }
    
    @IBAction func openURL1(sender: UIButton) {
        self.openURL(0)
    }
    
    @IBAction func openURL2(sender: UIButton) {
        self.openURL(1)
    }
    
    @IBAction func openURL3(sender: UIButton) {
        self.openURL(2)
    }
    
    @IBAction func openURL4(sender: UIButton) {
        self.openURL(3)
    }
    
    @IBAction func openURL5(sender: UIButton) {
        self.openURL(4)
    }

    func openURL(index: Int) {
        var url = NSURL(string:"snapdish://dish?id=" + self.imageIds[index])
        self.extensionContext?.openURL(url!, completionHandler:{
            (success: Bool) -> Void in
        })
    }

    // 横スクロールのオフセットをページ開始に合わせる
    func updateScrollViewContentOffset() {
        scrollView.setContentOffset(
            CGPoint(
                x: scrollView.frame.size.width * CGFloat(pageControl.currentPage),
                y: 0.0
            ),
            animated: true
        )
    }
    
    // 画像を更新する
    func updateImageInScrollViewContent(index: Int, imageId: String?) {
        if let imageId = imageId? {
            self.imageIds.append(imageId)
            let url = NSURL(string: imageURLBase + imageId + imageURLSize);
            let req = NSURLRequest(URL: url!)
            NSURLConnection.sendAsynchronousRequest(req, queue:NSOperationQueue.mainQueue()){
                (res, data, error) in
                if error == nil {
                    self.pageImageViews[index].image = UIImage(data: data)
                } else {
                    self.pageImageViews[index].image = nil;
                }
            }
        } else {
            self.pageImageViews[index].image = nil;
        }
    }
    
    // scrollViewの内容を更新する
    func updateScrollViewContent() {
        dispatch_async(dispatch_get_main_queue(), { // main threadへ移動
            self.pageControl.numberOfPages = self.numberOfPages
            if let newNumberOfPages = self.responseData?.count {
                if newNumberOfPages < self.numberOfPages {
                    self.pageControl.numberOfPages = newNumberOfPages
                }
            }
            let w = self.scrollView.frame.size.width
            for i in 0..<self.numberOfPages {
                if self.pageControl.numberOfPages > i {
                    if let dict = self.responseData?[i] {
                        self.pageViews[i].hidden = false
                        self.pageLabels[i].text = dict["title"] as? String
                        if let imageId = dict["image_id"] as? Dictionary<String, String> {
                            self.updateImageInScrollViewContent(i, imageId: imageId["$oid"])
                        }
                    }
                } else {
                    self.pageViews[i].hidden = true
                }
            }
            self.nextButton.hidden = false
            self.activityIndicatorView.stopAnimating()
        })
    }
}

```

## まとめ
今更だけどSwift書くとObjective-Cに戻れなくなりそう。直感的にサクサクかけてしまう。