hiragram no blog

iOSとか

Vapor + Vapor Cloudでクローラー的バッチ処理

この記事はVASILY Advent Calendar 2017の17日目の記事です。

また、以下の記事の続きです。

hiragram.hatenablog.jp

アプリにJSONを返す部分を安全にする話を前回したので、今度は取引所のAPIを叩いてデータを取得するバッチを紹介する。

データ集めのバッチもVaporの Command の仕組みで動かしている(ここでは詳細は割愛するが、Vapor Cloudはcronもサポートしている)。

今度はバッチ側が取引所APIのクライアントサイドの位置付けになるので、まずは自作フレームワークAbstractionKit を用いて各取引所APIを抽象化し、RxのObservableを経由してデータを取得するクライアント層を作る。

Coincheckの売買価格を取得するAPIをこう定義する。

struct CoincheckEndpoints {
    struct CurrentRate: EndpointDefinition {
        typealias Response = SingleResponse<Element>
        typealias Environment = CoincheckEnvironment

        struct Element: Himotoki.Decodable, SingleResponseElement {
            // 略
        }

        var path: String = "/api/exchange/orders/rate"

        var method: HTTPMethod = .get
        var parameters: [String: Any]

        enum Order {
            case sell
            case buy

            var name: String {
                switch self {
                case .buy:
                    return "buy"
                case .sell:
                    return "sell"
                }
            }
        }

        init(orderType: Order) {
            parameters = [
                "order_type": orderType.name,
                "pair": "btc_jpy",
                "amount": 0.005,
            ]
        }
    }
}

EndpointDefinition はAbstractionKitが提供するプロトコルの一つで、各エンドポイントのリクエストパラメータ、レスポンスの型、HTTPメソッド、パスなどを定義する。

次いで、 EndpointDefinition のインスタンスを受けて実際にリクエストを投げ、結果を取得できるObservableを返す request メソッドを作る。

    static func request<Endpoint: EndpointDefinition>(_ endpoint: Endpoint) -> Single<Endpoint.Response.Result> {
        return Single.create(subscribe: { (observer) -> Disposable in
            DispatchQueue.global(qos: .default).async {
                do {
                    let response: Response
                    let absoluteURLString = Endpoint.environment.url(forPath: endpoint.path).absoluteString
                    let parameters = endpoint.parameters.mapValues({ (value) -> String in
                        String.init(describing: value)
                    })

                    switch endpoint.method {
                    case .get:
                        response = try httpClient.get(absoluteURLString, query: parameters)
                    case .post:
                        response = try httpClient.post(absoluteURLString, query: parameters)
                    case .delete:
                        response = try httpClient.delete(absoluteURLString, query: parameters)
                    case .patch:
                        response = try httpClient.patch(absoluteURLString, query: parameters)
                    }
                    if response.status.statusCode >= 300 {
                        // Error
                        observer(.error(NetworkError.uncategorized(message: response.status.reasonPhrase)))
                        return
                    } else if let bytes = response.body.bytes {
                        let data = Data.init(bytes: bytes)
                        guard let json = try JSONSerialization.jsonObject(with: data, options: []) as? Endpoint.Response.JSON else {
                            observer(.error(NetworkError.uncategorized(message: "Failed to cast JSON type")))
                            return
                        }
                        let result = try Endpoint.Response.init(json: json).result
                        observer(.success(result))
                    } else {
                        observer(.error(NetworkError.uncategorized(message: "Failed to extract bytes")))
                        return
                    }
                } catch let error {
                    observer(.error(error))
                }
            }

            return Disposables.create {
            }
        })

必要になったものから実装しているので、HTTPリクエストの機能をすべて網羅していないが気にしないでほしい。

エンドポイントの定義とリクエストメソッドが揃ったので、以下のように通信を起動する事ができるようになった。

swift let endpoint = CoincheckEndpoints.CurrentRate.init(orderType: .buy) APIClient.request(endpoint).subscribe().disposed(by: bag) ExchangeClient というプロトコルを導入し、取引所APIジェネリックに扱えるようにする。

protocol ExchangeClient {
    func currentAskRate() -> Single<Ask>
    func currentBidRate() -> Single<Bid>
}

クロール処理自体はRxSwift+RxBlockingで管理している。結局同期的にやるのだが、Rxに乗せておけば順番を入れ替えたり並列処理に変えたりというのが極めて簡単にできる。楽。

    public func run(arguments: [String]) throws {
        let clients: [ExchangeClient] = [
            CoincheckClient.init(),
            BitflyerClient.init(),
        ]

        let askRatesObservable = Observable.combineLatest(clients.map { $0.currentAskRate().asObservable().take(1) })
        let bidRatesObservable = Observable.combineLatest(clients.map { $0.currentBidRate().asObservable().take(1) })

        let asks = try askRatesObservable.toBlocking().first() // ここで同期的に通信が完了するのを待っている
        let bids = try bidRatesObservable.toBlocking().first()

        try asks?.forEach {
            try $0.makeQuery(driver).save()
        }

        try bids?.forEach {
            try $0.makeQuery(driver).save()
        }
    }

Vapor Cloudの一番安いDBインスタンスは、直接SQLクライアントアプリ等からつなぐことが出来ず、phpMyAdminが提供されるだけなのがちょっとつらいが、まあ安いので目をつぶろう、という感じです。

簡単にだが、クローラーバッチの紹介を以上とする。

次は23日に

  • SwaggerからSwiftのコードを生成するPython製ジェネレータ

の記事を書くから期待してほしい。

安全サーバーサイドSwift

この記事はVASILY Advent Calendar 2017の9日目の記事です。

目指すのは労働からの引退。

VASILY開発合宿で取り組んだ内容です。

何を作りたいのか

ビットコインの売買価格は取引所によって異なる。そこで、安い取引所で買って、すぐに高い取引所で売ることができれば、ビットコインそのものの価格変動に左右されずに利益を得ることが出来る。これがアービトラージ取引である。

今回は、複数の取引所のAPIを叩いて定期的に売買価格を取得するサーバーサイドアプリケーションと、その情報を表示するiOSアプリケーションを作った。

システム構成

サーバーサイド

  • 言語: Swift
  • Webフレームワーク: Vapor
  • インフラ: Vapor Cloud
  • バッチ処理の一部にRxSwift

iOSアプリ

  • Swift, RxSwift, APIKit

その他

サーバーサイドのAPIとモデル型はSwaggerのドキュメントで管理し、各エンドポイントに対応するコントローラとモデル定義はSwaggerのYAMLから自動生成する仕組みを作った。具体的にはSwaggerのYAMLドキュメントをパースしてテンプレート通りにコードを生成するジェネレータをPythonで作った。テンプレートはAppleのgybで書く。

「Swaggerドキュメントから自動生成できないAPI/モデル定義はそもそも設計が間違っているのではないか」という仮説が自分の中にあり、それを検証するためのこのプロジェクトとも言える。

このコードジェネレータについてはVASILY Advent Calendar 2017の別枠で改めて記事にするのでぜひ購読しておいてほしい。

現状できているもの

f:id:hiragram:20171208200603p:plain:h600

CoincheckとBitflyerの価格を取得して、1BTCを安い方で買って高い方で売ったときの差益を表示している。スクリーンショットは1ヶ月ほど前のものなので、BTCすごいですねという感じ。古いスクリーンショットを出してきたのは、今の価格で表示したら桁が増えた影響でレイアウトがブチ壊れたからである。

アプリ側は特に面白いことはしていないので、この記事ではサーバーサイドアプリケーションについて解説する。

Vaporアプリケーションの開発

Vaporはオフィシャルに提供されたCLIコマンドがあり、Xcodeのプロジェクトファイルの生成やVapor Cloudへのデプロイ、Vapor CloudのDBインスタンスの管理など、開発フローの中で面倒に感じることの多い様々なことをコマンド一発でできるので便利。公式ドキュメントが充実しているので、詳しくは割愛する。

コントローラの自動生成

先述の通り、クライアントアプリに提供する各エンドポイントのコントローラはジェネレータによって自動生成される。

paths:
  /board/ask:
    get:
      tags:
        - Price
      summary: Latest fetched price information for each exchanges
      description: ""
      operationId: getLatestPriceForEachExchanges
      parameters: []
      responses:
        "200":
          description: successful operation
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: "#/components/schemas/Ask"

このようにSwagger側で定義されたコントローラは、以下のように生成される。

// path: /board/ask
final class Board_AskController {
    typealias GetResponse = ArrayResponse<Ask>

    let drop: Droplet

    init(drop: Droplet) {
        self.drop = drop
    }
}

extension Droplet {
    func setupGeneratedRoutes() throws {
        do {
            let controller = Board_AskController.init(drop: self)
            get("/board/ask", handler: controller.get) // (※)
        }
    }
}

ここで生成されるのは、各エンドポイントごとに1つのコントローラクラスと、Dropletと呼ばれるVaporアプリケーションのコアにルーティングを登録するコードである。賢明なSwiftエンジニアの読者は気づいたかもしれないが、このままでは(※)の行でコンパイルエラーになる。ルーターに対して /board/ask へのリクエストを Board_AskControllerget というメソッドに流すように登録しているが、生成されたコードにはそのようなメソッドは無い。

これがこの仕組みの最も気に入っているところで、Swagger上で定義されたモデル型のレスポンスを返すメソッドをデベロッパーが適切に実装しないとサーバーサイドアプリケーションのコンパイルが通らないのである。すなわち、Swaggerで定義されたエンドポイントを適切に実装していないと、デプロイはおろかコンパイルすら通らないので、あとになって実装漏れが発覚するとか、Swaggerと違う型のオブジェクトが返されてアプリ側が困ることは無い。

コンパイルを通すために、Ask 型のオブジェクトを配列で返す get メソッドを実装する。

extension Board_AskController: GetRequestHandler {
    func get(request: Request) throws -> ArrayResponse<Ask> {
        let exchanges = try Exchange.makeQuery().all()

        let prices = try exchanges.map {
            try Ask.makeQuery().filter("exchange_id", $0.id).all().last
            }
            .flatMap { $0 }

        return ArrayResponse.init(element: prices)
    }
}

これで、「DBにある各取引所の最新の買値データを配列にして返す」というエンドポイントが実装された。Ask の配列以外を返すとコンパイルは通らないので、間違った型のデータを返してしまう心配も無くなった。

ここまでまとめ

  • Swagger駆動開発、今のところ良いです
  • サーバーもクライアントもSwaggerを神ドキュメントとして扱う風習ができれば、業務に投入したい仕組みである。

この先

VASILYのアドベントカレンダーを3枠もらっているので、あと2枠で

  • 各取引所のAPIを叩いてデータを集めるバッチをVapor Cloud上に構築する
  • SwaggerからSwiftのコードを生成するPython製ジェネレータ

の話を書く。お楽しみに。

iOSDC2017参加した

トークの内容などは他の方がたくさん書いてるのでそういうのは割愛して感想だけ。

前回のiOSDCに参加したときはただトークを聞いてふんふんして終わったから帰るか〜という感じで当時はそれで満足していたけど、今年はTwitterで繋がってた人と初めて挨拶できたり僕が以前別の場所でした発表を聞いて覚えててくれた人に声かけられたり@tarunon@omochimetaruにくっついてたらすごい人を連れてきて議論してるのを聞けたり今までつながってなかった人と新しくつながれたり当日適当にメンバーを集めて終了後に焼肉に行くというコミュニティの一員感のあるムーブが出来たりしてよかった。

こういうカンファレンスの一番価値ある部分は上にあげたような対面コミュニケーションだなと感じた。トーク内容はスライドが公開されたり誰かがブログにまとめてたりあとで録画が公開されたりする(ありがたい。2017年最高)ので当日は無理して全部聞こうとするより空き時間にスピーカー捕まえて話聞いてみたり知人とこのコードどう思うみたいな議論をしまくるほうがチケット代と消費した時間がより有益に感じられると思った。

身近(と僕が一方的に思っている)な人はすごい人ばっかだし刺激をもらった。

あと、TwitterのアイコンとIDが明記された名札、あれが最高だった。

Pairsのクラッシュバグをみつけたお


pairs-2017-09-16

項目 情報
アプリバージョン 5.26.0
iOSバージョン 10.3.2
端末 iPhone 6s

操作

  • ユーザー詳細画面で、上方向に素早くスワイプしたあと、写真の上に表示されている右方向のインジケータをタップする
  • 次のユーザーに切り替わるが、スクロール位置的に本来隠れているべきと思われるNavigationBarもどきのViewが見えている
  • 同様の操作を数回(3回の場合が多い)繰り返すと数秒アプリが固まったのちクラッシュする

原因を想像する

わからん

iOSアプリのバックエンドにFirebase使ってみた感想

習作としてレシピのマスターデータがあってそれを作ったよというレポートをアプリから投稿できるようなやつを作ってみて感じたこと。

とりあえず動かすのが超簡単

コンソールからプロジェクトつくってキー発行してSDK入れて初期化すればDBにアクセスできて簡単だった。

考えなしに使うと多分いろいろ破綻する

  • DBがスキーマレスなのでなんでもぽいぽいpostできちゃう
    • DB操作する所は1箇所にまとめて抽象化しないと欠けたオブジェクトがDBに乗っちゃったりして詰みそう。そこはSwiftのカチッとした型と柔らかいスキーマレスDBとの境界で大変というかんじ。考えなしに進めてオブジェクトのプロパティがどんどんOptionalになっていくのは見ていられないので抽象化がんばりましょう
    • 既にデータが刺さってるモデルにプロパティ追加したりしても古いデータは当然マイグレートされないので古いデータが落ちてきてパースに失敗するみたいなこともよく起きそう。
  • DBへの書き込みはアプリ内で直接やらないほうがいいかもしれない
    • アプリのアップデートで書き込みの操作が変わっても古いアプリでDBいじられて壊れるみたいなことが将来ありそう。
    • 使ってないけどFirebase Functions経由でしかwrite操作できないようにすればいいのかしら。そうするとオフラインでも使えますみたいな旨味が無くなっちゃうけど。

オフラインDB、開発中は便利かもしれないけど本番運用で役に立つイメージあんまない

オフラインでデータの読み書きができたところで、画像などは結局ダウンロードできないので「オフラインでもアプリが完全に動作する」を実現するのはそんなに簡単じゃなさそうだなーという印象。 僕は通勤の電車のなかでコード書くのでその時はオフラインDBすごく助かった。

RxSwiftとの相性はいい感じに見えた

DB抽象層としてRxSwiftのObservableをふんだんにつかったRepository層みたいなのを作ったけどデータ更新されたら都度ストリームにながすみたいなことができてリアクティブプログラミングとの相性は良さそうな感じだった。

[2017/09/08追記] ユーザー管理が超楽

iOSアプリのバックエンドにFirebase使ってみた感想 - hiragram no blog

Firebase使ってみた× Firebase Realtime Database使ってみた◯

2017/09/08 09:54
b.hatena.ne.jp

仰る通りなので追記してみます

ユーザー管理、メールアドレスとパスワードとか、Twitter/facebook連携とか、そういうの全部面倒見てくれるのは体験としてすごく良かった。習作アプリは特にSNS連携とかいらなかったので、匿名ログインみたいなやつを使った。ユーザーから情報を得ることなく、端末を識別してくれるので「自分の投稿」をIDで抽出することができたし、トークンの更新みたいなことも全部SDKに丸投げでよかったのでよかった。

総評

お手軽に使えるが、お手軽に使いすぎると容易に死ぬことがわかったので長く保守する大きなアプリをFirebaseだけでやっていくのはなかなか難しいんじゃないかという感じがした。将来Firebaseを剥がすってなったときに死なないようなアプリ側/サービスの設計が必要で、それをちゃんとやろうとするとFirebaseのお手軽さみたいな所をどんどん鈍らせていく感じがしてものを作るというのは難しいなあと思いました。

行動指針

SwiftでiOSアプリを作るにあたっての自分の行動指針をまとめて公開してみた。

というのも、最近転職したり個人でアプリ作ったり友人とのプロジェクトが動き出しそうになったりしており、複数のコーディング規約にまたがってコードを書くことになりそうで(仕事-個人間では既に起きてる)、頭のスイッチコストやそもそもどういうルールだっけみたいなのが問題になりそうなので個人側の規約みたいなものもテキスト化してついでに公開しようと思った次第。

個人プロジェクトは僕一人でやってるのでチームで円滑にやるためのルールみたいなのは考えてないし、そもそも細かい具体的なコーディング規約はないので、迷ったときにはこう判断するのだというのを示す意味で行動指針としてみた。

以下のリポジトリにおいた。

hiragram/my-guidelines

一応思いついただけ普段書いてるコードの規約みたいなのを書き出してみたが言いたいことは冒頭のBasis of thoughtsの部分なのでそこだけ和訳しておいておく。

Humans must not do anything which a compiler can do. A compiler is far more reliable than humans are.

コンパイラができることを人間はやらない。コンパイラは人間よりはるかにずっと信頼できる。

型推論が効くところは型を書かない、省略記法が使える所は省略記法を使う

Do not make extra efforts for tiny readability. The most important thing is that each parson has same understanding, not same appearance of code.

小さな可読性のために余計な手間をかけない。重要なのは誰が読んでも同じ理解をできることで、コードの見た目が同じであることではない。

改行のあるなし、ブレースのあるなし、など。 ifの括弧かかないとかブラケット前後のスペースとかはまあ常識的に。

Take advantage of IDE(Xcode) strongly.

IDE(Xcode)をめちゃ活用する

定義へのジャンプ、型の表示、MARK、など

Do not choose a way that can crash apps on the runtime if there are another options that can prevent crash or detect errors on compilation time.

実行時にクラッシュしうるやり方より、クラッシュしなかったりコンパイル時にエラーチェックできる選択肢があるならそっちを使う。

User Defined Runtime Attributes, Implicitly Unwrapped Optional, Any, など

退職します!

3年2ヶ月在籍したSpeeeを退職します。今日が最終日です。

なにしてたの?

元々サーバーサイドのエンジニアとして新卒入社してPHPやっていて、自社サービスのiOSアプリ用のAPIを作ってました。そのうちアプリ側のちょっとしたデバッグとか修正をXcodeでやるようになり、アプリおもしれーなとなってきたタイミングで社内公用語がPHPからRubyになるとのことで、どうせ未経験から勉強するならRubyよりアプリやりたいなとなり転向させてもらいました。そこからはそのプロジェクトでObjective-Cを書いたり幾つかのプロジェクトでSwiftを書いたりしました。

取り組んだこととしては、

  • Swift1.xから2.0への移行
  • RxSwiftの導入
  • API抽象レイヤーの型々しい設計
  • TravisCI上でCarthageの依存フレームワークをキャッシュしたりCartfile.resolvedの差分をみて更新分だけ再ビルドしたりする仕組みを作って導入
  • typo警察

なんで辞めるの?

アプリチームは半分くらい(以上かな?)が業務委託メンバーで入れ替わりもそこそこあってさみしいなーみたいなことと、Ruby勢のわいわいやっていくぞという雰囲気を羨ましく思っていたことがちょっとモニョっていました。 あと、去年の夏秋くらいにプロジェクトにモニョモニョ〜という感じの状態があったことをきっかけに、漠然とあと1年くらいかな〜くらいな気持ちが湧きました。

あとは直接的な理由があるわけじゃなくて、名前を知ってる会社からオファーを貰って、経営層から現場のエンジニアまで話を聞かせてもらい、いろいろ比較するとこっちのほうがいい!となって辞めることにしたという感じです。9時半に出社しなくていいのと台風の時は自宅作業ができるのすごいという感じ。

次どうするの?

7月から五反田でiOSアプリエンジニアとして働きます。希望すればiOS以外のポジションも挑戦させてもらえそうなので、落ち着いたらKotlinでAndroidをやってみたいと思っています。六本木よりもランチが安いとのことで嬉しいです。かれーの店うどんという気になりすぎる店があるので楽しみです。ホームページがパンクで良かったです。

ほしいものリスト

http://amzn.asia/1otnYYA