LoginSignup
147
130

More than 5 years have passed since last update.

[Swift] RxSwift + Alamofire + ObjectMapper + Realmの基本的な使い方

Posted at

はじめに

最近、新しいテクノロジーを使う事が増えたのでそれらについてのメモ。
それと実際の使い方&組み合わせた使い方を紹介します。

これらのテクノロジーは単体でのドキュメントは揃ってますが、組み合わせた場合の文献が少し少ない気がしたので僕の体験談を書いておきます。

RxSwift

RxSwiftReactiveProgrammingSwiftで使いやすいように書かれているライブラリー(?)です。
何が良いかというと、以下のような時に簡潔なコードで書く事ができます。
(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ドキュメントが日本語なのでこれを読めば大体わかると思う。
僕の説明よりわかりやすいと思うので、ここでは概要だけ書いておく。

RealmCoreDataに変わるもので、オブジェクトの永続化が簡単にできる事が特徴である。
また、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

147
130
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
147
130