hiragram no blog

iOSとか

開発合宿した

仲良しが集まったコミュニティであるところの hiragram.slack.com メンバーで開発合宿をやりました。

メンバーは私, suthio, anoChick, miyachik。場所は湯河原にあるおんやど恵という宿。

出発当日の様子。

f:id:hiragram:20181030131147p:plain

本当は踊り子の中でさっそく酒盛りだ〜❗と思ってたけど寝不足で死ぬと思ったのでカルピスにした。

品川から湯河原まで特急踊り子で1時間くらい。

車窓から。

f:id:hiragram:20181030131224j:plain

12時くらいに着いたら昼ごはん。

お店はここです。

https://tabelog.com/kanagawa/A1410/A141002/14029882/

宿。

私はFirebase+Vue.js+Stripeでクレカ決済付きのwebページみたいなやつを作ってました。

あとはなんかいい感じのご飯を食べたり、

f:id:hiragram:20181030131237j:plain

プロジェクタ借りてマリオパーティしたり、

f:id:hiragram:20181030131303j:plain

wifiが届く足湯で作業したりしました。

施設はすごくきれいだし、温泉も夜通し入れるし、wifi足湯があるし、自由に使える会議室めっちゃ広いし、

という感じです。

作ったものはもうちょっと整えてそのうち公開します。

いつもhiragramを応援してくださる皆様へ

スタートトゥデイテクノロジーズ(旧VASILY)を退職します。今日から有給消化です。
次は決まっていません。8月からiOSらへんの技術者として雇ってもらえる会社を探しています。

最近業務やら個人やらでやったことは以下の通りです

今の所あんまりやりたいと思っていないことは以下の通りです

  • 医療
  • 金融

新しい環境に強く求めることは以下の通りです

  • 有給を使わなくても出社前に猫を病院に連れていけること

出来上がってるチームにあとから入るならめちゃめちゃ凄い人に学ばせてもらえる感じがいいです。スタートアップ博打にも興味はありますという感じです。

もしご興味持っていただける方がおられましたら、お気軽にTwitterまでご連絡ください。

f:id:hiragram:20180702020416j:plain

レギュレーションに則ったエントリを出すのが夢だったのですが、求職エントリになりました。

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のお手軽さみたいな所をどんどん鈍らせていく感じがしてものを作るというのは難しいなあと思いました。