从iOS应用访问Google日历


这次,我想通过Google Calenear API从iOS应用访问Google日历。

环境:Xcode 12.0,Swift 5

准备

首先,转到Google Cloud Platform并启用Google Calendar API。
接下来,获取用于OAuth身份验证的OAuth客户端ID。
请按照以下文章中的"注册客户端ID"中所述的过程来申请客户端ID。

我尝试使用Swift 4击中Google Calendar API

使用的外部库

Google API本身是REST API,但是很难直接调用它,因此我将使用一个外部库。
这次,我们将使用以下库。

?Google身份验证
AppAuth
GTMAppAuth

?使用Google日历访问
GoogleAPIClientForREST /日历

以上所有库都可以与CocoaPods一起安装。
如下所示编写Podfile并执行pod install进行安装。

1
2
3
4
5
6
7
8
9
10
platform :ios, '14.0'

target 'GoogleCalendarSample' do
  use_frameworks!

  pod 'AppAuth'
  pod 'GTMAppAuth'
  pod 'GoogleAPIClientForREST/Calendar'

end

验证

实现使用AppAuth和GTMAppAuth执行Google身份验证的过程。

首先,将OIDExternalUserAgentSession类添加到AppDelegate。

AppDelegate.swift

1
2
3
4
5
6
7
8
9
import UIKit
import AppAuth
import GTMAppAuth

@main
class AppDelegate: UIResponder, UIApplicationDelegate {
    var currentAuthorizationFlow: OIDExternalUserAgentSession?

---------------- (以下略) ----------------

然后在屏幕上描述用于Google身份验证的以下过程。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
import UIKit
import AppAuth
import GTMAppAuth
import GoogleAPIClientForREST

---------------- (中略) ----------------

private var authorization: GTMAppAuthFetcherAuthorization?
private let clientID = "xxxxxxxxxxxxxxxxxxxx"
private let reverseClientID = "xxxxxxxxxxxxxxxxxxxx"
typealias showAuthorizationDialogCallBack = ((Error?) -> Void)

private func showAuthorizationDialog(callBack: @escaping showAuthorizationDialogCallBack) {
    let scopes = ["https://www.googleapis.com/auth/calendar","https://www.googleapis.com/auth/calendar.readonly","https://www.googleapis.com/auth/calendar.events","https://www.googleapis.com/auth/calendar.events.readonly"]

    let configuration = GTMAppAuthFetcherAuthorization.configurationForGoogle()
    let redirectURL = URL.init(string: reverseClientID + ":/oauthredirect")

    let request = OIDAuthorizationRequest.init(configuration: configuration,
                                                   clientId: clientID,
                                                   scopes: scopes,
                                                   redirectURL: redirectURL!,
                                                   responseType: OIDResponseTypeCode,
                                                   additionalParameters: nil)

    let appDelegate: AppDelegate = UIApplication.shared.delegate as! AppDelegate
    appDelegate.currentAuthorizationFlow = OIDAuthState.authState(
        byPresenting: request,
        presenting: self,
        callback: { (authState, error) in
            if let error = error {
                NSLog("\(error)")
            } else {
                if let authState = authState {
                    self.authorization = GTMAppAuthFetcherAuthorization.init(authState: authState)
                    GTMAppAuthFetcherAuthorization.save(self.authorization!, toKeychainForName: "authorization")
                }
            }
            callBack(error)
    })
}

以下变量描述了获得OAuth客户端ID时获得的ID。
在clientID中输入OAuth 2.0客户端ID,在reverseClientID中输入反向客户端ID。

1
2
private let clientID = "xxxxxxxxxxxxxxxxxxxx"
private let reverseClientID = "xxxxxxxxxxxxxxxxxxxx"

这次将所需权限设置为数组作用域。这次,我们正在请求搜索和更改Google日历的权限。

1
let scopes = ["https://www.googleapis.com/auth/calendar","https://www.googleapis.com/auth/calendar.readonly","https://www.googleapis.com/auth/calendar.events","https://www.googleapis.com/auth/calendar.events.readonly"]

当执行OIDAuthState类的authState方法时,将显示以下Google身份验证对话框。
Google認証ダイアログ
如果用户在对话框中正确输入了gmail地址和密码,并且身份验证完成,则authState方法的回调函数将生成并保存GTMAppAuthFetcherAuthorization类。
在保留此GTMAppAuthFetcherAuthorization类的同时,无需重新显示身份验证对话框。

搜索事件

中,我想继续使用GoogleAPIClientForREST访问Google日历。
首先,我将描述从Google日历获取现有事件的过程。
如果将开始日期和时间以及结束日期和时间传递给get方法,则该程序会从Google日历中搜索开始日期和时间以及结束日期和时间之间的事件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
import UIKit
import AppAuth
import GTMAppAuth
import GoogleAPIClientForREST

---------------- (中略) ----------------

private var authorization: GTMAppAuthFetcherAuthorization?
private let clientID = "xxxxxxxxxxxxxxxxxxxx"
private let reverseClientID = "xxxxxxxxxxxxxxxxxxxx"
typealias showAuthorizationDialogCallBack = ((Error?) -> Void)
struct GoogleCalendaraEvent {
    var id: String
    var name: String
    var startDate: Date?
    var endDate: Date?
}
private var googleCalendarEventList: [GoogleCalendaraEvent] = []

private func showAuthorizationDialog(callBack: @escaping showAuthorizationDialogCallBack) {
---------------- (中略) ----------------
}

private func get(startDateTime: Date, endDateTime: Date) {
    if GTMAppAuthFetcherAuthorization(fromKeychainForName: "authorization") != nil {
        self.authorization = GTMAppAuthFetcherAuthorization(fromKeychainForName: "authorization")!
    }

    if self.authorization == nil {
        showAuthorizationDialog(callBack: {(error) -> Void in
            if error == nil {
                self.getCalendarEvents(startDateTime: startDateTime, endDateTime: endDateTime)
            }
        })
    } else {
        self.getCalendarEvents(startDateTime: startDateTime, endDateTime: endDateTime)
    }
}

private func getCalendarEvents(startDateTime: Date, endDateTime: Date) {
    let calendarService = GTLRCalendarService()
    calendarService.authorizer = self.authorization
    calendarService.shouldFetchNextPages = true

    let query = GTLRCalendarQuery_EventsList.query(withCalendarId: "primary")
    query.timeMin = GTLRDateTime(date: startDateTime)
    query.timeMax = GTLRDateTime(date: endDateTime)

    calendarService.executeQuery(query, completionHandler: { (ticket, event, error) -> Void in
        if let error = error {
            NSLog("\(error)")
        } else {
            if let event = event as? GTLRCalendar_Events, let items = event.items {
                self.googleCalendarEventList.removeAll()
                for item in items {
                    let id: String = item.identifier ?? ""
                    let name: String = item.summary ?? ""
                    let startDate: Date? = item.start?.dateTime?.date
                    let endDate: Date? = item.end?.dateTime?.date
                    self.googleCalendarEventList.append(GoogleCalendaraEvent(id: id, name: name, startDate: startDate, endDate: endDate))
                }
            }
        }
    })
}

首先,检查Goole身份验证是否完成。
检查是否已保存GTMAppAuthFetcherAuthorization类,如果未保存,请调用先前创建的showAuthorizationDialog函数以显示Google身份验证对话框并获取GTMAppAuthFetcherAuthorization类。
如果保存了GTMAppAuthFetcherAuthorization类,请按原样使用它。

然后使用GoogleAPIClientForREST从Googl日历获取事件。
首先,生成GTLRCalendarService类以访问Goole日历,并在authorizer属性中设置GTMAppAuthFetcherAuthorization类。

1
2
3
let calendarService = GTLRCalendarService()
calendarService.authorizer = self.authorization
calendarService.shouldFetchNextPages = true

接下来,生成GTLRCalendarQuery_EventsList类以从Google日历中搜索事件,并将开始日期和时间以及结束日期和时间设置为搜索条件。

1
2
3
let query = GTLRCalendarQuery_EventsList.query(withCalendarId: "primary")
query.timeMin = GTLRDateTime(date: startDateTime)
query.timeMax = GTLRDateTime(date: endDateTime)

然后,以该GTLRCalendarQuery_EventsList类作为参数,执行GTLRCalendarService类的executeQuery方法以从Google日历获取事件。
当可以获取事件时,executeQuery方法的Callback函数将返回GTLRCalendar_Events类,因此将从此处获取事件信息。

1
2
3
4
5
6
7
8
9
10
if let event = event as? GTLRCalendar_Events, let items = event.items {
    self.googleCalendarEventList.removeAll()
    for item in items {
        let id: String = item.identifier ?? ""
        let name: String = item.summary ?? ""
        let startDate: Date? = item.start?.dateTime?.date
        let endDate: Date? = item.end?.dateTime?.date
        self.googleCalendarEventList.append(GoogleCalendaraEvent(id: id, name: name, startDate: startDate, endDate: endDate))
    }
}

特别是标识符(事件的唯一ID)很重要。
更改或删除事件时,此标识符是关键。

新增活动

接下来,我想在

中向Google日历添加事件。
它是通过将事件名称,开始日期和时间以及结束日期和时间传递给add方法在Google日历中创建事件的程序。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
import UIKit
import AppAuth
import GTMAppAuth
import GoogleAPIClientForREST

---------------- (中略) ----------------

private var authorization: GTMAppAuthFetcherAuthorization?
typealias showAuthorizationDialogCallBack = ((Error?) -> Void)

private func showAuthorizationDialog(callBack: @escaping showAuthorizationDialogCallBack) {
---------------- (中略) ----------------
}

private func add(eventName: String, startDateTime: Date, endDateTime: Date) {

    if GTMAppAuthFetcherAuthorization(fromKeychainForName: "authorization") != nil {
        self.authorization = GTMAppAuthFetcherAuthorization(fromKeychainForName: "authorization")!
    }

    if self.authorization == nil {
        showAuthorizationDialog(callBack: {(error) -> Void in
            if error == nil {
                self.addCalendarEvent(eventName: eventName, startDateTime: startDateTime, endDateTime: endDateTime)
            }
        })
    } else {
        self.addCalendarEvent(eventName: eventName, startDateTime: startDateTime, endDateTime: endDateTime)
    }
}

private func addCalendarEvent(eventName: String, startDateTime: Date, endDateTime: Date) {

    let calendarService = GTLRCalendarService()
    calendarService.authorizer = self.authorization
    calendarService.shouldFetchNextPages = true

    let event = GTLRCalendar_Event()
    event.summary = eventName

    let gtlrDateTimeStart: GTLRDateTime = GTLRDateTime(date: startDateTime)
    let startEventDateTime: GTLRCalendar_EventDateTime = GTLRCalendar_EventDateTime()
    startEventDateTime.dateTime = gtlrDateTimeStart
    event.start = startEventDateTime

    let gtlrDateTimeEnd: GTLRDateTime = GTLRDateTime(date: endDateTime)
    let endEventDateTime: GTLRCalendar_EventDateTime = GTLRCalendar_EventDateTime()
    endEventDateTime.dateTime = gtlrDateTimeEnd
    event.end = endEventDateTime

    let query = GTLRCalendarQuery_EventsInsert.query(withObject: event, calendarId: "primary")
    calendarService.executeQuery(query, completionHandler: { (ticket, event, error) -> Void in
        if let error = error {
            NSLog("\(error)")
        }
    })
}

生成GTLRCalendarService类的过程与搜索时的过程相同,因此我们将在后续部分中进行说明。
生成GTLRCalendar_Event类以设置要添加的事件的信息。
这次,设置了事件名称,开始日期和时间以及结束日期和时间,因此在GTLRCalendar_Event类的summary属性,start属性和end属性中进行了设置。

1
2
3
4
5
6
7
8
9
10
11
12
let event = GTLRCalendar_Event()
event.summary = eventName

let gtlrDateTimeStart: GTLRDateTime = GTLRDateTime(date: startDateTime)
let startEventDateTime: GTLRCalendar_EventDateTime = GTLRCalendar_EventDateTime()
startEventDateTime.dateTime = gtlrDateTimeStart
event.start = startEventDateTime

let gtlrDateTimeEnd: GTLRDateTime = GTLRDateTime(date: endDateTime)
let endEventDateTime: GTLRCalendar_EventDateTime = GTLRCalendar_EventDateTime()
endEventDateTime.dateTime = gtlrDateTimeEnd
event.end = endEventDateTime

另外,在添加新事件的情况下,成为事件唯一ID的标识符由Google日历自动分配,因此无需在此处进行设置。

然后,以GTLRCalendar_Event类作为参数,生成一个GTLRCalendarQuery_EventsInsert类以添加到Google日历,并执行GTLRCalendarService类的executeQuery方法将一个新事件添加到Google Calendar。

活动变更

接下来,让我们更改现有事件的信息。
当将事件标识符,事件名称,开始日期和时间,结束日期和时间传递给更新方法时,该程序会更改Google日历中相应标识符的事件信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
import UIKit
import AppAuth
import GTMAppAuth
import GoogleAPIClientForREST

---------------- (中略) ----------------

private var authorization: GTMAppAuthFetcherAuthorization?
typealias showAuthorizationDialogCallBack = ((Error?) -> Void)

private func showAuthorizationDialog(callBack: @escaping showAuthorizationDialogCallBack) {
---------------- (中略) ----------------
}

private func update(eventId: String, eventName: String, startDateTime: Date, endDateTime: Date) {

    if GTMAppAuthFetcherAuthorization(fromKeychainForName: "authorization") != nil {
        self.authorization = GTMAppAuthFetcherAuthorization(fromKeychainForName: "authorization")!
    }

    if self.authorization == nil {
        showAuthorizationDialog(callBack: {(error) -> Void in
            if error == nil {
                self.updateCalendarEvent(eventId: eventId, eventName: eventName, startDateTime: startDateTime, endDateTime: endDateTime)
            }
        })
    } else {
        self.updateCalendarEvent(eventId: eventId, eventName: eventName, startDateTime: startDateTime, endDateTime: endDateTime)
    }
}

private func updateCalendarEvent(eventId: String, eventName: String, startDateTime: Date, endDateTime: Date) {
    let calendarService = GTLRCalendarService()
    calendarService.authorizer = self.authorization
    calendarService.shouldFetchNextPages = true

    let event = GTLRCalendar_Event()
    event.identifier = eventId
    event.summary = eventName

    let gtlrDateTimeStart: GTLRDateTime = GTLRDateTime(date: startDateTime)
    let startEventDateTime: GTLRCalendar_EventDateTime = GTLRCalendar_EventDateTime()
    startEventDateTime.dateTime = gtlrDateTimeStart
    event.start = startEventDateTime

    let gtlrDateTimeEnd: GTLRDateTime = GTLRDateTime(date: endDateTime)
    let endEventDateTime: GTLRCalendar_EventDateTime = GTLRCalendar_EventDateTime()
    endEventDateTime.dateTime = gtlrDateTimeEnd
    event.end = endEventDateTime

    let query = GTLRCalendarQuery_EventsUpdate.query(withObject: event, calendarId: "primary", eventId: eventId)
    calendarService.executeQuery(query, completionHandler: { (ticket, event, error) -> Void in
        if let error = error {
            NSLog("\(error)")
        }
    })
}

对于

更新,请在GTLRCalendar_Event类的identifier属性中设置相应事件的ID。然后,将要更改的值设置为GTLRCalendar_Event类的属性。
之后,生成GTLRCalendarQuery_EventsUpdate类以GTLRCalendar_Event类作为参数来更新Google日历事件,并以GTLRCalendarService类的executeQuery方法作为参数。

删除活动

最后,删除Google日历中的事件。
当事件的标识符传递给delete方法时,该程序将从Google日历中删除相应的事件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
import UIKit
import AppAuth
import GTMAppAuth
import GoogleAPIClientForREST

---------------- (中略) ----------------

private var authorization: GTMAppAuthFetcherAuthorization?
typealias showAuthorizationDialogCallBack = ((Error?) -> Void)

private func showAuthorizationDialog(callBack: @escaping showAuthorizationDialogCallBack) {
---------------- (中略) ----------------
}

private func delete(eventId: String) {

    if GTMAppAuthFetcherAuthorization(fromKeychainForName: "authorization") != nil {
        self.authorization = GTMAppAuthFetcherAuthorization(fromKeychainForName: "authorization")!
    }

    if self.authorization == nil {
        showAuthorizationDialog(callBack: {(error) -> Void in
            if error == nil {
                self.deleteCalendarEvent(eventId: eventId)
            }
        })
    } else {
        self.deleteCalendarEvent(eventId: eventId)
    }
}

private func deleteCalendarEvent(eventId: String) {
    let calendarService = GTLRCalendarService()
    calendarService.authorizer = self.authorization
    calendarService.shouldFetchNextPages = true

    let query = GTLRCalendarQuery_EventsDelete.query(withCalendarId: "primary", eventId: eventId)
    calendarService.executeQuery(query, completionHandler: { (ticket, event, error) -> Void in
        if let error = error {
            NSLog("\(error)")
        }
    })
}

删除可以通过生成以事件标识符为参数的GTLRCalendarQuery_EventsDelete类并以GTLRCalendarService的executeQuery方法为参数来完成。

样例程序

这次创建的示例程序可在GitHub上找到。
https://github.com/naosekig/GoogleCalendarSample

参考

CocoaDocs.org --GoogleAPIClientForRest
Qiita:我尝试使用Swift 4

击中Google Calendar API