入门 GraphQL & Apollo

本文翻译自 https://www.raywenderlich.com/158433/getting-started-graphql-apollo-ios

原作者:Nikolas Burk

译者:@nixzhu


当你配合一个REST API工作时,因为某个端点没有给你所需要的数据用于app的某些视图,你是否有感到过沮丧呢?例如需要多个请求才能从服务器获取到正确的信息,或者你需要报bug通知后端开发者才能调整API?现在,你无需再担忧,GraphQLApollo将拯救你。

GraphQL是一种新的API设计范式,由Facebook在2015年开源,不过早在2012,他们就开始用于给自家的app提供支持了。它消除了当今REST API的许多低效之处。对比REST,GraphQL只需要暴露一个端点,API的消费者就能精确地指定他们所需要的数据。

在本教程中,你将构建一个iPhone app来帮助用户计划参与哪些iOS会议。你将设置自己的GraphQL服务器并在app中用Apollo iOS Client与之交互。Apollo是一个网络库,它能让我们与GraphQL API工作起来如沐春风。

此app将包含如下特性:

  • 显示iOS会议列表
  • 标记自己想参加或不想参加某个会议
  • 查看参加某个会议的同伴

为了本教程的缘故,你要先通过Node Package Manager安装一些工具,请确保你已经安装有npm,且版本在4.5.0(或以上)。

开始

下载并打开用于本教程的starter project。它已包含有必要的UI组件,因此你可以专注于同API打交道,把数据正确地带到app中。

Storyboard看起来如下:

Application Main Storyboard

此app使用CocoaPods,所以在下载好第三方包后,你要打开ConferencePlanner.xcworkspaceApollo的pod已经被包含在项目中,不过你应该确保安装的是最新的版本。

打开终端,导航到项目所在目录,并执行pod install来更新:

Pod Install Output

为何使用GraphQL?

REST API暴露多个端点,每个端点都返回特定的信息。例如,你可能有如下端点:

  • /conferences:返回一个所有会议的列表,每个会议都有idnamecityyear字段
  • /conferences/__id__/attendees:返回某个会议所有的参加者(包括idname)和会议id

想象一下,你所写的app要显示所有会议的列表,加上每个会议最近注册的三个参加者。你有何选择?

iOS Screen Application Running

选项1:修改API

告诉后端开发者修改API,这样每次调用/conferences时都返回最近注册的三个参加者:

Conferences REST API Endpoint Data

选项2:发送n+1次请求

发送n+1次请求(此处n是会议的数量)来获取所需的信息,并且接受你可能会耗尽用户的网络流量的后果,因为虽然每个会议你只需要最近的三个与会者却也必须下载全部。

Conference Attendee REST Endpoint Data

这两个选项都不是很有吸引力,在更大的开发项目里也不具有很好的伸缩性!

使用GraphQL,你就可以简单地指定你在单个请求里所需要的数据,使用GraphQL语法以申明式的方式描述即可:

  1. {
  2. allConferences {
  3. name
  4. city
  5. year
  6. attendees(last: 3) {
  7. name
  8. }
  9. }
  10. }

这个查询的应答将包含一个会议的列表,其中每个会议都有namecityyear,而且带有三个最新的与会者。

在iOS中通过Apollo使用GraphQL

GraphQL在移动开发者社区中还不是很流行,不过很可能会随着更多相关工具的改进而改变。这种势头表现在Apollo iOS客户端上,它实现了许多方便特性,让你与API合作时更轻松。

目前,它的主要特性如下:

  1. 基于数据要求的静态类型生成(译者注:即自动生成模型代码)
  2. 缓存与监听查询

通过本教程,你对这两个特性都会有所体会。

与GraphQL交互

与API交互的主要目标通常是:

  • 获取数据
  • 创建、更新和删除数据

在GraphQL中,获取数据通过query完成,而写入数据库通过mutation来达成。

一个mutation看起来很像一个query,同样允许你申明需要服务器返回的信息,这就确保了在一个单程往返中你就可以获取更新后的信息。

考虑如下两个简单例子:

  1. query AllConferences {
  2. allConferences {
  3. id
  4. name
  5. }
  6. }

此query获取所有的会议并返回一个JSON数组,其中每个对象包带有会议的idname

  1. mutation CreateConference {
  2. createConference(name: "WWDC", city: "San Jose", year: "2017") {
  3. id
  4. }
  5. }

此mutation创建了一个新会议并类似地返回其id

如果你对这种语法还没什么感觉,别担心,稍后会探讨更多细节。

准备GraphQL服务器

为了本教程之目的,你将基于一个数据模型使用一个叫做Graphcool的服务来生成一个GraphQL服务器。

说到数据模型,下面是其样子,它的语法叫做GraphQL Interface Definition Language (IDL):

  1. type Conference {
  2. id: String!
  3. name: String!
  4. city: String!
  5. year: String!
  6. attendees: [Attendee] @relation(name: "Attendees")
  7. }
  8. type Attendee {
  9. id: String!
  10. name: String!
  11. conferences: [Conference] @relation(name: "Attendees")
  12. }

GraphQL有它自己的类型系统,你可在其上构建自己的类型。如本例中的ConferenceAttendee类型。每个类型都有一些属性,GraphQL术语叫做字段(field)。注意跟在每个类型后的!,它表示此字段是必须的。

话不多说,开始创建你的GraphQL服务器吧!

通过npm安装Graphcool CLI。打开终端,键入:

  1. npm install -g graphcool

使用graphcool创建服务器:

  1. graphcool init --schema http://graphqlbin.com/conferences.graphql --name ConferencePlanner

这个命令会创建一个叫做ConferencePlanner的Graphcool项目。在此之前,它还会打开一个浏览器窗口,你需要创建一个Graphcool账户(译者注:推荐使用GitHub登录)。一旦建立好,你就能访问GraphQL的全部功能了。

GraphCool command output showing Simple API and Relay API

拷贝Simple API端点留作之后使用。

搞定!你现在能访问完整的GraphQL API并通过Graphcool console管理。

输入初始会议数据

在继续之前,先准备一些初始数据到数据库中。

将前一步中Simple API返回的端点拷贝到浏览器地址栏中。这将打开一个GraphQL Playground,你可在其中用交互式的方式探索API。

添加如下GraphQL代码到Playground左边的输入框,以创建一些初始数据:

  1. mutation createUIKonfMutation {
  2. createConference(name: "UIKonf", city: "Berlin", year: "2017") {
  3. id
  4. }
  5. }
  6. mutation createWWDCMutation {
  7. createConference(name: "WWDC", city: "San Jose", year: "2017") {
  8. id
  9. }
  10. }

这个代码片段包含两个GraphQL mutation。点击Play按钮并通过下拉菜单将每个mutaiton都选择一次(译者注:每次执行一个mutation):

GraphQL playground

这就创建了两个新会议。为了验证已经创建成功,你可以通过Graphcool console中的data browser查看当前数据库的状态,或者发送之前见过的allConferences query。

GraphQL Playground AllConferences query result

配置Xcode并设置Apollo iOS客户端

之前提到,Apollo iOS客户端能做静态类型生成。也就是说,你不再需要写模型来表示来自你应用域名的信息了。作为替代,Apollo iOS客户端使用GraphQL query中的信息来生成对应的Swift类型。

注意:这种方式消除了Swift中JSON解析的不便。由于JSON没有类型,唯一真正安全的解析方式是在Swift类型上使用可选值,因为你不能100%确定JSON数据中包含某个特定类型的属性。

要在Xcode中实现静态类型生成,你要先配置几步:

1. 安装apollo-codegen

apollo-codegen会搜索Xcode项目中的GraphQL代码,并生成Swift类型。

打开终端,输入:

  1. npm install -g apollo-codegen

NPM results from apollo-codegen installation

2. 添加build phase

在Xcode中,选择Project Navigator中的ConferencePlanner。选择名为ConferencePlanner的target。选择Build Phases栏,再点击左上角的+按钮。

从菜单中选择New Run Script Phase

Xcode altering build phases adding new run script

重命名新建的build phase为Generate Apollo GraphQL API。并拖动它到Compile Sources上方。

拷贝如下代码片段到输入框(目前写着Type a script or drag a script file from your workspace to insert its path)中:

  1. APOLLO_FRAMEWORK_PATH="$(eval find $FRAMEWORK_SEARCH_PATHS -name "Apollo.framework" -maxdepth 1)"
  2. if [ -z "$APOLLO_FRAMEWORK_PATH" ]; then
  3. echo "error: Couldn't find Apollo.framework in FRAMEWORK_SEARCH_PATHS; make sure to add the framework to your project."
  4. exit 1
  5. fi
  6. cd "${SRCROOT}/${TARGET_NAME}"
  7. $APOLLO_FRAMEWORK_PATH/check-and-run-apollo-codegen.sh generate $(find . -name '*.graphql') --schema schema.json --output API.swift

检查一下,你的Build Phases应该看起来如下:

Build Script Run Phase Code to Run

3. 添加schema文件

在此你又会用到Simple API的端点。打开终端,输入如下命令(替换其中的__SIMPLE_API_ENDPOINT__为你之前生成的自定义GraphQL端点):

  1. apollo-codegen download-schema __SIMPLE_API_ENDPOINT__ --output schema.json

注意:如果你丢失了GraphQL端点,你可以在Graphcool console中点击左下角的ENDPOINTS按钮找回。
GraphCool Endpoint Console

接下来,将此文件放到Xcode工程的根目录。和AppDelegate.swift在同一个目录,即ConferencePlanner-starter/ConferencePlanner

Finder File Listing showing Schema.json

快速小结一下刚刚做的事:

  • 首先安装apollo-codegen,它用于生成Swift类型
  • 接着,在Xcode中添加一个build phase,它会在编译之前让apollo-codegen介入
  • 之后到你实际的GraphQL guery(稍后添加),apollo-codegen要求项目根目录里有一个上一步已下载的schema文件

初始化ApolloClient

终于要写点儿实际的代码了!

打开AppDelegate.swift,添加如下代码,注意替换其中的__SIMPLE_API_ENDPOINT__为你自己的端点。

  1. import Apollo
  2. let graphQLEndpoint = "__SIMPLE_API_ENDPOINT__"
  3. let apollo = ApolloClient(url: URL(string: graphQLEndpoint)!)

传递了Simple API端点后,ApolloClient就知道该和哪个GraphQL服务器通信了。生成的apollo对象将是你与API打交道的主要界面。

创建与会者并查询会议列表

所有与GraphQL API交互的准备工作都已搞定!首先,确保使用此app的用户能通过取个名字来进行注册。

编写第一个Mutation

在Xcode中的GraphQL group中新建一个文件,记得用Other下的Empty模版,命名为RegisterViewController.graphql

Xcode New File Picker

接下来,添加如下mutation到此文件中:

  1. # 1
  2. mutation CreateAttendee($name: String!) {
  3. # 2
  4. createAttendee(name: $name) {
  5. # 3
  6. id
  7. name
  8. }
  9. }

稍微解释一下:

  1. 这里用了mutation签名(类似Swift的函数)。这个mutation叫做CreateAttendee,接受一个名为name且类型为String的参数。感叹号表示这个参数必须传递。
  2. createAttendee引用了GraphQL API所暴露的一个mutaion。Graphcool Simple API给每个类型都默认提供一个create-mutation。
  3. 此mutation的payload,即你希望服务器执行此mutation后返回的数据。

在下一次构建项目时,apollo-codegen就会找到这些代码并为这个mutation生成Swift表示。用快捷键Cmd+B构建吧。

注意:如果你想给GraphQL代码增加语法高亮,请参考这里的指导。

apollo-codegen初次运行,它会在项目根目录创建一个叫做API.swift的新文件。所有后续的生成只会更新此文件。

生成的API.swift文件位于项目根目录,但你还需要将其添加到Xcode中,拖动它到GraphQL group里。注意不要选中Copy items if needed

Xcode Project Navigator showing API.swift in the GraphQL group

观察API.swift文件的内容,你会看到一个叫做CreateAttendeeMutation的类。它的初始化方法接受一个name变量作为参数。它还有一个嵌套的结构体,叫做Data,它又嵌套另一个结构体,叫做CreateAttendee。这会作为mutation的返回数据,包含attendee的idname

接下来,你要使用此mutation。打开RegisterViewController.swift并实现createAttendee方法如下:

  1. func createAttendee(name: String) {
  2. activityIndicator.startAnimating()
  3. // 1
  4. let createAttendeeMutation = CreateAttendeeMutation(name: name)
  5. // 2
  6. apollo.perform(mutation: createAttendeeMutation) { [weak self] result, error in
  7. self?.activityIndicator.stopAnimating()
  8. if let error = error {
  9. print(error.localizedDescription)
  10. return
  11. }
  12. // 3
  13. currentUserID = result?.data?.createAttendee?.id
  14. currentUserName = result?.data?.createAttendee?.name
  15. self?.performSegue(withIdentifier: "ShowConferencesAnimated", sender: nil)
  16. }
  17. }

上面的代码:

  1. 使用用户提供的名字初始化mutation
  2. 使用apollo实例发送此mutation到API
  3. 接受从服务器返回的数据,并存在全局变量上,代表当前的用户

注意:本教程所有的API调用都遵循如下模式:首先生成一个query或mutation的实例,然后将其传递给ApolloClient,最后在回调中使用结果。

因为允许用户改名,你可添加第二个mutation。打开RegisterViewController.graphql并添加如下代码:

  1. mutation UpdateAttendeeName($id: ID!, $newName: String!) {
  2. updateAttendee(id: $id, name: $newName) {
  3. id
  4. name
  5. }
  6. }

按下Cmd+Bapollo-codegen为此mutation生成Swift代码。之后,打开RegisterViewController.swift并替换updateAttendee如下:

  1. func updateAttendee(id: String, newName: String) {
  2. activityIndicator.startAnimating()
  3. let updateAttendeeNameMutation = UpdateAttendeeNameMutation(id: id, newName: newName)
  4. apollo.perform(mutation: updateAttendeeNameMutation) { [weak self] result, error in
  5. self?.activityIndicator.stopAnimating()
  6. if let error = error {
  7. print(error.localizedDescription)
  8. return
  9. }
  10. currentUserID = result?.data?.updateAttendee?.id
  11. currentUserName = result?.data?.updateAttendee?.name
  12. self?.performSegue(withIdentifier: "ShowConferencesAnimated", sender: nil)
  13. }
  14. }

这些代码和createAttendee几乎一样,除了要传递一个id来指代你要更新的是哪个用户。

编译并运行,输入一个名字,然后点击Save按钮。一个新的attentee就在GraphQL后端被创建好了。

User Settings page for the application

你可在Playground中验证,发送allAttendees query即可:

GraphCool Playground showing AllAttendees query

查询所有会议

下一步要在ConferencesTableViewController中显示所有的会议。

在GraphQL group中新建一个文件,命名为ConferenceTableViewController.graphql并添加如下代码:

  1. fragment ConferenceDetails on Conference {
  2. id
  3. name
  4. city
  5. year
  6. attendees {
  7. id
  8. }
  9. }
  10. query AllConferences {
  11. allConferences {
  12. ...ConferenceDetails
  13. }
  14. }

这里的fragment是什么东西?

简单来说,Fragments是可重用的零件,它能包装GraphQL类型的一些字段。它们与静态类型一起使用非常方便,因为它们增强了GraphQL服务器返回的信息的可重用性,并且每个片段将由其自己的结构体表示。

Fragment能通过...加上片段名集成到任何query或mutation中。当AllConferencesquery被发出时,...ConferenceDetails就被ConferenceDetails片段中包含的所有字段替换。(译者注:就像宏替换一样)

接下来该使用query填充table view了。

按下Cmd+B以确保生成新的query和fragment的Swift代码,然后打开ConferencesTableViewController.swift并添加如下属性到其顶部:

  1. var conferences: [ConferenceDetails] = [] {
  2. didSet {
  3. tableView.reloadData()
  4. }
  5. }

viewDidLoad最后,添加如下代码以发送query并显示结果:

  1. let allConferencesQuery = AllConferencesQuery()
  2. apollo.fetch(query: allConferencesQuery) { [weak self] result, error in
  3. guard let conferences = result?.data?.allConferences else { return }
  4. self?.conferences = conferences.map { $0.fragments.conferenceDetails }
  5. }

和你在第一个mutation所看到的一样,你使用的是同样的模式,不过这次你发送的是一个query。在初始化此query之后,你将其传递给apollo实例并在回调中获取会议列表。这个列表的类型是[AllConferencesQuery.Data.AllConference],所以为了使用这个信息,你要访问每个元素的fragments并映射出ConferenceDetails

剩下的就是告知UITableView如何显示会议数据了。

打开ConferenceCell.swift并添加如下属性:

  1. var conference: ConferenceDetails! {
  2. didSet {
  3. nameLabel.text = "(conference.name) (conference.year)"
  4. let attendeeCount = conference.numberOfAttendees
  5. infoLabel.text =
  6. "\(conference.city) (\(attendeeCount) \(attendeeCount == 1 ? "attendee" : "attendees"))"
  7. }
  8. }

注意代码还不能通过编译,因为numberOfAttendees还不可用。你稍后会修复它。

接下来,打开ConferencesTableViewController.swift并替换UITableViewDataSource实现如下:

  1. override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
  2. return conferences.count
  3. }
  4. override func tableView(_ tableView: UITableView,
  5. cellForRowAt indexPath: IndexPath) -> UITableViewCell {
  6. let cell = tableView.dequeueReusableCell(withIdentifier: "ConferenceCell") as! ConferenceCell
  7. let conference = conferences[indexPath.row]
  8. cell.conference = conference
  9. cell.isCurrentUserAttending = conference.isAttendedBy(currentUserID!)
  10. return cell
  11. }

这是标准的UITableViewDataSource实现。然而,编译器会抱怨不能在ConferenceDetails中找到isAttendedBy

numberOfAttendeesisAttendedBy都表示了有用的信息,我们希望它们作为“模型”的工具函数。然而,记得吗,ConferenceDetails是自动生成在_API.swift里的。你不应该手动修改这个文件,因为你的改动会在Xcode下次构建时被覆盖!

逃离此困境的方法之一是在另外一个文件中创建一个extension来实现你想要的功能。打开Utils.swift并添加如下extension:

  1. extension ConferenceDetails {
  2. var numberOfAttendees: Int {
  3. return attendees?.count ?? 0
  4. }
  5. func isAttendedBy(_ attendeeID: String) -> Bool {
  6. return attendees?.contains(where: { $0.id == attendeeID }) ?? false
  7. }
  8. }

运行app,你会看到之前添加的会议都出现在列表中。

Listing of the conferences

显示会议的详细信息

ConferenceDetailViewController会显示被选中的会议的详细信息,包括与会者的列表。

你将通过编写GraphQL query并生成Swift类型来准备。

创建一个新文件ConferenceDetailViewController.graphql并添加如下代码:

  1. query ConferenceDetails($id: ID!) {
  2. conference: Conference(id: $id) {
  3. ...ConferenceDetails
  4. }
  5. }
  6. query AttendeesForConference($conferenceId: ID!) {
  7. conference: Conference(id: $conferenceId) {
  8. id
  9. attendees {
  10. ...AttendeeDetails
  11. }
  12. }
  13. }
  14. fragment AttendeeDetails on Attendee {
  15. id
  16. name
  17. _conferencesMeta {
  18. count
  19. }
  20. }

在第一个query中,你提供一个id来请求特定的会议。第二个query返回此会议所有的与会者,对于每一个与会者,所有信息都由AttendeeDetails指定,包括idname,以及与会者参加的所有会议的数量。

The _conferencesMeta field in AttendeeDetails fragment allows you to retrieve additional information about the relation. Here you’re asking for the number of attendees using count.

AttendeeDetailsfragment中的_conferencesMeta字段让你可以获取额外的关系信息。你通过使用count得到与会者所有会议的数量。(译者注:这里的原文是”Here you’re asking for the number of attendees using count“,疑似有误)

构建应用生成Swift类型。

接下来,打开ConferenceDetailViewController.swift并添加如下属性到IBOutlet声明的下方:

  1. var conference: ConferenceDetails! {
  2. didSet {
  3. if isViewLoaded {
  4. updateUI()
  5. }
  6. }
  7. }
  8. var attendees: [AttendeeDetails]? {
  9. didSet {
  10. attendeesTableView.reloadData()
  11. }
  12. }
  13. var isCurrentUserAttending: Bool {
  14. return conference?.isAttendedBy(currentUserID!) ?? false
  15. }

头两个属性实现的didSet属性监听器确保UI的即时更新。最后一个计算当前用户参与的会议的意图是否被显示。

updateUI方法将用选择的会议信息来配置UI元素。实现如下:

  1. func updateUI() {
  2. nameLabel.text = conference.name
  3. infoLabel.text = "(conference.city), (conference.year)"
  4. attendingLabel.text = isCurrentUserAttending ? attendingText : notAttendingText
  5. toggleAttendingButton.setTitle(isCurrentUserAttending ? attendingButtonText : notAttendingButtonText, for: .normal)
  6. }

最后,在ConferenceDetailViewcontroller.swift中替换tableView(_:numberOfRowsInSection:)tableView(_:cellForRowAt:)的实现如下:

  1. func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
  2. return attendees?.count ?? 0
  3. }
  4. func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
  5. guard let attendees = self.attendees else { return UITableViewCell() }
  6. let cell = tableView.dequeueReusableCell(withIdentifier: "AttendeeCell")!
  7. let attendeeDetails = attendees[indexPath.row]
  8. cell.textLabel?.text = attendeeDetails.name
  9. let otherConferencesCount = attendeeDetails.numberOfConferencesAttending - 1
  10. cell.detailTextLabel?.text = "attends (otherConferencesCount) other conferences"
  11. return cell
  12. }

如之前所见,编译器会抱怨AttendeeDetails中没有numberOfConferencesAttending。你同样通过将其实现在extension中来修复。

打开Utils.swift添加如下extension:

  1. extension AttendeeDetails {
  2. var numberOfConferencesAttending: Int {
  3. return conferencesMeta.count
  4. }
  5. }

最后通过在viewDidLoad中加载数据来完成ConferenceDetailViewController

  1. let conferenceDetailsQuery = ConferenceDetailsQuery(id: conference.id)
  2. apollo.fetch(query: conferenceDetailsQuery) { result, error in
  3. guard let conference = result?.data?.conference else { return }
  4. self.conference = conference.fragments.conferenceDetails
  5. }
  6. let attendeesForConferenceQuery = AttendeesForConferenceQuery(conferenceId: conference.id)
  7. apollo.fetch(query: attendeesForConferenceQuery) { result, error in
  8. guard let conference = result?.data?.conference else { return }
  9. self.attendees = conference.attendees?.map { $0.fragments.attendeeDetails }
  10. }

最后,你需要传递被选中会议的信息给ConferenceDetailViewController,这刚好可在segue被执行时搞定。

打开ConferencesTableViewController.swift并实现prepare(for:sender:)如下:

  1. override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
  2. let conferenceDetailViewController = segue.destination as! ConferenceDetailViewController
  3. conferenceDetailViewController.conference = conferences[tableView.indexPathForSelectedRow!.row]
  4. }

好了!运行app并从列表中选择一个会议。在详细界面,你会看到被选中会议的细节信息。

入门 GraphQL & Apollo - 图22

改变参加意愿后自动更新UI

使用Apollo iOS客户端的一大优势是它对之前查询的规范化和缓存。当发送一个mutation时,它知道数据中的哪些比特改变了,然后特定地更新缓存,而不需要重新发送原始的query。一个很棒的副作用是它允许“自动UI更新”,接下来体会。

ConferenceDetailViewController中,有个按钮允许用户修改他们参加会议的意愿。为了改变后端的状态,你需要先在ConferenceDetailViewController.graphql中创建两个mutation:

  1. mutation AttendConference($conferenceId: ID!, $attendeeId: ID!) {
  2. addToAttendees(conferencesConferenceId: $conferenceId, attendeesAttendeeId: $attendeeId) {
  3. conferencesConference {
  4. id
  5. attendees {
  6. ...AttendeeDetails
  7. }
  8. }
  9. }
  10. }
  11. mutation NotAttendConference($conferenceId: ID!, $attendeeId: ID!) {
  12. removeFromAttendees(conferencesConferenceId: $conferenceId, attendeesAttendeeId: $attendeeId) {
  13. conferencesConference {
  14. id
  15. attendees {
  16. ...AttendeeDetails
  17. }
  18. }
  19. }
  20. }

第一个mutation用于添加一个与会者到会议;第二个移除一个与会者。

构建应用确保所有的mutation的Swift类型都被创建。

打开ConferenceDetailViewController.swift并替换attendingButtonPressed方法如下:

  1. @IBAction func attendingButtonPressed() {
  2. if isCurrentUserAttending {
  3. let notAttendingConferenceMutation =
  4. NotAttendConferenceMutation(conferenceId: conference.id,
  5. attendeeId: currentUserID!)
  6. apollo.perform(mutation: notAttendingConferenceMutation, resultHandler: nil)
  7. } else {
  8. let attendingConferenceMutation =
  9. AttendConferenceMutation(conferenceId: conference.id,
  10. attendeeId: currentUserID!)
  11. apollo.perform(mutation: attendingConferenceMutation, resultHandler: nil)
  12. }
  13. }

如果你现在运行app,你将能改变参加的某个会议的状态(你可通过在Graphcool console的数据浏览器中验证)。但目前这个改变还不会反映到UI上。

别担心,Apollo iOS客户端会罩着你!通过使用GraphQLQueryWatcher,你能监听mutation带来的改变。要使用GraphQLQueryWatcher,只需做如下少量修改。

首先,打开ConferenceDetailViewController.swift并添加两个属性在顶部:

  1. var conferenceWatcher: GraphQLQueryWatcher<ConferenceDetailsQuery>?
  2. var attendeesWatcher: GraphQLQueryWatcher<ConferenceDetailsQuery>?

接下来,你要修改viewDidLoad中发送query的方式,使用方法watch替换fetch,并将返回值赋给上面创建的属性:

  1. ...
  2. let conferenceDetailsQuery = ConferenceDetailsQuery(id: conference.id)
  3. conferenceWatcher = apollo.watch(query: conferenceDetailsQuery) { [weak self] result, error in guard let conference = result?.data?.conference else { return }
  4. self?.conference = conference.fragments.conferenceDetails
  5. }
  6. ...

以及

  1. ...
  2. let attendeesForConferenceQuery = AttendeesForConferenceQuery(conferenceId: conference.id)
  3. attendeesWatcher = apollo.watch(query: attendeesForConferenceQuery) { [weak self] result, error in
  4. guard let conference = result?.data?.conference else { return }
  5. self?.attendees = conference.attendees?.map { $0.fragments.attendeeDetails }
  6. }
  7. ...

ConferenceDetailsQuery相关的数据或与AttendeesForConferenceQuery在缓存中的修改相关的数据,都会导致传递给watch的尾随闭包的的执行,从而更新UI。

最后一件为了让watchers正常工作的事是实现ApolloClient实例的cacheKeyForObject方法。这个方法告诉Apollo你想如何唯一识别它放入缓存的对象。在此例中,检查属性id就很好。

实现cacheKeyForObject的绝佳之处是在app初次启动时。打开AppDelegate.swift并添加如下代码到application(_:didFinishLaunchingWithOptions:)return语句前:

  1. apollo.cacheKeyForObject = { $0["id"] }

注意:如果你想知道更多关于为何这是必要的以及Apollo缓存的工作方式,你可阅读 Apollo blog

再次运行app,并修改你参加会议的意图,现在UI会立即更新。然而,当你回到ConferencesTableViewController,你还不会看到会议cell中状态的更新。

入门 GraphQL & Apollo - 图24

为了修复此问题,同样是用一个GraphQLQueryWatcher。打开ConferencesTableViewController.swift并增加如下属性到类的顶部:

  1. var allConferencesWatcher: GraphQLQueryWatcher?

接着,更新viewDidLoad中的query:

  1. ...
  2. let allConferencesQuery = AllConferencesQuery()
  3. allConferencesWatcher = apollo.watch(query: allConferencesQuery) { result, error in
  4. guard let conferences = result?.data?.allConferences else { return }
  5. self.conferences = conferences.map { $0.fragments.conferenceDetails }
  6. }
  7. ...

这就确保了传递给watch的尾随闭包在每次缓存中与AllConferencesQuery相关的改变发生时自动执行。

接下来如何?

看看此教程的final project,也许你需要对比一下。

如果你想学到更多关于GraphQL的知识,你可以开始阅读它良好的文档,或者订阅GraphQL weekly

更多关于GraphQL社区的精彩内容可在ApolloGraphcool的博客中找到。

作为一个挑战,你可实现添加新会议的功能。这个特性同样已包含在样本代码中。

我们希望你学习GraphQL时感到有趣!请让我们知道你对这种新的API范式的想法,加入下面的论坛讨论。


欢迎转载,但请一定注明出处! https://github.com/nixzhu/dev-blog