使用 Microsoft Graph 构建 iOS Swift 应用
本教程指导你如何使用 Swift 生成 iOS 应用,该应用使用 Microsoft Graph API 检索用户的日历信息。
提示
如果只想下载已完成的教程,可以下载或克隆GitHub存储库。
先决条件
在开始本教程之前,应在开发计算机上安装以下内容。
您还应该有一个在 Outlook.com 上拥有邮箱的个人 Microsoft 帐户,或者一个 Microsoft 工作或学校帐户。 如果你没有 Microsoft 帐户,则有几个选项可以获取免费帐户:
- 你可以 注册新的个人 Microsoft 帐户。
- 你可以注册开发人员计划Microsoft 365免费订阅Microsoft 365订阅。
备注
本教程使用 Xcode 版本 12.3 和 CocoaPods 版本 1.10.1 编写,但本指南中的步骤可能与其他版本一起运行,但尚未经过测试。
反馈
Please provide any feedback on this tutorial in the GitHub repository.
创建 iOS Swift 应用
首先创建新的 Swift 项目。
打开 Xcode。 在"文件" 菜单上,选择 "新建",然后选择 Project"。
选择"应用" 模板,然后选择"下一 步"。
将"产品名称" 设置为
GraphTutorial
,将"语言"设置为**"Swift"。**填写其余字段,然后选择"下一 步"。
选择项目的位置, 然后选择创建。
安装依赖项
在继续之前,请安装一些你稍后将使用的其他依赖项。
- Microsoft Authentication Library (MSAL) for iOS for authenticating to Azure AD。
- Microsoft Graph SDK for Objective C,用于调用 Microsoft Graph。
- Microsoft Graph Models SDK for Objective C for strong-typed objects representing Microsoft Graph resources like users or events.
退出 Xcode。
打开终端,将目录更改为 GraphTutorial 项目的位置。
运行以下命令以创建 Podfile。
pod init
打开 Podfile,并添加行后的以下
use_frameworks!
行。pod 'MSAL', '~> 1.1.13' pod 'MSGraphClientSDK', ' ~> 1.0.0' pod 'MSGraphClientModels', '~> 1.3.0'
保存 Podfile,然后运行以下命令以安装依赖项。
pod install
命令完成后,在 Xcode 中打开新创建的 GraphTu一l.xcworkspace。
设计应用
在此部分中,你将为应用创建视图:登录页、选项卡栏导航器、欢迎页和日历页。 你还将创建活动指示器覆盖层。
创建登录页
展开 Xcode 中的 GraphTutorial 文件夹,然后选择 "ViewController.swift"。
在 文件检查器 中 ,将文件 的名称更改为
SignInViewController.swift
。打开 SignInViewController.swift, 并将其内容替换为以下代码。
import UIKit class SignInViewController: UIViewController { override func viewDidLoad() { super.viewDidLoad() // Do any additional setup after loading the view. } @IBAction func signIn() { self.performSegue(withIdentifier: "userSignedIn", sender: nil) } }
打开 Main.storyboard。 展开 "查看控制器场景", 然后选择"查看控制器"。
选择标识 检查器,然后将类 下拉列表更改为 SignInViewController。
选择库 ,然后将按钮拖到登录视图控制器上。
选中按钮后,选择 "属性检查器 ",将按钮 的"标题 "更改为
Sign In
。选中按钮后 ,选择情节 提要底部的"对齐"按钮。 同时选择容器中 的"水平"和"容器内垂直"约束,将其值保留为 0,然后选择"添加 2 个约束"。
选择"登录视图控制器", 然后选择"连接 检查器"。
在 "已接收操作"下,将"登录" 旁边的未 填充圆圈拖到按钮上。 选择 弹出菜单上的 "内部触摸"。
创建选项卡栏
选择" 库", 然后将选项卡 栏控制器 拖动到情节提要上。
选择"登录视图控制器", 然后选择"连接 检查器"。
在 "触发的 Segues" 下,将手动旁的未填充圆圈拖到情节提要上的 选项卡 栏控制器上。 在 弹出菜单中选择 "模式显示"。
选择刚刚添加的 segue,然后选择 属性检查器。 将 "标识符" 字段设置为
userSignedIn
,将 **"演示文稿"**设置为"全屏"。选择"项目 1 场景", 然后选择"连接 检查器"。
在 触发的 Segues 下,将手动旁的未填充圆圈拖到情节提要上的 登录视图 控制器上。 在 弹出菜单中选择 "模式显示"。
选择刚刚添加的 segue,然后选择 属性检查器。 将 "标识符" 字段设置为
userSignedOut
,将 **"演示文稿"**设置为"全屏"。
创建欢迎页面
选择 Assets.xcassets 文件。
在"编辑器" 菜单上,选择"添加新资产", 然后选择"图像集"。
选择新的 Image 资源并使用 属性检查器 将其 名称设置为
DefaultUserPhoto
。添加要用作默认用户配置文件照片的任何图像。
在 GraphTutorial 文件夹中新建一个名为 的 Cocoa Touch 类文件
WelcomeViewController
。 在 "子类"字段中选择"UIViewController"。打开 WelcomeViewController.swift, 并将其内容替换为以下代码。
import UIKit class WelcomeViewController: UIViewController { @IBOutlet var userProfilePhoto: UIImageView! @IBOutlet var userDisplayName: UILabel! @IBOutlet var userEmail: UILabel! override func viewDidLoad() { super.viewDidLoad() // Do any additional setup after loading the view. // TEMPORARY self.userProfilePhoto.image = UIImage(imageLiteralResourceName: "DefaultUserPhoto") self.userDisplayName.text = "Default User" self.userEmail.text = "default@contoso.com" } @IBAction func signOut() { self.performSegue(withIdentifier: "userSignedOut", sender: nil) } }
打开 Main.storyboard。 选择" 项目 1 场景", 然后选择 标识检查器。 将 Class 值更改为 WelcomeViewController。
使用 库 , 将以下项添加到项目 1 场景。
- 一 个图像视图
- 两 个标签
- 一 个按钮
使用连接 检查器 进行以下连接。
- 将 userDisplayName 出口链接到第一个标签。
- 将 userEmail 出口 链接到第二个标签。
- 将 userProfilePhoto 出口链接到图像视图。
- 将 signOut received 操作链接到按钮的 "内部触摸"。
选择图像视图,然后选择大小 检查器。
将 Width 和 Height 设置为 196。
使用 "对齐 "按钮添加值为 0 的" 水平在容器内"约束。
使用"对齐" 按钮 (旁边的"添加新约束") 添加以下约束:
- 将顶部对齐到:保险箱 Area,值:0
- 下空间:用户显示名称,值:标准
- 高度,值:196
- 宽度,值:196
选择第一个标签,然后使用 "对齐" 按钮添加值为 0 的 " 在容器中水平放置"约束。
使用 "添加新约束" 按钮可添加以下约束:
- 顶部空间:用户配置文件照片,值:标准
- 底部空间:用户电子邮件,值:标准
选择第二个标签,然后选择 属性检查器。
将"颜色"更改为 "深灰色", 将 "字体"更改为 "系统 12.0"。
使用 "对齐 "按钮添加值为 0 的" 水平在容器内"约束。
使用 "添加新约束" 按钮可添加以下约束:
- Top Space to: User Display Name, value: Standard
- 下空间:注销,值:14
选择该按钮,然后选择"属性检查器"。
将 "标题" 更改为
Sign Out
。使用 "对齐 "按钮添加值为 0 的" 水平在容器内"约束。
使用 "添加新约束" 按钮可添加以下约束:
- 顶部空间:用户电子邮件,值:14
选择场景底部的选项卡栏项,然后选择 属性检查器。 将 "标题" 更改为
Me
。
完成操作后,欢迎场景看起来应该与此类似。
创建日历页
在 GraphTutorial 文件夹中新建一个名为 的 Cocoa Touch 类文件
CalendarViewController
。 在 "子类"字段中选择"UIViewController"。打开 CalendarViewController.swift, 并将其内容替换为以下代码。
import UIKit class CalendarViewController: UIViewController { @IBOutlet var calendarJSON: UITextView! override func viewDidLoad() { super.viewDidLoad() // Do any additional setup after loading the view. // TEMPORARY calendarJSON.text = "Calendar" calendarJSON.sizeToFit() } }
打开 Main.storyboard。 选择" 项目 2 场景", 然后选择 标识检查器。 将 Class 值更改为 CalendarViewController。
使用 库 , 将 文本视图添加到 项目 2 场景。
选择刚添加的文本视图。 在"编辑器" 菜单上,选择"嵌入", 然后选择"滚动视图"。
调整滚动视图和文本视图的大小以包含整个屏幕。
使用连接 检查器 将 calendarJSON 出口 连接到文本视图。
选择场景底部的选项卡栏项,然后选择 属性检查器。 将 "标题" 更改为
Calendar
。在"编辑器" 菜单上,选择"解决自动布局 问题",然后选择"日历视图控制器"中"所有视图"下的"添加缺少的约束"。
完成后,日历场景看起来应该与此类似。
创建活动指示器
在 GraphTutorial 文件夹中新建一个名为 的 Cocoa Touch 类文件
SpinnerViewController
。 在 "子类"字段中选择"UIViewController"。打开 SpinnerViewController.swift, 并将其内容替换为以下代码。
import UIKit class SpinnerViewController: UIViewController { var spinner = UIActivityIndicatorView(style: .large) override func loadView() { view = UIView() view.backgroundColor = UIColor(white: 0, alpha: 0.7) spinner.translatesAutoresizingMaskIntoConstraints = false spinner.startAnimating() view.addSubview(spinner) spinner.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true spinner.centerYAnchor.constraint(equalTo: view.centerYAnchor).isActive = true } public func start(container: UIViewController) { container.addChild(self) self.view.frame = container.view.frame container.view.addSubview(self.view) self.didMove(toParent: container) } public func stop() { self.willMove(toParent: nil) self.view.removeFromSuperview() self.removeFromParent() } }
测试应用程序
保存更改并启动应用。 你应该能够使用"登录"和"注销"按钮和选项卡栏在屏幕之间移动。
在门户中注册该应用
在此练习中,你将使用管理中心Azure Active Directory Azure AD 本机应用程序。
打开浏览器,并转到 Azure Active Directory 管理中心。然后,使用 个人帐户(亦称为“Microsoft 帐户”)或 工作或学校帐户 登录。
选择左侧导航栏中的“Azure Active Directory”,再选择“管理”下的“应用注册”。
选择“新注册”。 在“注册应用”页上,按如下方式设置值。
- 将“名称”设置为“
iOS Swift Graph Tutorial
”。 - 将“受支持的帐户类型”设置为“任何组织目录中的帐户和个人 Microsoft 帐户”。
- 保留“重定向 URI”为空。
- 将“名称”设置为“
选择“注册”。 在 "iOS Swift Graph 教程"页面上,复制"应用程序 (客户端) ID"的值并 保存它,你将在下一步中需要该值。
选择“管理”下的“身份验证”。 选择 "添加平台", 然后选择 "iOS/macOS"。
输入你的应用的捆绑包 ID,然后选择 **配置,**然后选择完成。
添加 Azure AD 身份验证
在此练习中,你将从上一练习中扩展应用程序,以支持使用 Azure AD 进行身份验证。 这是必需的,才能获取必要的 OAuth 访问令牌来调用 Microsoft Graph。 为此,您需要将适用于 iOS (MICROSOFT 身份验证库) MSAL 文档 集成到应用程序中。
在 GraphTutorial 项目中 新建一个名为 AuthSettings.plist 的属性列表文件。
将以下项添加到根字典 中的文件 。
键 类型 值 AppId
String Azure 门户中的应用程序 ID GraphScopes
数组 三个 String 值 User.Read
:、MailboxSettings.Read
和Calendars.ReadWrite
重要
如果你使用的是源代码管理(如 git),那么现在应该从源代码管理中排除 AuthSettings.plist 文件,以避免意外泄露应用 ID。
实施登录
在此部分中,你将为 MSAL 配置项目、创建身份验证管理器类,并更新应用以登录和注销。
为 MSAL 配置项目
将新的钥匙链组添加到项目的功能中。
- 选择 GraphTutorial 项目,然后选择 "&功能"。
- 选择 + 功能,然后双击 钥匙链共享。
- 添加值为 的钥匙链组
com.microsoft.adalcache
。
控件单击 "Info.plist", 然后选择"打开为",然后选择"源代码"。
在 元素中添加
<dict>
以下内容。<key>CFBundleURLTypes</key> <array> <dict> <key>CFBundleURLSchemes</key> <array> <string>msauth.$(PRODUCT_BUNDLE_IDENTIFIER)</string> </array> </dict> </array> <key>LSApplicationQueriesSchemes</key> <array> <string>msauthv2</string> <string>msauthv3</string> </array>
打开 AppDelegate.swift, 在文件顶部添加以下导入语句。
import MSAL
将以下函数添加到
AppDelegate
类。func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey : Any] = [:]) -> Bool { guard let sourceApplication = options[UIApplication.OpenURLOptionsKey.sourceApplication] as? String else { return false } return MSALPublicClientApplication.handleMSALResponse(url, sourceApplication: sourceApplication) }
创建身份验证管理器
在名为 AuthenticationManager.swift****的 GraphTutorial 项目中创建新的 Swift 文件。 将以下代码添加到文件中。
import Foundation import MSAL import MSGraphClientSDK // Implement the MSAuthenticationProvider interface so // this class can be used as an auth provider for the Graph SDK class AuthenticationManager: NSObject, MSAuthenticationProvider { // Implement singleton pattern static let instance = AuthenticationManager() private let publicClient: MSALPublicClientApplication? private let appId: String private let graphScopes: Array<String> private override init() { // Get app ID and scopes from AuthSettings.plist let bundle = Bundle.main let authConfigPath = bundle.path(forResource: "AuthSettings", ofType: "plist")! let authConfig = NSDictionary(contentsOfFile: authConfigPath)! self.appId = authConfig["AppId"] as! String self.graphScopes = authConfig["GraphScopes"] as! Array<String> do { // Create the MSAL client try self.publicClient = MSALPublicClientApplication(clientId: self.appId) } catch { print("Error creating MSAL public client: \(error)") self.publicClient = nil } } // Required function for the MSAuthenticationProvider interface func getAccessToken(for authProviderOptions: MSAuthenticationProviderOptions!, andCompletion completion: ((String?, Error?) -> Void)!) { getTokenSilently(completion: completion) } public func getTokenInteractively(parentView: UIViewController, completion: @escaping(_ accessToken: String?, Error?) -> Void) { let webParameters = MSALWebviewParameters(authPresentationViewController: parentView) let interactiveParameters = MSALInteractiveTokenParameters(scopes: self.graphScopes, webviewParameters: webParameters) interactiveParameters.promptType = MSALPromptType.selectAccount // Call acquireToken to open a browser so the user can sign in publicClient?.acquireToken(with: interactiveParameters, completionBlock: { (result: MSALResult?, error: Error?) in guard let tokenResult = result, error == nil else { print("Error getting token interactively: \(String(describing: error))") completion(nil, error) return } print("Got token interactively: \(tokenResult.accessToken)") completion(tokenResult.accessToken, nil) }) } public func getTokenSilently(completion: @escaping(_ accessToken: String?, Error?) -> Void) { // Check if there is an account in the cache var userAccount: MSALAccount? do { userAccount = try publicClient?.allAccounts().first } catch { print("Error getting account: \(error)") } if (userAccount != nil) { // Attempt to get token silently let silentParameters = MSALSilentTokenParameters(scopes: self.graphScopes, account: userAccount!) publicClient?.acquireTokenSilent(with: silentParameters, completionBlock: { (result: MSALResult?, error: Error?) in guard let tokenResult = result, error == nil else { print("Error getting token silently: \(String(describing: error))") completion(nil, error) return } print("Got token silently: \(tokenResult.accessToken)") completion(tokenResult.accessToken, nil) }) } else { print("No account in cache") completion(nil, NSError(domain: "AuthenticationManager", code: MSALError.interactionRequired.rawValue, userInfo: nil)) } } public func signOut() -> Void { do { // Remove all accounts from the cache let accounts = try publicClient?.allAccounts() try accounts!.forEach({ (account: MSALAccount) in try publicClient?.remove(account) }) } catch { print("Sign out error: \(String(describing: error))") } } }
添加登录和注销
打开 SignInViewController.swift, 并将其内容替换为以下代码。
import UIKit class SignInViewController: UIViewController { private let spinner = SpinnerViewController() override func viewDidLoad() { super.viewDidLoad() // Do any additional setup after loading the view. // See if a user is already signed in spinner.start(container: self) AuthenticationManager.instance.getTokenSilently { (token: String?, error: Error?) in DispatchQueue.main.async { self.spinner.stop() guard let _ = token, error == nil else { // If there is no token or if there's an error, // no user is signed in, so stay here return } // Since we got a token, a user is signed in // Go to welcome page self.performSegue(withIdentifier: "userSignedIn", sender: nil) } } } @IBAction func signIn() { spinner.start(container: self) // Do an interactive sign in AuthenticationManager.instance.getTokenInteractively(parentView: self) { (token: String?, error: Error?) in DispatchQueue.main.async { self.spinner.stop() guard let _ = token, error == nil else { // Show the error and stay on the sign-in page let alert = UIAlertController(title: "Error signing in", message: error.debugDescription, preferredStyle: .alert) alert.addAction(UIAlertAction(title: "OK", style: .default, handler: nil)) self.present(alert, animated: true) return } // Signed in successfully // Go to welcome page self.performSegue(withIdentifier: "userSignedIn", sender: nil) } } } }
打开 WelcomeViewController.swift, 将现有
signOut
函数替换为以下内容。@IBAction func signOut() { AuthenticationManager.instance.signOut() self.performSegue(withIdentifier: "userSignedOut", sender: nil) }
保存更改,在模拟器中重新启动应用程序。
如果登录应用,应该会看到访问令牌显示在 Xcode 的输出窗口中。
获取用户详细信息
在此部分中,你将创建一个帮助程序类来保存对 Microsoft Graph 的所有调用,并更新 为使用此新类获取 WelcomeViewController
登录用户。
在 GraphTutorial****项目中新建 一个名为 GraphManager.swift 的 Swift 文件。 将以下代码添加到文件中。
import Foundation import MSGraphClientSDK import MSGraphClientModels class GraphManager { // Implement singleton pattern static let instance = GraphManager() private let client: MSHTTPClient? public var userTimeZone: String private init() { client = MSClientFactory.createHTTPClient(with: AuthenticationManager.instance) userTimeZone = "UTC" } public func getMe(completion: @escaping(MSGraphUser?, Error?) -> Void) { // GET /me let select = "$select=displayName,mail,mailboxSettings,userPrincipalName" let meRequest = NSMutableURLRequest(url: URL(string: "\(MSGraphBaseURL)/me?\(select)")!) let meDataTask = MSURLSessionDataTask(request: meRequest, client: self.client, completion: { (data: Data?, response: URLResponse?, graphError: Error?) in guard let meData = data, graphError == nil else { completion(nil, graphError) return } do { // Deserialize response as a user let user = try MSGraphUser(data: meData) completion(user, nil) } catch { completion(nil, error) } }) // Execute the request meDataTask?.execute() } }
打开 WelcomeViewController.swift, 在文件顶部
import
添加以下语句。import MSGraphClientModels
将以下属性添加到
WelcomeViewController
类。private let spinner = SpinnerViewController()
将 现有的
viewDidLoad
替换为以下代码。override func viewDidLoad() { super.viewDidLoad() // Do any additional setup after loading the view. self.spinner.start(container: self) // Get the signed-in user self.userProfilePhoto.image = UIImage(imageLiteralResourceName: "DefaultUserPhoto") GraphManager.instance.getMe { (user: MSGraphUser?, error: Error?) in DispatchQueue.main.async { self.spinner.stop() guard let currentUser = user, error == nil else { print("Error getting user: \(String(describing: error))") return } // Set display name self.userDisplayName.text = currentUser.displayName ?? "Mysterious Stranger" self.userDisplayName.sizeToFit() // AAD users have email in the mail attribute // Personal accounts have email in the userPrincipalName attribute self.userEmail.text = currentUser.mail ?? currentUser.userPrincipalName ?? "" self.userEmail.sizeToFit() // Save the user's time zone GraphManager.instance.userTimeZone = currentUser.mailboxSettings?.timeZone ?? "UTC" } } }
如果保存更改并立即重新启动应用,则登录 UI 后会使用用户的 显示名称 电子邮件地址进行更新。
获取日历视图
在此练习中,你将将 Microsoft Graph合并到应用程序中。 对于此应用程序,你将使用 Microsoft Graph SDK for Objective C调用 Microsoft Graph。
从 Outlook 获取日历事件
在此部分中,你将扩展 类以添加一个函数,以获取用户本周的事件,并 GraphManager
更新为 CalendarViewController
使用这些新函数。
打开 GraphManager.swift, 将以下方法添加到
GraphManager
类。public func getCalendarView(viewStart: String, viewEnd: String, completion: @escaping(Data?, Error?) -> Void) { // GET /me/calendarview // Set start and end of the view let start = "startDateTime=\(viewStart)" let end = "endDateTime=\(viewEnd)" // Only return these fields in results let select = "$select=subject,organizer,start,end" // Sort results by when they were created, newest first let orderBy = "$orderby=start/dateTime" // Request at most 25 results let top = "$top=25" let eventsRequest = NSMutableURLRequest(url: URL(string: "\(MSGraphBaseURL)/me/calendarview?\(start)&\(end)&\(select)&\(orderBy)&\(top)")!) // Add the Prefer: outlook.timezone header to get start and end times // in user's time zone eventsRequest.addValue("outlook.timezone=\"\(self.userTimeZone)\"", forHTTPHeaderField: "Prefer") let eventsDataTask = MSURLSessionDataTask(request: eventsRequest, client: self.client, completion: { (data: Data?, response: URLResponse?, graphError: Error?) in guard let eventsData = data, graphError == nil else { completion(nil, graphError) return } // TEMPORARY completion(eventsData, nil) }) // Execute the request eventsDataTask?.execute() }
备注
考虑代码正在
getCalendarView
执行哪些工作。- 将调用的 URL 为
/v1.0/me/calendarview
。- 和
startDateTime
endDateTime
查询参数定义日历视图的起始和结束。 select
查询参数将每个事件返回的字段限定为视图将实际使用的字段。orderby
查询参数按开始时间对结果进行排序。- 查询
top
参数请求每页 25 个结果。 - 标头使 Microsoft Graph返回用户时区中每个事件的
Prefer: outlook.timezone
开始时间和结束时间。
- 和
- 将调用的 URL 为
在 GraphTu一****l 项目中新建一个名为 GraphToIana.swift 的 Swift 文件。 将以下代码添加到文件中。
import Foundation // Basic lookup for mapping Windows time zone identifiers to // IANA identifiers // Mappings taken from // https://github.com/unicode-org/cldr/blob/master/common/supplemental/windowsZones.xml class GraphToIana { private static let timeZoneMap = [ "Dateline Standard Time" : "Etc/GMT+12", "UTC-11" : "Etc/GMT+11", "Aleutian Standard Time" : "America/Adak", "Hawaiian Standard Time" : "Pacific/Honolulu", "Marquesas Standard Time" : "Pacific/Marquesas", "Alaskan Standard Time" : "America/Anchorage", "UTC-09" : "Etc/GMT+9", "Pacific Standard Time (Mexico)" : "America/Tijuana", "UTC-08" : "Etc/GMT+8", "Pacific Standard Time" : "America/Los_Angeles", "US Mountain Standard Time" : "America/Phoenix", "Mountain Standard Time (Mexico)" : "America/Chihuahua", "Mountain Standard Time" : "America/Denver", "Central America Standard Time" : "America/Guatemala", "Central Standard Time" : "America/Chicago", "Easter Island Standard Time" : "Pacific/Easter", "Central Standard Time (Mexico)" : "America/Mexico_City", "Canada Central Standard Time" : "America/Regina", "SA Pacific Standard Time" : "America/Bogota", "Eastern Standard Time (Mexico)" : "America/Cancun", "Eastern Standard Time" : "America/New_York", "Haiti Standard Time" : "America/Port-au-Prince", "Cuba Standard Time" : "America/Havana", "US Eastern Standard Time" : "America/Indianapolis", "Turks And Caicos Standard Time" : "America/Grand_Turk", "Paraguay Standard Time" : "America/Asuncion", "Atlantic Standard Time" : "America/Halifax", "Venezuela Standard Time" : "America/Caracas", "Central Brazilian Standard Time" : "America/Cuiaba", "SA Western Standard Time" : "America/La_Paz", "Pacific SA Standard Time" : "America/Santiago", "Newfoundland Standard Time" : "America/St_Johns", "Tocantins Standard Time" : "America/Araguaina", "E. South America Standard Time" : "America/Sao_Paulo", "SA Eastern Standard Time" : "America/Cayenne", "Argentina Standard Time" : "America/Buenos_Aires", "Greenland Standard Time" : "America/Godthab", "Montevideo Standard Time" : "America/Montevideo", "Magallanes Standard Time" : "America/Punta_Arenas", "Saint Pierre Standard Time" : "America/Miquelon", "Bahia Standard Time" : "America/Bahia", "UTC-02" : "Etc/GMT+2", "Azores Standard Time" : "Atlantic/Azores", "Cape Verde Standard Time" : "Atlantic/Cape_Verde", "UTC" : "Etc/GMT", "GMT Standard Time" : "Europe/London", "Greenwich Standard Time" : "Atlantic/Reykjavik", "Sao Tome Standard Time" : "Africa/Sao_Tome", "Morocco Standard Time" : "Africa/Casablanca", "W. Europe Standard Time" : "Europe/Berlin", "Central Europe Standard Time" : "Europe/Budapest", "Romance Standard Time" : "Europe/Paris", "Central European Standard Time" : "Europe/Warsaw", "W. Central Africa Standard Time" : "Africa/Lagos", "Jordan Standard Time" : "Asia/Amman", "GTB Standard Time" : "Europe/Bucharest", "Middle East Standard Time" : "Asia/Beirut", "Egypt Standard Time" : "Africa/Cairo", "E. Europe Standard Time" : "Europe/Chisinau", "Syria Standard Time" : "Asia/Damascus", "West Bank Standard Time" : "Asia/Hebron", "South Africa Standard Time" : "Africa/Johannesburg", "FLE Standard Time" : "Europe/Kiev", "Israel Standard Time" : "Asia/Jerusalem", "Kaliningrad Standard Time" : "Europe/Kaliningrad", "Sudan Standard Time" : "Africa/Khartoum", "Libya Standard Time" : "Africa/Tripoli", "Namibia Standard Time" : "Africa/Windhoek", "Arabic Standard Time" : "Asia/Baghdad", "Turkey Standard Time" : "Europe/Istanbul", "Arab Standard Time" : "Asia/Riyadh", "Belarus Standard Time" : "Europe/Minsk", "Russian Standard Time" : "Europe/Moscow", "E. Africa Standard Time" : "Africa/Nairobi", "Iran Standard Time" : "Asia/Tehran", "Arabian Standard Time" : "Asia/Dubai", "Astrakhan Standard Time" : "Europe/Astrakhan", "Azerbaijan Standard Time" : "Asia/Baku", "Russia Time Zone 3" : "Europe/Samara", "Mauritius Standard Time" : "Indian/Mauritius", "Saratov Standard Time" : "Europe/Saratov", "Georgian Standard Time" : "Asia/Tbilisi", "Volgograd Standard Time" : "Europe/Volgograd", "Caucasus Standard Time" : "Asia/Yerevan", "Afghanistan Standard Time" : "Asia/Kabul", "West Asia Standard Time" : "Asia/Tashkent", "Ekaterinburg Standard Time" : "Asia/Yekaterinburg", "Pakistan Standard Time" : "Asia/Karachi", "Qyzylorda Standard Time" : "Asia/Qyzylorda", "India Standard Time" : "Asia/Calcutta", "Sri Lanka Standard Time" : "Asia/Colombo", "Nepal Standard Time" : "Asia/Katmandu", "Central Asia Standard Time" : "Asia/Almaty", "Bangladesh Standard Time" : "Asia/Dhaka", "Omsk Standard Time" : "Asia/Omsk", "Myanmar Standard Time" : "Asia/Rangoon", "SE Asia Standard Time" : "Asia/Bangkok", "Altai Standard Time" : "Asia/Barnaul", "W. Mongolia Standard Time" : "Asia/Hovd", "North Asia Standard Time" : "Asia/Krasnoyarsk", "N. Central Asia Standard Time" : "Asia/Novosibirsk", "Tomsk Standard Time" : "Asia/Tomsk", "China Standard Time" : "Asia/Shanghai", "North Asia East Standard Time" : "Asia/Irkutsk", "Singapore Standard Time" : "Asia/Singapore", "W. Australia Standard Time" : "Australia/Perth", "Taipei Standard Time" : "Asia/Taipei", "Ulaanbaatar Standard Time" : "Asia/Ulaanbaatar", "Aus Central W. Standard Time" : "Australia/Eucla", "Transbaikal Standard Time" : "Asia/Chita", "Tokyo Standard Time" : "Asia/Tokyo", "North Korea Standard Time" : "Asia/Pyongyang", "Korea Standard Time" : "Asia/Seoul", "Yakutsk Standard Time" : "Asia/Yakutsk", "Cen. Australia Standard Time" : "Australia/Adelaide", "AUS Central Standard Time" : "Australia/Darwin", "E. Australia Standard Time" : "Australia/Brisbane", "AUS Eastern Standard Time" : "Australia/Sydney", "West Pacific Standard Time" : "Pacific/Port_Moresby", "Tasmania Standard Time" : "Australia/Hobart", "Vladivostok Standard Time" : "Asia/Vladivostok", "Lord Howe Standard Time" : "Australia/Lord_Howe", "Bougainville Standard Time" : "Pacific/Bougainville", "Russia Time Zone 10" : "Asia/Srednekolymsk", "Magadan Standard Time" : "Asia/Magadan", "Norfolk Standard Time" : "Pacific/Norfolk", "Sakhalin Standard Time" : "Asia/Sakhalin", "Central Pacific Standard Time" : "Pacific/Guadalcanal", "Russia Time Zone 11" : "Asia/Kamchatka", "New Zealand Standard Time" : "Pacific/Auckland", "UTC+12" : "Etc/GMT-12", "Fiji Standard Time" : "Pacific/Fiji", "Chatham Islands Standard Time" : "Pacific/Chatham", "UTC+13" : "Etc/GMT-13", "Tonga Standard Time" : "Pacific/Tongatapu", "Samoa Standard Time" : "Pacific/Apia", "Line Islands Standard Time" : "Pacific/Kiritimati" ] public static func getIanaIdentifier(graphIdentifer: String) -> String { // If a mapping was not found, assume the value passed // was already an IANA identifier return timeZoneMap[graphIdentifer] ?? graphIdentifer } }
这将进行简单的查找,以根据 Microsoft Graph 返回的时区名称查找 IANA 时区Graph。
打开 CalendarViewController.swift, 并将其全部内容替换为以下代码。
import UIKit import MSGraphClientModels class CalendarViewController: UIViewController { @IBOutlet var calendarJSON: UITextView! private let spinner = SpinnerViewController() override func viewDidLoad() { super.viewDidLoad() // Do any additional setup after loading the view. self.spinner.start(container: self) // Calculate the start and end of the current week let timeZone = GraphToIana.getIanaIdentifier(graphIdentifer: GraphManager.instance.userTimeZone) let now = Date() var calendar = Calendar(identifier: .gregorian) calendar.timeZone = TimeZone(identifier: timeZone)! let startOfWeek = calendar.dateComponents([.calendar, .yearForWeekOfYear, .weekOfYear], from: now).date! let endOfWeek = calendar.date(byAdding: .day, value: 7, to: startOfWeek)! // Convert start and end to ISO 8601 strings let isoFormatter = ISO8601DateFormatter() let viewStart = isoFormatter.string(from: startOfWeek) let viewEnd = isoFormatter.string(from: endOfWeek) GraphManager.instance.getCalendarView(viewStart: viewStart, viewEnd: viewEnd) { (data: Data?, error: Error?) in DispatchQueue.main.async { self.spinner.stop() // TEMPORARY guard let eventsData = data, error == nil else { self.calendarJSON.text = error.debugDescription return } let jsonString = String(data: eventsData, encoding: .utf8) self.calendarJSON.text = jsonString self.calendarJSON.sizeToFit() } } } }
现在,你可以运行应用、登录并点击菜单中的 "日历 "导航项。 你应该会看到应用中事件的 JSON 转储。
显示结果
现在,可以将 JSON 转储替换为某些内容,以用户友好的方式显示结果。 在此部分中,您将修改 函数以返回强类型对象,并进行修改以使用表视图 getCalendarView
CalendarViewController
呈现事件。
打开 GraphManager.swift。 将现有的
getCalendarView
函数替换为以下内容。public func getCalendarView(viewStart: String, viewEnd: String, completion: @escaping([MSGraphEvent]?, Error?) -> Void) { // GET /me/calendarview // Set start and end of the view let start = "startDateTime=\(viewStart)" let end = "endDateTime=\(viewEnd)" // Only return these fields in results let select = "$select=subject,organizer,start,end" // Sort results by when they were created, newest first let orderBy = "$orderby=start/dateTime" // Request at most 25 results let top = "$top=25" let eventsRequest = NSMutableURLRequest(url: URL(string: "\(MSGraphBaseURL)/me/calendarview?\(start)&\(end)&\(select)&\(orderBy)&\(top)")!) // Add the Prefer: outlook.timezone header to get start and end times // in user's time zone eventsRequest.addValue("outlook.timezone=\"\(self.userTimeZone)\"", forHTTPHeaderField: "Prefer") let eventsDataTask = MSURLSessionDataTask(request: eventsRequest, client: self.client, completion: { (data: Data?, response: URLResponse?, graphError: Error?) in guard let eventsData = data, graphError == nil else { completion(nil, graphError) return } do { // Deserialize response as events collection let eventsCollection = try MSCollection(data: eventsData) var eventArray: [MSGraphEvent] = [] eventsCollection.value.forEach({ (rawEvent: Any) in // Convert JSON to a dictionary guard let eventDict = rawEvent as? [String: Any] else { return } // Deserialize event from the dictionary let event = MSGraphEvent(dictionary: eventDict)! eventArray.append(event) }) // Return the array completion(eventArray, nil) } catch { completion(nil, error) } }) // Execute the request eventsDataTask?.execute() }
在 GraphTutorial 项目中新建一个名为 的 Cocoa Touch 类文件
CalendarTableViewController.swift
。 在 "子类"字段中选择"UITableViewController"。打开 CalendarTableViewController.swift, 并将其内容替换为以下内容。
import UIKit import MSGraphClientModels class CalendarTableViewController: UITableViewController { private let tableCellIdentifier = "EventCell" private var events: [MSGraphEvent]? override func viewDidLoad() { super.viewDidLoad() tableView.rowHeight = UITableView.automaticDimension tableView.estimatedRowHeight = 100 } // Number of sections, always 1 override func numberOfSections(in tableView: UITableView) -> Int { return 1 } // Return the number of events in the table override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { return events?.count ?? 0 } override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { let cell = tableView.dequeueReusableCell(withIdentifier: tableCellIdentifier, for: indexPath) as! CalendarTableViewCell // Get the event that corresponds to the row let event = events?[indexPath.row] // Configure the cell cell.subject = event?.subject cell.organizer = event?.organizer?.emailAddress?.name // Build a duration string let duration = "\(self.formatGraphDateTime(dateTime: event?.start)) to \(self.formatGraphDateTime(dateTime: event?.end))" cell.duration = duration return cell } private func formatGraphDateTime(dateTime: MSGraphDateTimeTimeZone?) -> String { guard let graphDateTime = dateTime else { return "" } // Create a formatter to parse Graph's date format let isoFormatter = DateFormatter() isoFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSSSSS" let date = isoFormatter.date(from: graphDateTime.dateTime) // Output like 5/5/2019, 2:00 PM let dateFormatter = DateFormatter() dateFormatter.dateStyle = .short dateFormatter.timeStyle = .short return dateFormatter.string(from: date!) } public func setEvents(events: [MSGraphEvent]?) -> Void { self.events = events self.tableView.reloadData() } }
在 GraphTutorial 项目中新建一个名为 的 Cocoa Touch 类文件
CalendarTableViewCell.swift
。 在"子类"字段中 选择**"UITableViewCell"。**打开 CalendarTableViewCell.swift, 将以下属性添加到
CalendarTableViewCell
类。@IBOutlet var subjectLabel: UILabel! @IBOutlet var organizerLabel: UILabel! @IBOutlet var durationLabel: UILabel! var subject: String? { didSet { subjectLabel.text = subject } } var organizer: String? { didSet { organizerLabel.text = organizer } } var duration: String? { didSet { durationLabel.text = duration } }
打开 Main.storyboard 并找到 "日历场景"。 从根视图中删除滚动视图。
使用 库,将 导航栏 添加到视图的顶部。
双击导航栏中的 "标题 ",然后更新为
Calendar
。使用 库,将 栏按钮项 添加到导航栏的右侧。
选择新栏按钮,然后选择属性 检查器。 将 Image 更改为 加。
将 "库"中的**"容器视图**"添加到导航栏下的视图中。 调整容器视图的大小以占用视图中的所有剩余空间。
按如下方式设置导航栏和容器视图的约束。
- 导航栏
- 添加约束:Height,值:44
- 添加约束:区域前导保险箱,值:0
- 添加约束:区域尾部保险箱,值:0
- 添加约束:区域顶部保险箱,值:0
- 容器视图
- 添加约束:区域前导保险箱,值:0
- 添加约束:区域尾部保险箱,值:0
- 添加约束:顶部空间添加到导航栏底部,值:0
- 添加约束:区域的底部保险箱,值:0
- 导航栏
在添加容器视图时,找到添加到情节提要的第二个视图控制器。 它通过嵌入的 segue 连接到日历场景。 选择此控制器并使用 标识检查器 将 类 更改为 CalendarTableViewController。
从 日历表 视图 控制器中删除视图。
将库中 的表视图添加到日历表视图控制器。
选择表视图,然后选择属性 检查器。 将 原型单元格设置为 1。
拖动原型单元格的底部边缘,以为您提供更大的区域来使用。
使用 库向 原型单元格 添加三 个标签。
选择原型单元格,然后选择标识 检查器。 将 类 更改为 CalendarTableViewCell。
选择" 属性检查器", 将 "标识符" 设置为
EventCell
。选择 EventCell 后,选择 "连接检查 器",并连接 、 和 添加到情节提要
durationLabel
organizerLabel
subjectLabel
上的单元格的标签。按如下所示设置三个标签的属性和约束。
- 主题标签
- 添加约束:内容视图前导边距前导空格,值:0
- 向内容视图尾随边距添加尾随空格,值:0
- 添加约束:内容视图上边距的顶部空间,值:0
- 组织者标签
- 字体:System 12.0
- 添加约束:Height,值:15
- 添加约束:内容视图前导边距前导空格,值:0
- 向内容视图尾随边距添加尾随空格,值:0
- 添加约束:主题标签顶部空间底部,值:标准
- 持续时间标签
- 字体:System 12.0
- 颜色:深灰色
- 添加约束:Height,值:15
- 添加约束:内容视图前导边距前导空格,值:0
- 向内容视图尾随边距添加尾随空格,值:0
- 添加约束:将顶部空间添加到组织者标签底部,值:Standard
- 添加约束:内容视图下边距的底部空间,值:0
- 主题标签
选择 EventCell,然后选择大小 检查器。 为 行 高 启用自动。
打开 CalendarViewController.swift, 并将其内容替换为以下代码。
import UIKit import MSGraphClientModels class CalendarViewController: UIViewController { private let spinner = SpinnerViewController() private var tableViewController: CalendarTableViewController? override func viewDidLoad() { super.viewDidLoad() self.spinner.start(container: self) // Calculate the start and end of the current week let timeZone = GraphToIana.getIanaIdentifier(graphIdentifer: GraphManager.instance.userTimeZone) let now = Date() var calendar = Calendar(identifier: .gregorian) calendar.timeZone = TimeZone(identifier: timeZone)! let startOfWeek = calendar.dateComponents([.calendar, .yearForWeekOfYear, .weekOfYear], from: now).date! let endOfWeek = calendar.date(byAdding: .day, value: 7, to: startOfWeek)! // Convert start and end to ISO 8601 strings let isoFormatter = ISO8601DateFormatter() let viewStart = isoFormatter.string(from: startOfWeek) let viewEnd = isoFormatter.string(from: endOfWeek) GraphManager.instance.getCalendarView(viewStart: viewStart, viewEnd: viewEnd) { (eventArray: [MSGraphEvent]?, error: Error?) in DispatchQueue.main.async { self.spinner.stop() guard let events = eventArray, error == nil else { // Show the error let alert = UIAlertController(title: "Error getting events", message: error.debugDescription, preferredStyle: .alert) alert.addAction(UIAlertAction(title: "OK", style: .default, handler: nil)) self.present(alert, animated: true) return } self.tableViewController?.setEvents(events: events) } } } internal override func prepare(for segue: UIStoryboardSegue, sender: Any?) { // Save reference to the contained table view if segue.destination is CalendarTableViewController { self.tableViewController = segue.destination as? CalendarTableViewController } } @IBAction func showNewEventForm() { self.performSegue(withIdentifier: "showEventForm", sender: self) } }
运行应用、登录,然后点击" 日历" 选项卡。你应该会看到事件列表。
创建新事件
在此部分中,您将添加在用户日历上创建事件的能力。
打开 GraphManager.swift 并添加以下函数以在用户日历上创建新事件。
public func createEvent(subject: String, start: Date, end: Date, attendees: [Substring]?, body: String?, completion: @escaping(MSGraphEvent?, Error?) -> Void) { let isoFormatter = DateFormatter() isoFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss" // Create a dictionary to represent the event // Current version of the Graph SDK models don't serialize properly // see https://github.com/microsoftgraph/msgraph-sdk-objc-models/issues/27 var newEventDict: [String: Any] = [ "subject": subject, "start": [ "dateTime": isoFormatter.string(from: start), "timeZone": self.userTimeZone ], "end": [ "dateTime": isoFormatter.string(from: end), "timeZone": self.userTimeZone ] ] if attendees?.count ?? 0 > 0 { var attendeeArray: [Any] = [] for attendee in attendees! { let attendeeDict: [String: Any] = [ "type": "required", "emailAddress": [ "address": String(attendee) ] ] attendeeArray.append(attendeeDict) } newEventDict["attendees"] = attendeeArray } if !(body?.isEmpty ?? false) { newEventDict["body"] = [ "content": body, "contentType": "text" ] } let eventData = try? JSONSerialization.data(withJSONObject: newEventDict) let createEventRequest = NSMutableURLRequest(url: URL(string: "\(MSGraphBaseURL)/me/events")!) createEventRequest.httpMethod = "POST" createEventRequest.httpBody = eventData createEventRequest.addValue("application/json", forHTTPHeaderField: "Content-Type") let createEventTask = MSURLSessionDataTask(request: createEventRequest, client: self.client, completion: { (data: Data?, response: URLResponse?, graphError: Error?) in guard let eventData = data, graphError == nil else { completion(nil, graphError) return } do { // Deserialize response as event let returnedEvent = try MSGraphEvent(data: eventData) // Return the event completion(returnedEvent, nil) } catch { completion(nil, error) } }) // Execute the task createEventTask?.execute() }
在 GraphTutorial 文件夹中新建一个名为 的 Cocoa Touch 类文件
NewEventViewController
。 在 "子类"字段中选择"UIViewController"。打开 NewEventViewController.swift, 并将其内容替换为以下内容。
import UIKit import MSGraphClientModels class NewEventViewController: UIViewController { @IBOutlet var subject: UITextField! @IBOutlet var attendees: UITextField! @IBOutlet var start: UIDatePicker! @IBOutlet var end: UIDatePicker! @IBOutlet var body: UITextView! private let spinner = SpinnerViewController() override func viewDidLoad() { super.viewDidLoad() // Add border around text view let borderColor : UIColor = UIColor(red: 0.85, green: 0.85, blue: 0.85, alpha: 1.0) body.layer.borderWidth = 0.5 body.layer.borderColor = borderColor.cgColor body.layer.cornerRadius = 5.0 // Set start picker to the next closest half-hour let now = Date() let calendar = Calendar.current let components = calendar.dateComponents([.minute], from:now) let offset = 30 - (components.minute! % 30) let start = calendar.date(byAdding: .minute, value: offset, to: now) self.start.date = start! // Set end picker to start + 30 min let end = calendar.date(byAdding: .minute, value: 30, to: start!) self.end.date = end! } @IBAction func createEvent() { self.spinner.start(container: self) // Do create let subject = self.subject.text ?? "" let attendees = self.attendees.text?.split(separator: ";") let start = self.start.date let end = self.end.date let body = self.body.text ?? "" GraphManager.instance.createEvent(subject: subject, start: start, end: end, attendees: attendees, body: body) { (event: MSGraphEvent?, error: Error?) in DispatchQueue.main.async { self.spinner.stop() guard let _ = event, error == nil else { // Show the error let alert = UIAlertController(title: "Error creating event", message: error.debugDescription, preferredStyle: .alert) alert.addAction(UIAlertAction(title: "OK", style: .default, handler: nil)) self.present(alert, animated: true) return } let alert = UIAlertController(title: "Success", message: "Event created", preferredStyle: .alert) alert.addAction(UIAlertAction(title: "OK", style: .default, handler: { (action: UIAlertAction?) in self.dismiss(animated: true, completion: nil) })) self.present(alert, animated: true) } } } @IBAction func cancel() { self.dismiss(animated: true, completion: nil) } }
打开 Main.storyboard。 使用 库 将视图 控制器拖动 到情节提要上。
使用 库,将 导航栏 添加到视图控制器。
双击导航栏中的 "标题 ",然后更新为
New Event
。使用 库,将 栏按钮项 添加到导航栏的左侧。
选择新栏按钮,然后选择属性 检查器。 将 "标题" 更改为
Cancel
。使用 库,将 栏按钮项 添加到导航栏的右侧。
选择新栏按钮,然后选择属性 检查器。 将 "标题" 更改为
Create
。选择视图控制器,然后选择标识 检查器。 将 类 更改为 NewEventViewController。
将库中的以下 控件 添加到视图中。
- 在导航 栏下 添加 Label。 将文本设置为
Subject
。 - 在标签 下添加 文本字段。 将 Placeholder 属性设置为
Subject
。 - 在文本 字段 下添加 Label。 将文本设置为
Attendees
。 - 在标签 下添加 文本字段。 将 Placeholder 属性设置为
Separate multiple entries with ;
。 - 在文本 字段 下添加 Label。 将文本设置为
Start
。 - 在标签 下添加日期 选取器。 将"首选样式"设置为"精简",将"间隔"设置为 15 分钟,将高度设置为 35。
- 在日期 选取 器下添加 Label。 将文本设置为
End
。 - 在标签 下添加日期 选取器。 将"首选样式"设置为"精简",将"间隔"设置为 15 分钟,将高度设置为 35。
- 在日期 选取器 下添加文本视图。
- 在导航 栏下 添加 Label。 将文本设置为
选择" 新建事件视图控制器", 并使用 连接 检查器建立以下连接。
- 连接"取消 栏"按钮添加"取消 接收"操作。
- 连接 createEvent received 操作的操作控制到 "创建栏" 按钮。
- 连接 第一个 文本字段添加主题出口。
- 连接 到第 二个文本字段。
- 连接 到第一个 日期选取器。
- 连接 到第二个 日期选取器。
- 连接 向文本视图 添加正文出口。
添加以下约束。
- 导航栏
- 区域前导保险箱,值:0
- 区域尾随保险箱,值:0
- 区域顶部保险箱,值:0
- 高度,值:44
- 主题标签
- 查看边距前导空格,值:0
- 查看边距的尾部空格,值:0
- 导航栏顶部空间,值:20
- 主题文本字段
- 查看边距前导空格,值:0
- 查看边距的尾部空格,值:0
- 主题标签顶部空间,值:Standard
- 与会者标签
- 查看边距前导空格,值:0
- 查看边距的尾部空格,值:0
- 主题文本字段的上空间,值:Standard
- 与会者文本字段
- 查看边距前导空格,值:0
- 查看边距的尾部空格,值:0
- 与会者标签顶部空间,值:Standard
- 开始标签
- 查看边距前导空格,值:0
- 查看边距的尾部空格,值:0
- 主题文本字段的上空间,值:Standard
- 开始日期选取器
- 查看边距前导空格,值:0
- 查看边距的尾部空格,值:0
- 与会者标签顶部空间,值:Standard
- 高度,值:35
- 结束标签
- 查看边距前导空格,值:0
- 查看边距的尾部空格,值:0
- "开始日期选取器"的顶部空间,值:Standard
- 结束日期选取器
- 查看边距前导空格,值:0
- 查看边距的尾部空格,值:0
- 结束标签的上空间,值:Standard
- 高度:35
- 正文文本视图
- 查看边距前导空格,值:0
- 查看边距的尾部空格,值:0
- 结束日期选取器的顶部空间,值:Standard
- 查看边距的底部空间,值:0
- 导航栏
选择"日历场景", 然后选择"连接 检查器"。
在 "触发的 Segues" 下,将手动旁的未填充圆圈拖到情节提要上的"新建事件视图 控制器"上。 在 弹出菜单中选择 "模式显示"。
选择刚刚添加的 segue,然后选择 属性检查器。 将 "标识符" 字段设置为
showEventForm
。连接 showNewEventForm 接收操作的操作更新 + 到导航栏按钮。
保存更改并重新启动该应用。 转到日历页面,然后点击 + 该按钮。 填写表单并点击" 创建" 创建新事件。
恭喜!
你已完成 iOS Swift Microsoft Graph教程。 现在,你已经拥有一个调用 Microsoft Graph,你可以试验和添加新功能。 请访问Microsoft Graph概述,查看可以使用 Microsoft Graph 访问的所有数据。
反馈
Please provide any feedback on this tutorial in the GitHub repository.
你有关于此部分的问题? 如果有,请向我们提供反馈,以便我们对此部分作出改进。