はじめましてiOSエンジニアのしだです。
おかげさまで、去年12月に るくみーnote という園で利用してもらう連絡帳アプリをリリースしました。(実際に園でご利用いただくのは4月からの予定です)
iOS版の るくみーnote では主に Alamofire
、 ObjectMapper
、RxSwift
、 SQLite.swift
などのライブラリを利用しています。
今回は、そのときに実践した Alamofire + ObjectMapper + RxSwift
を使ったAPIクライアント周りの実装方法を紹介したいと思います。
以下のサンプルの実装は Slack WebAPI を使って説明しています。
準備
- Xcode 8.2(Swift 3)
- Alamofire 4.2.0
- ObjectMapper 2.2.1
- RxSwift 3.0.1、 RxCocoa 3.0.1
- Slack Web API
APIの定義
今回は SlackのWeb APIのemoji.list
、chat.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])
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 {
var buildURL: URL { get }
var baseURL: String { get }
var path: String { get }
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クライアントの定義
最後に、 Alamofire
、 RxSwfit
、 ObjectMapper
を使って、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()
}
}
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())
}
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())
}
static func mapping<T: Mappable>() -> ((Any) -> T) {
return { (json) -> T in
let item: T = Mapper<T>().map(JSONObject: json)!
return item
}
}
static func mappingToArray<T: Mappable>() -> ((Any) -> [T]) {
return { (json) -> [T] in
let item: [T] = Mapper<T>().mapArray(JSONObject: json) ?? []
return item
}
}
}
Genericsを使って、 ObjectMapper
でマッピングしたオブジェクトを返す get
や post
メソッドを用意しておくと、使うときにClient.get(api: SlackApi.emojiList)
と書けるので可読性がいいかなと考えます。
また、レスポンスがJSON形式であれば、 Clientクラスはそのまま流用可能です。
iOS版 るくみーnoteの実装も、認証処理や put
・ patch
が追加されてるなどの違いはありますがほとんど一緒です。
あと、ちょっと無駄のようにも思えますが、 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
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アプリ開発の参考になれば幸いです。