mitsu's techlog

Customer Reliability Engineerになりました。備忘録も兼ねて技術ネタを適当に。技術以外はこっち→http://mitsu9.hatenablog.com/

SwiftでGoogle Calendarの情報を取得してみた

SwiftからGoogleのCalendar APIを利用してカレンダー情報を取得してみました。 内容は認証の後、取得したカレンダーのデータをコンソールに出力するだけです。

参考サイト

Google Developer iOS Quickstart

事前準備

Google Developer iOS Quickstart のStep1に従ってClient IDを取得

CocoaPodでライブラリをインストール

Podfileに以下を追加し、pod install します。

pod 'Google-API-Client/Calendar', '~> 1.0'

Swiftから利用できるようにBridging-Headerに以下の情報を追加します。

#import "GTMOAuth2ViewControllerTouch.h"
#import "GTLCalendar.h"

カレンダーの情報を取得

import UIKit

class ViewController: UIViewController
{
    private var calendarService: GTLServiceCalendar? = nil
    
    private struct Info {
        static let KeychainItemName: String = "Google Calendar Quickstart"
        static let ClientID: String = "YOUR_CLIENT_ID_HERE"
        static let ClientSecret: String = "YOUR_CLIENT_SECRET_HERE"
    }
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        // 端末に保存されている情報を読み込む
        calendarService = GTLServiceCalendar.init()
        calendarService?.authorizer = GTMOAuth2ViewControllerTouch.authForGoogleFromKeychainForName(
            Info.KeychainItemName,
            clientID: Info.ClientID,
            clientSecret: Info.ClientSecret)
    }
    
    override func viewDidAppear(animated: Bool) {
        super.viewDidAppear(animated)
        
        // 認証がまだなら認証画面を出す. 認証済みならデータを取得
        if let canAuthorize = calendarService?.authorizer.canAuthorize {
            if !canAuthorize {
                self.presentViewController(createAuthController(), animated: true, completion: nil)
            } else {
                fetchEvents()
            }
        }
    }
    
    // 認証画面を作成して返す
    private func createAuthController() -> GTMOAuth2ViewControllerTouch{
        let authController = GTMOAuth2ViewControllerTouch(
            scope: kGTLAuthScopeCalendarReadonly,
            clientID: Info.ClientID,
            clientSecret: Info.ClientSecret,
            keychainItemName: Info.KeychainItemName,
            delegate: self,
            finishedSelector: "viewController:finishedWithAuth:errorOrNil:")
        return authController
    }
    
    // カレンダーのイベントを取得してコンソールに出力
    private func fetchEvents() {
        // クエリの設定.イベントは10個, 時間順にソート
        let query = GTLQueryCalendar.queryForEventsListWithCalendarId("primary") as! GTLQueryCalendar
        query.maxResults = 10
        query.timeMin = GTLDateTime(date: NSDate(), timeZone: NSTimeZone.localTimeZone())
        query.singleEvents = true
        query.orderBy = kGTLCalendarOrderByStartTime
        
        // クエリ実行. 完了後の処理はClosureで渡す
        calendarService?.executeQuery(query, completionHandler:{ (_, events, errorOrNil) -> Void in
            if let error = errorOrNil {
                self.showAlert("Error", message:"Unable to get calendar events.")
            } else {
                var result = ""
                if let items = events.items {
                    if items.count == 0 {
                        result = "No upcoming events found."
                    } else {
                        result = "Upcoming 10 events:\n"
                        for object in items {
                            if let event = object as? GTLCalendarEvent {
                                let start: GTLDateTime = event.start.dateTime ?? event.start.date
                                let startString = NSDateFormatter.localizedStringFromDate(
                                    start.date,
                                    dateStyle: NSDateFormatterStyle.ShortStyle,
                                    timeStyle: NSDateFormatterStyle.ShortStyle)
                                // イベントは開始時間とタイトルを出力
                                result += "\(startString) - \(event.summary)\n"
                            }
                        }
                    }
                } else {
                    result = "events.items() == nil\n"
                }
                // 結果の出力
                println(result)
            }
        })
    }
    
    // 認証後に呼ばれる関数.完了したら認証画面を閉じてイベントの取得.
    func viewController(viewController: GTMOAuth2ViewControllerTouch, finishedWithAuth authResult: GTMOAuth2Authentication, errorOrNil: NSError?) {
        if let error = errorOrNil {
            showAlert("Authentication Error", message: error.localizedDescription)
            calendarService?.authorizer = nil
        } else {
            calendarService?.authorizer = authResult;
            self.dismissViewControllerAnimated(true, completion: nil)
        }
    }
    
    // エラーを表示する
    private func showAlert(title: String, message: String) {
        let alert = UIAlertView(title: title, message: message, delegate: nil, cancelButtonTitle: "OK")
        alert.show()
    }
    
}

感想

SwiftからObjective-cのライブラリを利用するので、Optionalの扱いに気をつけないといけないですねー。

Xcodeはoption(alt)を押して変数をクリックすると型などの情報が表示されるので、それを使いながら確認して進めていきました。

SwiftyJson コードリーディングしてみた

SwiftyJsonをコードリーディングしてみました。

僕はobjective-cは1年近く触っていますが、swiftはまだ全然勉強していないという人です。 複雑な処理がないので初心者でも読みやすく、subscriptやSequenceTypeといった基本的なことがわかるのでオススメです。

SwiftyJsonとは

SwiftyJsonとはswiftjsonを使うときにはほぼデファクトとなっているライブラリです。 以下のような感じで簡単にjsonを使えるので便利です。

let json: JSON = JSON(data: dataFromNetworking)
let user: <String, JSON> = json["user"]. dictionaryValue
let name: String = json["user"]["name"].string

さて、コードリーディングをして気になった部分をメモしていきます。

変数の型のチェック方法

型のチェックはobject:AnyObjectのsetterの中で以下のようにas を利用して書かれていました。

あと、swiftのswitch文ではbreakが必要無いんですね。逆に続けて処理をしたい時はfallthroughと書くらしいです。 switch文の中では似たような方法でlet n where n < 10のように範囲指定したりもできるそうです。タプルも使えるみたいなので、swiftのswitch文の柔軟性は凄いですね。

switch newValue {
case let number as NSNumber:
    if number.isBool {
        _type = .Bool
    } else {
        _type = .Number
    }
case let string as NSString:
    _type = .String
case let null as NSNull:
    _type = .Null
case let array as [AnyObject]:
    _type = .Array
case let dictionary as [String : AnyObject]:
    _type = .Dictionary
default:
    // error handling
}

number.isBoolは以下のようにextensionを書いて実装していました。 swiftではtrue/falseを1/0で表現しているためBoolのtrueかNumberの1か判断できないのですが、 String.fromCString(trueNumber.objCType)を利用することで判断しています。

NSNumberはプリミティブ型のラッパークラスなのでbool, int, long, floatなど様々な値を代入することができ、objCTypeプロパティはその中のどれが代入されているのかを示すものです(多分)。playgroundで確認したところ、Boolならば"c"が返ってきました。

private let trueNumber = NSNumber(bool: true)
private let falseNumber = NSNumber(bool: false)
private let trueObjCType = String.fromCString(trueNumber.objCType)
private let falseObjCType = String.fromCString(falseNumber.objCType)

extension NSNumber: Swift.Comparable {
    var isBool:Bool {
        get {
            let objCType = String.fromCString(self.objCType)
            if (self.compare(trueNumber) == NSComparisonResult.OrderedSame &&  objCType == trueObjCType) ||  (self.compare(falseNumber) == NSComparisonResult.OrderedSame && objCType == falseObjCType){
                return true
            } else {
                return false
            }
        }
    }
}

subscript

subscriptとは[ ]で要素にアクセスするやつです。 SwiftyJsonでは以下のようにpathをarrayとして渡してアクセスすることができます。

let path = [9,"list","person","name"] 
let name = json[path]

パスのArrayの中は、ArrayのときにInt、DictionaryのときにStringを受け取ることができるので、IntとStringにSubscriptTypeプロトコルを追加し、[SubscriptType]を引数に受け取るように実装してありました。

Arrayの中に複数の型が入っているときは[AnyObject]とするのではなく、このように実装すると安全で良いですね。

public protocol SubscriptType {}
extension Int: SubscriptType {}
extension String: SubscriptType {}

extension JSON {
    public subscript(path: [SubscriptType]) -> JSON {
        get {
            if path.count == 0 {
                return JSON.nullJSON
            }
         
            var next = self
            for sub in path {
                next = next[sub:sub]
            }
            return next
        }
        set {
           // setter
        }
    }
}

subscriptの中ではエラーチェックの後、配列で渡されたパスをfor文を用いて一つづつ処理しています。 一つづつの処理はInt(Array)とString(Dictionary)のときで処理が変わるので振り分けるだけのメソッドを用意してあります。

その中の分岐でsub is Stringという構文が使われていました。 switch文の中では case let string as String、if文の中では sub is Stringとすることで型チェックができるみたいです。

private subscript(#sub: SubscriptType) -> JSON {
        get {
            if sub is String {
                return self[key:sub as! String]
            } else {
                return self[index:sub as! Int]
            }
        }
        set {
            // setter
        }
}

SequenceType

SwiftyJsonではjsonがArray/Dictionaryのときにfor..inを使ったループ処理ができます。

for (key: String, subJson: JSON) in json {
   //Do something you want
}

これを実現するためにはSequenceTypeプロトコルを実装する必要があります。 SequenceTypeプロトコルはgenerateメソッドを要求しています。

generateメソッドはGeneratorTypeプロトコルを実装しているGeneratorを返す必要があるのですが、今回はArrayとDictionaryを利用するのでGeneratorTypeについては気にする必要はありません。 もし自作クラスにSequenceTypeを適用するときにはGeneratorTypeも実装する必要があります。

SwiftyJsonではgenerateメソッドは以下のようになっています。 ArrayとDictionaryで処理を分けているだけですね。

GeneratorはGeneratorOfでClosureを使って実装しています。

public func generate() -> GeneratorOf <(String, JSON)> {
    switch self.type {
    case .Array:
        let array_ = object as! [AnyObject]
        var generate_ = array_.generate()
        var index_: Int = 0
        return GeneratorOf<(String, JSON)> {
            if let element_: AnyObject = generate_.next() {
                return ("\(index_++)", JSON(element_))
            } else {
                return nil
            }
        }
    case .Dictionary:
        let dictionary_ = object as! [String : AnyObject]
        var generate_ = dictionary_.generate()
        return GeneratorOf<(String, JSON)> {
            if let (key_: String, value_: AnyObject) = generate_.next() {
                return (key_, JSON(value_))
            } else {
                return nil
            }
        }
    default:
        return GeneratorOf<(String, JSON)> {
            return nil
        }
    }
}

Literal convertibles

Literal convertiblesとは以下のように異なる型でも代入できるようにすることです。 以下の例ではjsonJSON型として宣言されているのでStringは代入できないはずですが、JSONがStringLiteralConvertibleプロトコルを実装しているのでStringを代入することができます。

let json: JSON = "I'm a json"

StringLiteralConvertibleの他にはIntegerLiteralConvertibleやBooleanLiteralConvertibleなどもあります。 これらのプロトコルを実装することで異なる型の代入を可能にしています。

実装が必要なメソッドプロトコルによって異なりますが、基本的には内部で準備をして、initを呼ぶだけです。 SwiftyJsonではIntegerのときはinitを呼ぶだけですが、Dictionaryのときは引数をdictionaryに格納してからinitを読んでいることがわかります。 これは引数をArrayで受け取るため、Dictionaryに変換しています。

extension JSON: Swift.IntegerLiteralConvertible {
    public init(integerLiteral value: IntegerLiteralType) {
        self.init(value)
    }
}

extension JSON: Swift.DictionaryLiteralConvertible {
    public init(dictionaryLiteral elements: (String, AnyObject)...) {
        var dictionary_ = [String : AnyObject]()
        for (key_, value) in elements {
            dictionary_[key_] = value
        }
        self.init(dictionary_)
    }
}