はじめに
最近、新しいテクノロジーを使う事が増えたのでそれらについてのメモ。
それと実際の使い方&組み合わせた使い方を紹介します。
これらのテクノロジーは単体でのドキュメントは揃ってますが、組み合わせた場合の文献が少し少ない気がしたので僕の体験談を書いておきます。
RxSwift
RxSwift
はReactiveProgramming
をSwift
で使いやすいように書かれているライブラリー(?)です。
何が良いかというと、以下のような時に簡潔なコードで書く事ができます。
(RxSwiftを使わなくても出来るのですが、使ったほうがコードが綺麗になります。)
二つ以上のHTTPリクエストを待ち受ける
リクエストに限らないのですが、非同期で2つ以上の処理を行って両方の結果を待ってから処理をする場合に便利です。
//
let intOb1 = PublishSubject<String>()
let intOb2 = PublishSubject<Int>()
_ = Observable.combineLatest(intOb1, intOb2) {
"\($0) \($1)"
}
.subscribe {
print($0)
}
intOb1.on(.Next("A"))
intOb2.on(.Next(1))
intOb1.on(.Next("B"))
intOb2.on(.Next(2))
結果
Next(A 1)
Next(B 1)
Next(B 2)
処理の途中結果を変更できる
後述しますが、Alamofireなどでサーバーから取得したデータを変換したい場合などに便利です。
※URLRequestConvertibleにはリクエスト情報が入っているとする。
let manager = Manager.sharedInstance
let request = rx_request(URLRequestConvertible).flatMap {
rx_responseResult(responseSerializer: Request.ObjectMapperSerializer("user"))
}.flatMap { Observable.just($1) }
request.subscribe(onNext: { (user) in
// これでユーザーオブジェクトとしてデータを取得できている。
print (user)
}, onError: nil)
結果をfilterする場合にも便利
APIに対してはあまり使う事が無いですが、同じ情報を別々の画面に表示するときに便利だったり。
(端末側でユーザー情報をフォローしている人としてない人で分ける場合とか。)
let subscription = Observable.of(0, 1, 2, 3, 4, 5, 6, 7, 8, 9)
.filter {
$0 % 2 == 0
}
.subscribe {
print($0)
}
結果
Next(0)
Next(2)
Next(4)
Next(6)
Next(8)
Completed
RealmのFilterは使用頻度が高いのですが、RxSwiftのFilterはあまり使いません。(僕は)
画面系各種
以下のようなものが便利だったりします。
rx_observeとrx_observeWeakly
これはかなり使用頻度が高いです。
KVO(KeyValueObserve[キー値監視])
をするために使います。
あるオブジェクトの中身が変わったら通知してくれます。
使用用途としては「画面の上部と下部のデータが連動している時」や「今の画面で変更された情報を他の画面に反映させる時」などに役立ちます。
class hogeObject: NSObject {
dynamic var value:String = ""
}
_ = hogeObject.rx_observeWeakly(String.self, "value").subscribeNext({ [weak self] (value) in
// 変更があったので反映する。
self?.label.text = value
})
内部がObjective-c
で書かれているのか、Swiftで使う場合はdynamic
キーワードが必要です。
そのためBool
型の監視はできませんでした。
rx_observe
も同じ動作をしますが、強参照になるためrx_observeWeakly
を使用したほうがいいです。
(String.self, "value")
の部分は監視したいキー名とそのオブジェクト型を指定します。
rx_text
インクリメンタルサーチなどでテキストの中身が変わったことを監視したい場合に使います。
_ = self.textfield.rx_text.debounce(0.7, scheduler: MainScheduler.instance).distinctUntilChanged().subscribeNext{ (text) in
print(text)
}
debounce
は監視するタイミングです。上記の例では0.7
秒毎にチェックします。
distinctUntilChanged
をつければ、変更されていない場合は通知されなくなります。(入力+入力取り消しなどで0.7秒間でデータが変わっていない場合など。)
さらにfilter
を組み合わせれば以下のように書くこともできます。
// 長いので変数に格納させてます。もちろん変数を受け渡すこともできます。
let obs = self.textfield.rx_text.debounce(0.7, scheduler: MainScheduler.instance).distinctUntilChanged()
obs.filter{$0.utf16.count >= 2}.subscribeNext{ (text) in
print(text)
}
こうすることによって2文字以上入力された場合に処理をすることもできます。
Observable.combineLatest
と組み合わせれば、二つ以上の入力が行われた時点で検索を行わせるなどのような事も可能になります。
rx_tap
nibファイルを使い回すと、UIButtonのアクションを@IBAction
に紐付ける必要があります。
それを以下のように簡略化できるので、配置が便利になります。
_ = self.button.rx_tap.subscribeNext {
// タップされた場合の処理
}
Alamofire
APIへの接続が楽にできます。無理に使わなくてもいいのですが、これから作るアプリでは使ったほうが便利です。
基本的にはAPI
やRouterなどのクラスを作成して使うと便利です、
パラメータの作成
今回の例ではパラメータは割愛します。
// このようなプロトコルを用意しておくと便利。
public protocol APIParamsConvertible {
var APIParams: Dictionary<String, AnyObject> { get }
}
class UserParams: APIParamsConvertible {
var id:Int = 0
var APIParams: [String: AnyObject] {
// 本当はここで追加するパラメータを作成する。
// SwiftSerializerなどを使うと便利。
return [String: AnyObject]()
}
}
ルーターの作成
import Alamofire
class Router {
// 基底クラスを作成しておき、拡張させていく。
}
// User用
extension Router {
enum User: URLRequestConvertible {
case Get(Int)
case Patch(UserParams)
var method: Alamofire.Method {
switch self {
case .Get:
return .GET
case .Patch:
return .PATCH
}
}
var path: String {
switch self {
case .Get(let userId):
return "/users/\(userId).json"
case .Patch(let userParams):
return "/users/\(userParams.id).json"
}
}
var URLRequest: NSMutableURLRequest {
// この辺りを基底クラスに定義しておくと楽になる。
let URL = NSURL(string: "http://****/v1/")!
let request = NSMutableURLRequest(URL: URL.URLByAppendingPathComponent(self.path))
request.HTTPMethod = self.method.rawValue
switch self {
case .Get:
return Alamofire.ParameterEncoding.URL.encode(request, parameters: Params().APIParams).0
case .Patch(let userParams):
// URLではなくJSONでパラメータを付与する。
return Alamofire.ParameterEncoding.JSON.encode(request, parameters: Params().APIParams).0
}
}
}
}
リクエスト
実際のリクエストは以下のように行います。
import RxSwift
// 拡張用API
class API {
// 後でmanagerを加工する場合もあるので、共通定義しておく
static let manager = Manager.sharedInstance
}
// 実際は別クラスで管理すると見通しが良くなる。
extension API {
class User {
static func Get(userId: Int) -> Observable<User> {
return API.manager.rx_request(Router.User.Get(userId)).flatMap{
// シリアライズする。(この部分はManagerを拡張すると便利)
// ここの説明は後述するRealmで行います。
manager.rx_responseResult(responseSerializer: Request.ObjectMapperSerializer("user"))
}.flatMap { _, user in
// NSHTTPURLResponseは使わないので、ここで削る。
Observable.just(user)
}
}
static func Update(params: UserParams) -> Observable<AnyObject> {
// 処理結果を使わない場合は、とりあえずjsonで返しておく。(Observableは必ず返す必要があるため。)
return API.manager.rx_request(Router.User.Patch(params)).flatMap {$0.rx_JSON()}
}
}
}
使用例
上記のような構成で作成すると使用する側は以下のようになります。
let userId = 1
API.User.Get(userId).subscribe(onNext: { (user) in
// これでユーザーオブジェクトとしてデータを取得できている。
print (user)
}, onError: nil)
この辺りは本家を見ると良いです。
英語ですがサンプルが多いので、英語が苦手な僕のような人でもある程度理解できます。
Realm
Realm
はドキュメントが日本語なのでこれを読めば大体わかると思う。
僕の説明よりわかりやすいと思うので、ここでは概要だけ書いておく。
Realm
はCoreData
に変わるもので、オブジェクトの永続化が簡単にできる事が特徴である。
また、CoreData
で苦しんだマイグレーションがとても簡単になっているのもポイントである。
便利な処理抜粋
僕がよく使う便利な処理を抜粋します。
書き込み
便利というか、基本的な構文ですが・・・
let realm = try! Realm()
try! realm.write {
realm.add(user, update: true)
}
ユーザーオブジェクトを永続化します。
その際に同一キー(primaryKey
で指定したもの)がある場合は上書きします。
内容変更
Realmでは内容を変更する場合にwriteトランザクションを使う必要があります。
let realm = try! Realm()
try! realm.write {
user.name = "hoge"
}
filterや順番
端末内のデータを取得する
let realm = try! Realm()
realm.objects(User).filter("name == 'hoge'").sorted("createdat").flatMap{$0}
これでUser
オブジェクトの名前がhoge
の人をcreatedat
順に取得できます。
KVO
RxSwift
とは別にKVO
が存在します。
Realm
は同じプライマリーキーのデータを書き換えると、端末内すべてのデータが書き換わるのでデータ一元化がとても楽にできます。
// このオブジェクトがなくなると監視をやめます。(privateで定義する必要があります。)
private var realmToken:NotificationToken?
func settingKVO(userId:Int) {
let realm = try! Realm()
// すでに監視しているものがある場合は解除
self.realmToken?.stop()
self.realmToken = realm.objects(User).filter("id = \(userId)").addNotificationBlock { (user, _) in
// 変更されたユーザー
print(user)
}
}
上記のテクノロジーを組み合わせる
上記のAlamofire
の部分で出てきた以下のコード部分がRealmで実装すると便利な部分だ。
manager.rx_responseResult(responseSerializer: Request.ObjectMapperSerializer("user"))
このケースでは以下のようなJsonをシリアライズできる。
{
"user": {
"id": 1
}
}
これをマッピングさせるRealm
のUserオブジェクトは以下のようになる。
import RxSwift
import RealmSwift
import ObjectMapper
class User: Object, Mappable {
dynamic var id = 0
required convenience init?(_ map: Map) {
self.init()
}
// プライマリーキーをつけておくと、検索が楽にできる。
override static func primaryKey() -> String? {
return "id"
}
func mapping(map: Map) {
// 以下の形式でデータをマッピングしてくれる。
// 割愛するが、TransformOfというクラスも用意されているため型が違うものもマッピングする事ができる。
id <- map["id"]
}
// 後述するが、以下のようにデータを検索して存在しない場合はHTTPリクエストで取りに行かせることもできる。
class func findById(id: Int) -> Observable<User> {
let realm = try! Realm()
return Observable.create { (observable: AnyObserver<User>) in
if let user = realm.objectForPrimaryKey(User, key: id) {
observable.onNext(user)
observable.onCompleted()
return NopDisposable.instance
}
return API.User.Get(id).subscribe(
onNext: { user in
try! realm.write {
realm.add(user, update: true)
}
observable.onNext(user)
observable.onCompleted()
},
onError: { error in
observable.onError(error)
})
}
}
このようにすべての技術を組み合わせると、上記に記載した以下のコードが使えるようになる。
let userId = 1
API.User.Get(userId).subscribe(onNext: { (user) in
print (user)
}, onError: nil)
また、User#findById
のようなものを用意しておくと以下のようにデータを取得できます。
let userId = 1
User.findById(userId).subscribe(onNext: { (user) in
print (user)
}, onError: nil)
これでキャッシュにデータが存在すればキャッシュを使用してくれて、キャッシュになければデータを取得してくれます。
注意
上記のコードはわかりやすくするために、同一クラス名などを使ってます。
そのまま使うとコンパイルエラーが発生しますので、注意してください。
(プロジェクト名.User
などとする必要があります。)
現在のプロジェクトからよく使っているものを抜粋してるので、動かない箇所がスペルミスがあるかもしません。
最後に
この記事が誰かの役に立てれば幸いです。
そして、TownSoftに依頼が増えてくれると僕が幸せになりますw