仮想通貨取引所のReal Time APIをiOSで取得する

iOSで各取引所の Real Time API取得に小々手こずってしまったため、導入から表示まで紹介したいと思います。

開発環境は
・iOS 11.x 
・swift 4.x 

今回扱う仮想通貨取引所は
・bitFlyer    [(参照：bitFlyer API)](https://lightning.bitflyer.jp/docs?lang=ja&_ga=2.77444630.20947120.1519443559-1359730683.1517879873#realtime-api)
・coincheck [(参照：coincheck API)](https://coincheck.com/ja/documents/exchange/api#websocket)
・zaif　[(参照：zaif API)](https://corp.zaif.jp/api-docs/)

使用したライブラリは
・PubNub  [(参照:PubNub GitHub)](https://github.com/pubnub/objective-c)
・Starscream  [(参照：Starscream GitHub)](https://github.com/daltoniam/starscream)

PubNubは主にbitFlyer
Starscreamは主にcoincheckとzaif
のAPIで使用します。

#１．各ライブラリの導入
PubNubは公式documentを参考に進めます。
https://www.pubnub.com/docs/swift/pubnub-swift-sdk
ライブラリがObjective-Cのため、iOS7.0以下である場合は「Objective-C Bridging-Header」が必要になるため、注意が必要である。

StarscreamはGitHubのRead Meを参照して進めます。
https://github.com/daltoniam/starscream

#２．ネットワーク接続に関する設定
iOS 9.0以降ではiPhoneアプリからアクセス出来るURLは「https」で始まる、安全性の高いURLのみとなりました。
安全性の低いURLにアクセスする場合は「App Transport Security」の設定を下げる必要がある。
今回の双方向通信ではWebSocket通信とと呼ばれ、HTTPでサーバーにリクエストを送った後、TCPのコネクションが確立されて、定期的にsubscribe出来る仕組みとのことで、上記の設定で受け付けない可能性があるため編集します。
(※ 設定不要かもしれません。自分はこうすることで解決できました。)

Info.plist
1. 「App Transport Security Settings」を新しく追加します。
2. 左端にある「▶」をクリックして、「▼」にした状態で「Allow Arbitrary Loads」を新しく追加します。
3. 「Value」値がNOになっているため、YESに変更します。

#３．PubNubの設定
App Delegateにしかimportが出来なかったため、App Delegateに記述します。
アプリが起動されている間ずっとデータをsubscribeしてしまっては、無駄にデータを受信してしまうため、必要なタイミングでPubNubを起動できるように以下のように書き換えます。

```AppDelegate.swift
import UIKit
import PubNub

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {

    var window: UIWindow?
    var client: PubNub?

    // PubNub 設定
    func createConfiguration(subscribeKey: String) -> PubNub {
        let conf = PNConfiguration(publishKey: "none", subscribeKey: subscribeKey)
        conf.stripMobilePayload = false
        return PubNub.clientWithConfiguration(conf)
    }
```

```AppDelegate.swift
extension AppDelegate: PNObjectEventListener {

    func client(_ client: PubNub, didReceive status: PNStatus) {

        if status.category == .PNUnexpectedDisconnectCategory {}
        else if status.category == .PNConnectedCategory {}
        else if status.category == .PNReconnectedCategory {}
        else if status.category == .PNDisconnectedCategory {}
        else if status.category == .PNDecryptionErrorCategory {}
    }

    func client(_ client: PubNub, didReceiveMessage message: PNMessageResult) {

        if !(message.data.channel.isEmpty) {
          // (※1) 
        }
    }
}
```

ViewControllerで実際にPubNubを起動させます。
今回は画面表示された際(ViewWillAppear)でsubscribeを開始し、画面非表示になった際(ViewWillDisappear)でsubscribeを終了します。
(※説明に不要な部分は抜いています。)

```ViewController.swift
import UIKit
class ViewController: UIViewController{
    fileprivate let appDelegate: AppDelegate = UIApplication.shared.delegate as! AppDelegate

    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)
        appDelegate.client = appDelegate.createConfiguration(subscribeKey: "sub-c-52a9ab50-291b-11e5-baaa-0619f8945a4f")
        appDelegate.client?.addListener(appDelegate)
        appDelegate.client?.subscribeToChannels(["lightning_board_BTC_JPY"], withPresence: false)
    }

    override func viewWillDisappear(_ animated: Bool) {
        super.viewWillDisappear(animated)
        appDelegate.client?.unsubscribeFromChannels(["lightning_board_BTC_JPY"], withPresence: false)
    }
}
```
このsubscribeによって取得したデータは  (※1)に記述し、ViewModelの値を更新するなどして無理やり実装してみました。


#４．Starscreamの設定
Starscreamではzaifでは単純に実装できましたが、coincheckで小々手こずってしまいました。
coincheckでは複数channelがあるため、どのchannelをsubscribeするかリクエストを送る必要があります。リクエストを送るタイミングを誤ってしまったことが原因で接続に時間がかかってしまいました。
Starscreamはsubscribeしたいファイルに直接記述できます。

```ViewController.swift
import UIKit
import Starscream

class ViewController: UIViewController{
    fileprivate lazy var socket: WebSocket = {
        // coincheckの場合
        let socket = WebSocket(url: URL(string: "wss://ws-api.coincheck.com/")!)
        // zaifの場合
        // let socket = WebSocket(url: URL(string: "wss://ws.zaif.jp:8888/stream?currency_pair=btc_jpy")!)
        socket.delegate = self
        return socket
    }()

    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)
        socket.connect()    
    }

    override func viewWillDisappear(_ animated: Bool) {
        super.viewWillDisappear(animated)
        socket.disconnect()
    }

}
```
Starscreamでは「WebSocketDelegate」が必要です。
coincheckのリクエスト送信は、websocketDidConnect()に記述しています。
またWebSocketで受信したデータは(※ 2)で取得できるため、その後の処理を記述する。

```ViewController.swift
extension ViewController : WebSocketDelegate {
    func websocketDidConnect(socket: WebSocketClient) {

         var jsonObj = Dictionary<String,Any>()
         jsonObj["type"] = "subscribe"
         jsonObj["channel"] = "btc_jpy-orderbook"
         do {
             let jsonData = try JSONSerialization.data(withJSONObject: jsonObj, options: [])
             let jsonStr = String(bytes: jsonData, encoding: .utf8)!
             socket.write(string: jsonStr)
         } catch let error {
             print("error: \(error)")
         }
    }

    func websocketDidDisconnect(socket: WebSocketClient, error: Error?) {
        print("コネクションが切断されました。(error: \(String(describing: error))")
    }

    func websocketDidReceiveMessage(socket: WebSocketClient, text: String) {
       // (※ 2)
    }

    func websocketDidReceiveData(socket: WebSocketClient, data: Data) {}
}
```