ユニファ開発者ブログ

ユニファ株式会社プロダクトデベロップメント本部メンバーによるブログです。

Swiftを使ったAPIクライアントの実装方法

はじめましてiOSエンジニアのしだです。
おかげさまで、去年12月に るくみーnote という園で利用してもらう連絡帳アプリをリリースしました。(実際に園でご利用いただくのは4月からの予定です)

iOS版の るくみーnote では主に AlamofireObjectMapperRxSwiftSQLite.swift などのライブラリを利用しています。
今回は、そのときに実践した Alamofire + ObjectMapper + RxSwift を使ったAPIクライアント周りの実装方法を紹介したいと思います。

以下のサンプルの実装は Slack WebAPI を使って説明しています。

準備

APIの定義

今回は SlackのWeb APIのemoji.listchat.postMessageの実装例を示します。

  • emoji.list: 設定されている絵文字のリストが取得できます
  • chat.postMessage: 指定したchannelにメッセージを送信できます

まず Slack Web APIのmethodsを Enumeration を使って以下のように定義します。

enum SlackApi: API {

    /// 絵文字リストの取得
    case emojiList

    /// メッセージを送信
    case chatPostMessage(text: String, channel: String, options: [String: String])

    /// :
    /// ファイルのアップロード
    /// case fileUpload(filename: String, file: Data, options: [String: String])

    /// 任意のSlackのTest Tokenを利用してください
    static let token = “xoxp-xxx"

    var buildURL: URL {
        return URL(string: "\(baseURL)\(path)")!
    }

    var baseURL: String {
        return "https://slack.com/api"
    }

    var path: String {
        switch self {
        case .emojiList:                                        return "/emoji.list"
        case .chatPostMessage(text: _, channel: _, options: _): return "/chat.postMessage"
        }
    }

    var parameters: Parameters {
        var params = ["token": SlackApi.token]
        switch self {
        case .emojiList:
            return params
        case .chatPostMessage(text: let text, channel: let channel, options: let options):
            params["text"] = text
            params["channel"] = channel
            options.forEach { (k, v) in params[k] = v }
            return params
        }
    }
}

protocol API {
    /// リクエストURL(e.g. https://slack.com/api/emoji.list)
    var buildURL: URL { get }

    /// APIのベースURL(e.g. https://slack.com/api)
    var baseURL: String { get }

    /// APIのパス(e.g. /emoji.list)
    var path: String { get }

    /// リクエストのパラメーター(e.g. token=xoxp-xxx)
    var parameters: [String: Any] { get }
}

ポイントとしては、各APIを列挙型の要素で定義することと、 Associated Values を使ってリクエストに必要なパラメータがわかるようにしてあります。
欠点としては、APIの数が多くなれば、 enum SlackApi のコード量が多くなりますがAPIを一覧できるほうが利点かなと思います。

モデルの定義

続いて、レスポンスのJSON形式からオブジェクトに変換するために、 ObjectMapper を使ってモデルの定義をします。

/// 絵文字
struct Emoji: Mappable {
    var ok: Bool = false
    var list: [String: String] = [:]

    init?(map: Map) { }

    mutating func mapping(map: Map) {
        ok   <- map["ok"]
        list <- map["emoji"]
    }
}

/// メッセージ送信レスポンス
struct PostMessage: Mappable {
    var ok: Bool = false
    var ts: String = ""
    var channel: String = ""
    var message: AnyObject?

    init?(map: Map) { }

    mutating func mapping(map: Map) {
        ok      <- map["ok"]
        ts      <- map["ts"]
        channel <- map["channel"]
        message <- map["message"]
    }
}

HTTPクライアントの定義

最後に、 AlamofireRxSwfitObjectMapper を使って、HTTPのクライアントを定義します。

import Alamofire
import ObjectMapper
import RxSwift

struct Client {
    static let manager = Alamofire.SessionManager.default
    
    private static func json(method: HTTPMethod = .get, api: API, encoding: ParameterEncoding = URLEncoding.default, headers: [String: String]? = nil) -> Observable<Any> {
        return dataRequest(method: method, api: api, encoding: encoding, headers: headers)
            .flatMap { (request) -> Observable<Any> in
                return manager.rx.json(request: request)
        }
    }
    
    private static func dataRequest(method: HTTPMethod = .get, api: API, encoding: ParameterEncoding = URLEncoding.default, headers: [String: String]? = nil) -> Observable<DataRequest> {
        return Observable<DataRequest>
            .create { (observer) -> Disposable in
                let url = api.buildURL
                let request = manager.request(url, method: method, parameters: api.parameters, encoding: encoding, headers: headers)
                observer.onNext(request)
                observer.onCompleted()
                return Disposables.create()
        }
    }
    
    /// レスポンスのJSON形式がDictionary(JSONObject)の場合
    static func get<T: Mappable>(api: API, encoding: ParameterEncoding = URLEncoding.default, headers: [String: String]? = nil) -> Observable<T> {
        return Client.json(method: .get, api: api, encoding: encoding, headers: headers).map(mapping())
    }
    
    /// レスポンスのJSON形式がArray(JSONArray)の場合
    static func get<T: Mappable>(api: API, encoding: ParameterEncoding = URLEncoding.default, headers: [String: String]? = nil) -> Observable<[T]> {
        return Client.json(method: .get, api: api, encoding: encoding, headers: headers).map(mappingToArray())
    }
    
    static func post<T: Mappable>(api: API, encoding: ParameterEncoding = URLEncoding.default, headers: [String: String]? = nil) -> Observable<T> {
        return Client.json(method: .post, api: api, encoding: encoding, headers: headers).map(mapping())
    }
    
    /// JSONからオブジェクトへのマッピング
    static func mapping<T: Mappable>() -> ((Any) -> T) {
        return { (json) -> T in
            let item: T = Mapper<T>().map(JSONObject: json)!
            return item
        }
    }
    
    /// JSONからArrayタイプのオブジェクトへマッピング
    static func mappingToArray<T: Mappable>() -> ((Any) -> [T]) {
        return { (json) -> [T] in
            let item: [T] = Mapper<T>().mapArray(JSONObject: json) ?? []
            return item
        }
    }
}

Genericsを使って、 ObjectMapper でマッピングしたオブジェクトを返す getpost メソッドを用意しておくと、使うときにClient.get(api: SlackApi.emojiList)と書けるので可読性がいいかなと考えます。

また、レスポンスがJSON形式であれば、 Clientクラスはそのまま流用可能です。
iOS版 るくみーnoteの実装も、認証処理や putpatch が追加されてるなどの違いはありますがほとんど一緒です。

あと、ちょっと無駄のようにも思えますが、 Observable<DateRequest> を返すメソッドも用意しておくと、アクセストークンの有効期限切れなどリトライする場合に役に立ちます。

使い方

絵文字リストの取得: emoji.list

/// 絵文字のリストを取得する
let request: Observable<Emoji> = Client.get(api: SlackApi.emojiList)
request.subscribe(
    onNext: { (emoji) in
        print(emoji)
    },
    onError: { (error) in
        print(error)
    })
    .addDisposableTo(disposeBag)

メッセージの送信: chat.postMessage

/// #generalにメッセージを送信する
let api = SlackApi.chatPostMessage(
    text: "てすと",
    channel: "general",
    options: ["username": "Bot", "icon_emoji": ":robot_face:"]
)
let request: Observable<PostMessage> = Client.post(api: api)
request.subscribe(
    onNext: { (message) in
        print(message)
    },
    onError: { (error) in
        print(error)
    })
    .addDisposableTo(disposeBag)

まとめ

今回はSlack Web APIを例に、iOSのAPI周りの実装方法をこんな感じで書いてますという紹介でした。
(コールバックを何個も用意したり、NSURLSessionを自前で書いたり、JSONからオブジェクトへ変換するのにif letをたくさん書いたり、、なにもかもみな懐かしいです。)

僕個人もはじめて Alamofire + ObjectMapper + RxSwift をプロダクトで使ってみましたがとてもよかったです。
さらにSQLite.swiftも一緒に使いましたので今後そのあたりも共有できたらいいなと思います。

iOSアプリ開発の参考になれば幸いです。

Ruby biz Grand prix 2016

あけましておめでとうございます。ユニファの田渕です。

みなさま、年末年始は十分に楽しめたでしょうか?
今年は曜日の並び上、超大型休暇にはなかなかしづらく、弊社でも昨年の年末を思い出しため息を吐く人が多数……。
と言っても、色々と話を聞いていると、エンジニアは休暇中も自分の興味のある技術や言語を試した方が多いようです。

ユニファは昨年、大変有難いことに、色々な賞を頂きました。
その中でも、エンジニアとして印象深かったRuby biz Grand prix 2016について本日は書きたいと思います。

Ruby biz Grand prix 2016とは?

とても簡単に言うと

  • Rubyを使っている企業が各社のサービスや技術などをアピール

  • エントリー企業の中から、大賞や特別賞などを選考(うまくいけば賞が貰える!)

というものです。 詳細については、下記の公式ページを参照ください。

一つ前の記事でも話が出たように、弊社はRubyを利用しています。
せっかくだから応募してみようか?
(あわよくば、まつもと ゆきひろさんに会えるかも)
ということで、エントリー資料を書いたのが昨年の夏のことでした。

華々しい表彰式

Ruby biz Grand prixの受賞企業は、事務局より表彰式のご案内が届きます。
この度、ありがたくも、ご連絡を頂いた弊社。
この時点では何の賞を頂くのかはわかっておらず、当日発表される形です。
普段、華々しい席にはエンジニアはあまり赴かないのですが、(そういうの苦手な人エンジニアに多いですよね) 今回はRubyの賞であることもあり、エンジニア勢で出席することに。
そうして迎えた当日……。
f:id:unifa_tech:20170105144411j:plain
会場に着いた途端に我々を出迎えた、とっても立派な看板……。
もちろん、立派なのは、看板だけではありませんでした。
ライトに煌々と照らされた眩しいステージ、プレゼン用の巨大スクリーン……ちょっと色々気後れします。
若干緊張気味のCTOをステージ前の待機席に送りだし、我々は受賞企業用の席で落ち着かないまま待っておりました。

結果は??

弊社の名前が呼ばれたのは「審査員特別賞」の発表の時でした。

審査員特別賞は3点あり、弊社は三つ目に名前を呼ばれました。
実はここで呼ばれなければ残るは「大賞」だけだったので、お、もしや!?と思っていたのですが……。

とはいえ、「審査員特別賞」でも十分すぎる結果です!
賞状とトロフィー(るくみーロゴ入り!)、副賞の30万円を頂きました!

受賞と同時に、携帯からSlackで、オフィスに居る皆さんに実況。
Slack上では大いに盛り上がりました。

それだけでは終わらない

若干の緊張感のある表彰式、フォトセッション(!)の後、我々を待っていたのは、事務局が用意してくださった豪華なお昼でした!
立食形式で、他の受賞者の皆様と交流を深めつつ、お肉やデザートを頂きました。
こういう場に出て、他の会社の方々とお話をさせて頂くと、いろいろと発見があります。
業界によっての考え方や文化の違い、 かと思うと、まったく遠いと思っていた業界の事例が実は参考にできそうだな、とか 広い様で狭い、狭い様で広いこの世界を改めて実感します。

なんて思いながら、CTOと二人、話していたら、側をまつもと ゆきひろさんが通っていくではありませんか!
二人揃って、慌ててご挨拶に伺います。
こういう場ですので、長くお話は出来ませんでしたが、ありがたいお言葉を頂き、感無量。

エンジニアとしてそれなりの期間を過ごしていても、外部のこういった賞を頂く機会というのはなかなかありませんので、大変貴重な経験をさせて頂いたと思っております。

事務局の皆様、ご協力頂いた皆様、本当に有難う御座いました。

システム開発部の体制の紹介

f:id:akanuma-hiroaki:20161218150539p:plain

 こんにちは、ユニファ株式会社CTOの赤沼です。ちょっと前に2016年になったと思ったのに、もう今年も終わってしまいますね。秋葉原オフィス周辺も今はすっかりクリスマスムードですが、25日が過ぎると一気にお正月モードに変わるんでしょうね。

 さて、本日からユニファ開発者ブログをスタートします。システム開発部のメンバーが持ち回りで、弊社サービス開発に関係する技術的な内容や、それぞれ興味を持っている技術的なトピック、開発プロセス、開発体制、社内の文化などについてゆるく書いていきます。どんなメンバーがどんな風に開発しているのかを知ってもらう場になればと思っています。

 今回は初回ということで、ユニファの開発体制について簡単に紹介させていただこうと思います。

メンバー構成

 2016年12月現在、ユニファ全体での正社員数は(グループウェアの名簿を数えたところ)32名、パートタイムのメンバーも含めると40名をこえるぐらいという規模です。そのうちシステム開発部メンバーは正社員が15名で、それ以外にフリーランスエンジニアの方数名にパートタイムでお手伝いいただいています。

 開発部内でのメンバー構成は下記のようになっています。

  • Webエンジニア:6名
  • スマートフォンアプリエンジニア:3名
  • デザイナー:2名(1名育休中)
  • QA:2名
  • インフラエンジニア:1名
  • フリーランスのパートタイムメンバー:2〜3名

 この一年ぐらいで人数は倍ぐらいに増えた感じですね。個人的にはスタートアップの中でもQAがいる会社は少ないのではないかと思っています。年齢層としては30代前半が多く、以前は大企業で仕事をしていたり、受託開発中心に仕事をしていたり、様々な経験を持つメンバーが集まっています。ユニファに入るために上海から日本に来てくれた中国人エンジニアもいたりします。

拠点

 弊社は2013年に名古屋で創業したので、本社は名古屋なのですが、2015年2月に東京オフィスを設立し、現在は東京オフィスを中心に開発を行っています。開発部に限らず拠点間でミーティングをするときはSkypeを活用しています。また、開発部では自宅勤務も取り入れているので、その日オフィスにいないメンバーとミーティングをするときもSkypeを利用しています。リモート勤務については他のメンバーが詳しく書いてくれるのではないかと思うので、詳細はそちらに譲ります。

言語・フレームワーク・インフラ

 Webアプリでは基本的にはRuby/Railsで開発しています。最近開発したサービスでは最新のバージョンのRuby/Railsを使用していますが、初期の頃にリリースしたサービスでは結構古いバージョンを使っているので、バージョンアップしたいと思っていますがなかなか実施できていません。

 スマートフォンアプリは、 iOSアプリはSwift、AndroidアプリはJavaで開発していまして、それぞれ Xcode、 Android Studioを使って開発しています。

 インフラは全てAWS上に構成していて、主にインフラエンジニアが面倒を見ています。AWSはWebエンジニアでも扱いやすい部分もあるので、開発初期の段階ではWebエンジニアが直接AWS環境で色々と試してみたりもしています。

開発ツール

 開発プロセスの詳細を書いていると長くなってしまうので、今回は使っているツールだけひとまずご紹介しておきます。実際にどのように活用してどんな流れで開発しているかはまた別の機会に。

 社内のコミュニケーションは主にSlackを使用しています。タスク管理と情報共有にはJIRA/Confluenceを使用し、ソースコードのバージョン管理はGitで、Bitbucketのプライベートリポジトリで運用しています。

 CIツールとしてはWerckerを使用していて、Bitbucketにpushすると自動的にテストを実行し、結果をSlackに通知しています。

 開発環境の構築にはVagrantやDockerを使い、サービス開発初期のプロトタイピングにはProttを利用しています。

 また、簡易な脆弱性チェックツールとしてVAddyを利用していて、日次で開発環境で稼働しているサービスに対して脆弱性チェックをかけています。

ビジネスモデル・サービス領域

 弊社のサービスは主に保育園や幼稚園に導入していただき、その園に通われている園児の保護者の方に使っていただくサービスになりますので、ビジネスモデルとしてはB2B2Cになります。対象の業界としては主に保育業界ということになります。私もユニファに入ってから色々と知ったのですが、一般的なB2BやB2Cのサービスとはまた違う、保育業界ならではの気をつけないといけなことも多いので、業界をよく知るビジネスサイドのメンバーとサービス内容を検討したり、現場の保育士の方にヒアリングをさせていただいたりしています。

 ということで、開発部について簡単に紹介させていただきました。それぞれ詳細についてはまた改めてご紹介させていただこうと思いますので、ユニファ開発者ブログをよろしくお願いします。