SwiftlySalesforceって知ってますか?

はじめに

みなさん、こんにちは。
走れるシステムエンジニア、溝口です。

いよいよ夏到来ですね。ナンバーガールの水色革命の季節です。
走る方は走りこみの時期に入ってきていますが、今年は補強トレーニングもしっかり入れていきます。
秋のトラックシーズンでは5000m、10000mでそれなりの記録は狙って行きたいです。

作る方は今絶賛売り出し中の弊社のソーシャルウェア、mitocoのモバイルアプリを開発中です。
mitocoブログ も開設いたしましたので、定期的に覗いて頂けると幸いです!

iOSアプリ開発をSaleforceでする場合に使うライブラリって

さてさて、早速ですがみなさん、SalesforceのiOSアプリ開発をする時、どんなライブラリを使っていますか?
もちろん大多数の方はSalesforce Mobile SDKで実装したり、SOAP APIを使いたい方はzkSforceを使っているかと思います。

それか、「ありもののSDKなんて使ってられるか!」と、AFNetworkingやAlamofireを使って独自に実装している方も居るかもしれません。

でも、もし、Salesforce Mobile SDKよりも手軽に、モダンに、シンプルに実装出来るようなライブラリがあったら良いと思いませんか?

その「もし」を実現するライブラリが実はあるんです!

SwiftlySalesforceって?

そんな素敵なライブラリがこちらです!
https://github.com/mike4aday/SwiftlySalesforce

Salesforce社のエバンジェリストであるMichael Epsteinさんがホストしているライブラリで、Swift謹製、かつシンプル、モダンなSalesforceライブラリとなっています。

このライブラリの何が良いかと言うと

  • 通信部分はAlamofire、処理部分にPromiseKitを使っているので、非同期処理が非常に簡単に書ける
  • キーチェーン共有でLockSmithを使っていて、トークンの共有が出来る
  • Swift謹製で、実装もSwiftの良さが非常によく出ていて見やすい
  • ライブラリそのものの容量が軽い

特にキーチェーンの部分はSalesforce Mobile SDKでは難しかった部分なので、嬉しい機能では無いでしょうか?

これがどういう事かというと、OAuthのコールバックURIが同じアプリに関しては、OAuthの認証情報が共有されるので、複数アプリを同じURIで実装した場合、一度のログイン認証情報を複数のアプリで使い回すことが出来ます。

例えばSwiftly Salesforceで同一コールバックURIを使うAアプリ、Bアプリがあった場合、Aアプリでログインを実施した場合、Bアプリを初めて開く場合でも、ログインをスキップすることが出来るようになります。

すごい!!

手軽に、モダンに、シンプルに

さて、僕は冒頭で「Salesforce Mobile SDKよりも手軽に、モダンに、シンプルに」という風に書きました。
じゃあ、どれだけ良くなるのよ?という風に思っているあなた!

実際にどれだけ実装が変わるのか、簡単なアプリの実装を踏まえて解説していきたいと思います!
今回は取引先の一覧を取得して表示するだけのアプリを作ってみたので、見ていきましょう。


まず、始めにAppDelegateはどうなっているのでしょうか。

Salesforce Mobile SDKでは色々な処理をあらかじめ記述しておかなければいけなくて、見るのもうんざり・・・となっていた人も居るかと思います。そんなAppDelegateの実装がこうなります!

import UIKit
import SwiftlySalesforce
import PromiseKit

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate, LoginViewPresentable {

    var window: UIWindow?
    let RemoteAccessConsumerKey = "3MVG9Iu66FKeHhINkB1l7xt7kR8czFcCTUhgoA8Ol2Ltf1eYHOU4SqQRSEitYFDUpqRWcoQ2.dBv_a1Dyu5xa"
    let OAuthRedirectURI = NSURL(string: "testsfdc:///mobilesdk/detect/oauth/done")!


    func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
        self.window = UIWindow(frame: UIScreen.mainScreen().bounds)
        self.window?.rootViewController = UINavigationController(rootViewController: AccountTableViewController())
        self.window?.makeKeyAndVisible()
        
        OAuth2Manager.sharedInstance.configureWithConsumerKey(self.RemoteAccessConsumerKey, redirectURL: self.OAuthRedirectURI)
        OAuth2Manager.sharedInstance.authenticationDelegate = self
        OAuth2Manager.sharedInstance.authorize()
        
        return true
    }

    func application(application: UIApplication, openURL url: NSURL, sourceApplication: String?, annotation: AnyObject) -> Bool {
        handleRedirectURL(url)
        return true
    }


}

(つд⊂)ゴシゴシ
 
(;゚д゚) ・・・
 
(つд⊂)ゴシゴシゴシ
  _, ._
(;゚ Д゚) ...!?

これだけ!?と思った方も居るかもしれません。そう、これだけです!
これだけでログインやログアウト周りの処理が全て実装されています。

ちなみにauthorize()メソッドはPromiseで実装されているので、.thenでログイン完了後の処理、エラー時の処理を簡単に追加出来ます。


そしてメインの一覧画面表示部分、AccountTableViewControllerと、AccountManagerを見ていきましょう。

import UIKit
import SwiftlySalesforce
import PromiseKit

class AccountTableViewController: UITableViewController {
    
    override func viewDidLoad() {
        super.viewDidLoad()
        self.loadData()
    }

    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
    }
    
    func loadData() {
        AccountManager.sharedInstance.getAccounts().then {
            [weak self] result -> () in
            self?.tableView.reloadData()
        }.error {
            error -> () in
            let alert = UIAlertController(title: "Error", message: "\(error)", preferredStyle: UIAlertControllerStyle.Alert)
            alert.addAction(UIAlertAction(title: "Close", style: UIAlertActionStyle.Default, handler: nil))
            self.presentViewController(alert, animated: true, completion: nil)
        }
    }
    
    override func numberOfSectionsInTableView(tableView: UITableView) -> Int {
        return 1
    }
    
    override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return AccountManager.sharedInstance.accounts?.count ?? 0
    }
    
    override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
        let cell: UITableViewCell = UITableViewCell(style: UITableViewCellStyle.Subtitle, reuseIdentifier: "accountCell")
        cell.textLabel?.text = AccountManager.sharedInstance.accounts![indexPath.row].name
        return cell
    }
}

import Foundation
import SwiftlySalesforce
import PromiseKit

public final class AccountManager: NSObject {
    
    public static let sharedInstance = AccountManager()
    public private(set) var accounts: [Account]?
    
    
   public func getAccounts() -> Promise<[Account]> {
        return Promise<[Account]> { fulfill, reject in
            firstly {
                () -> Promise<AnyObject> in
                UIApplication.sharedApplication().networkActivityIndicatorVisible = true
                let queryString = "SELECT Id,Name FROM Account LIMIT 100"
                return SalesforceAPI.Query(soql: queryString).request()
            }.then {
                result -> () in
                UIApplication.sharedApplication().networkActivityIndicatorVisible = false
                guard let records = result["records"] as? [[String: AnyObject]] else {
                    throw NSError(domain: "AccountGet", code: -101, userInfo: nil)
                }
                let accounts = records.map { Account(dictionary: $0) }
                self.accounts = accounts
                fulfill(accounts)
            }.error {
                error -> () in
                UIApplication.sharedApplication().networkActivityIndicatorVisible = false
                reject(error)
            }
        }
    }
}

public final class Account {
    public var salesforceId: String?
    public var name: String?
    
    public init(dictionary: [String: AnyObject]) {
        for (key, value) in dictionary {
            switch key.lowercaseString {
            case "id":
                self.salesforceId = value as? String
            case "name":
                self.name = value as? String
            default:
                continue
            }
        }
    }
}

これを見てお気づきの方も居るでしょう、そう、ViewControllerとビジネスロジックが綺麗に分けられているではありませんか!!

Salesforce Mobile SDKでクエリのコールバック処理を含む諸々の実装をViewControllerから切り離す場合、NSNotificationCenterでメッセージをサブスクライブして、非同期処理が終わったらメッセージを受け取ってTableViewをリロードして・・・、みたいな処理を書いていたかと思います。

サブスクライブしたメッセージをアンサブスクライブし忘れてメモリリークしちゃう、とか、全てAnyObjectでメッセージの受け渡しがなされるので、タイプエラーで実行時に落ちてしまったり・・・とか辛い思い出が蘇るのでは無いでしょうか?(それでもReactive Extension系のライブラリを使えば多少マシになるんですが・・・)

それがSwiftlySalesforceではこんなにも綺麗に書けてしまう訳です!ちょっと感動ですね。



ちなみに、一応アプリを実行した画面を貼っておきます。

(これは別に貼らなくても良かったかな・・・)

まとめ

Salesforce Mobile SDKはSalesforce関連のライブラリが沢山用意されていたり、オフライン対応用にSmartStoreやSmartSyncの様な仕組みが用意されていて、これらを使う分には全然選択肢としては有りだと思います。

ですが、最低限の通信部分のライブラリが欲しい、という方や、内部DBはRealmみたいなORマッパーを使うので、SmartSoreは必要無いよという方、「コールバックを綺麗に書きたい!!」という方には、SwiftlySalesforceはうってつけのライブラリでは無いでしょうか?

こうやってSalesforceのiOS開発で使える選択肢が増えていくのは非常に良いと思います!

お知らせ

僕のインタビュー記事をBCN様のFaceというインタビューコーナーに掲載して頂きました!
http://biz.bcnranking.jp/article/interview/face/1606/160623_142533.html
走るエンジニア、絶賛募集中です!


あと、実はテラスカイは自社イベントを今月22日に実施します!
Salesforce関連の深い話やぶっちゃけトーク、AWSのセッションもあったりして、僕が言うのも何ですが、非常に面白そうなイベントになっています。

気になった方は是非ご来場下さい!

詳細・お申込みはこちら!