翻译自:Introduction to Protocol Buffers on iOSpython
对大多数的应用来讲,后台服务、传输和存储数据都是个重要的模块。开发者在给一个 web service 写接口时,一般使用 JSON 或者 XML 来发送和接收数据,而后根据这些数据生成结构并解析。ios
尽管有大量的 API 和框架帮助咱们序列化和反序列化,来支持一些后台接口开发的平常工做,好比说更新代码或者解析器来支持后台的模型变化。git
可是若是你真的想提高你的新项目的健壮性的话 ,考虑下用 protocol buffers,它是由 Google 开发用来序列化数据结构的一种跨语言的方法。在不少状况下,它比传统的 JSON 和 XML 更加灵活有效。其中一个关键的特色就是,你只须要在其支持的任何语言和编译器下,定义一次数据结构——包括 Swift! 建立的类文件就能够很轻松的读写成对象。github
在这篇教程中,会使用一个 Python 服务端与一个 iOS 程序交互。你会学到 protocol buffers 是如何工做,如何配置环境,最后怎样使用 protocol buffers 传输数据。web
怎么,仍是不相信 protocol buffers 就是你所须要的东西?接着往下读吧。json
注意:这篇教程是基于你已经有了必定的 iOS 和 Swift 经验,同时有必定的基本的服务端和 terminal 基础。 同时,确保你使用的是苹果的 Xcode 8.2或之后的版本.flask
##准备开始 RWCards这个APP能够用来查看你的会议门票和演讲者名单。下载Starter Project并打开根目录Starter。先熟悉一下这下面这三部分: #####The Client 在 Starter/RWCards下,打开 RWCards.xcworkspace,咱们来看看这几个主要的文件:swift
整个工程使用 CocoaPods 来拉取这两个框架:后端
注意:这篇教程中你会用到 Swift Protobuf 0.9.24 和 Google’s Protoc Compiler 3.1.0. 它们已经打包在项目里了,因此你不须要再作别的。api
开始使用 protocol buffers 前,首先要定义一个 .proto 文件。在这个文件中指定了你的数据结构信息。下面是一个 .proto 文件的示例:
syntax = "proto3";
message Contact {
enum ContactType {
SPEAKER = 0;
ATTENDANT = 1;
VOLUNTEER = 2;
}
string first_name = 1;
string last_name = 2;
string twitter_name = 3;
string email = 4;
string github_link = 5;
ContactType type = 6;
string imageName = 7;
};
复制代码
这个文件里定义了一个 Contact 的 message 和它的相关属性。
.proto 文件定义好了后,你只须要把这个文件交给 protocol buffer 的编译器,编译器会用你选择的语言建立好一个数据类(Swift 中的 结构)。你能够直接在项目中使用这个类/结构,很是简单!
JSON 和 XML 多是目前开发者们用来存储和传输数据的标准方案,而 protocol buffers 与之相比有如下优点:
Protocol buffers 虽然有着诸多优点,可是它也不是万能的:
尽管并非适合于全部的状况,但 protocol buffers 确确实实有着不少的优点。 把程序运行起来试试看吧。
Head back to Finder and look inside Starter/ProtoSchema. You’ll see the following files: 打开 Starter/ProtoSchema 目录,你会看到这些文件:
Starter/Server 目录下包括:
RWServer.py 是放在Flask上的一个 Python 服务。包含两个 GET 请求:
RWDict.py 包含了 RWServer 将要读取的演讲者列表数据.
如今是时候配置环境来运行 protocol buffers 了。在下面的章节中,你会建立好运行 Google 的 protocol buffer编译器环境,Swift 的 Protobuf 插件,并安装 Flask 来运行你的 Python 服务。
在使用 protocol buffers 以前须要安装许多的工具和库。starter 项目中包含了一个名为 protoInstallation.sh 的脚本帮你搞定了这些。它会在安装以前检查是否已经安装过这些库。 这个脚本须要花一点时间来安装,尤为是安装 Google 的 protocol buffer 库。打开你的终端,cd 命令进入到 Starter 目录执行下面这个命令:
$ ./protoInstallation.sh
复制代码
注意:执行的过程当中你可能会被要求输入管理员密码。
脚本执行完成后,再运行一次以确保的到如下输出结果:
若是你看到这些,那表示脚本已经执行完毕。若是脚本执行失败了,那检查下你是否是输入了错误的管理员密码。并从新运行脚本;它不会从新安装那些已经成功的库。 这个脚本作了这些事:
注意:你能够用编辑器打开 protoInstallation.sh 文件来了解这个脚本是如何工做的。这须要必定的 bash 基础。
好了,如今你已经作好了使用 protocol buffers 的全部准备工做。
.proto 文件定义了 protocol buffer 描述你的数据结构的 message。把这个文件中的内容传递给 protocol buffer 编译器后,编译器会生成你的数据结构。
注意:在这篇教程中,你将使用 proto3 来定义 message,这是 protocol buffer 语言的最新版本。能够访问Google’s guidelines以获取更多的 proto3 的信息。
用你最习惯的编辑器打开 ProtoSchema/contact.proto ,这里已经定义好了演讲者的 message:
syntax = "proto3";
message Contact { // 1
enum ContactType { // 2
SPEAKER = 0;
ATTENDANT = 1;
VOLUNTEER = 2;
}
string first_name = 1; //3
string last_name = 2;
string twitter_name = 3;
string email = 4;
string github_link = 5;
ContactType type = 6;
string imageName = 7;
};
message Speakers { // 4
repeated Contact contacts = 1;
};
复制代码
咱们来看一下这里面包含了哪些内容:
The Contact model describes a person’s contact information. This will be displayed on their badges in the app.
##生成 Swift 结构 把 contact.proto 传递给 protoc 程序,proto 文件中的 message 将会被转化生成 Swift 的结构。这些结构会遵循 ProtobufMessage.protoc 并提供 Swift 中构造、方法来序列化和反序列化数据的途径。
注意:想了解更多关于 Swift 的 protobuf API, 访问苹果的 Protobuf API documentation.
在终端中,进入** Starter/ProtoSchema **目录,用编辑器打开 protoScript.sh,你会看到:
#!/bin/bash
echo 'Running ProtoBuf Compiler to convert .proto schema to Swift'
protoc --swift_out=. contact.proto // 1
echo 'Running Protobuf Compiler to convert .proto schema to Python'
protoc -I=. --python_out=. ./contact.proto // 2
复制代码
这个脚本对 contact.proto 文件执行了两次 protoc 命令,分别建立了 Swift 和 Python 的源文件。 回到终端,执行下面的命令:
$ ./protoScript.sh
复制代码
你会看到如下输出结果:
Running ProtoBuf Compiler to convert .proto schema to Swift
protoc-gen-swift: Generating Swift for contact.proto
Running Protobuf Compiler to convert .proto schema to Python
复制代码
你已经建立好了 Swift 和 Python 的源文件。 在 ** ProtoSchema** 目录下,你会看到一个 Swift 和一个 Python 文件。同时分别还有一个对应的 .pb.swift 和 .pb.py. pb 前缀表示这是 protocol buffer 生成的类。
把 contact.pb.swift 拖到 Xcode 的 project navigator 下的 Protocol Buffer Objects 组. 勾上“Copy items if needed”选项。同时将 contact_pb2.py 拷贝到 Starter/Server 目录。 看一眼 ** contact.pb.swift** 和 contact_pb2.py中的内容,看看 proto message 是如何转换成目标语言的。 如今你已经有了生成好的模型对象了,能够开始集成了! ##运行本地服务器 示例代码中包含了一个 Python 服务。这个服务提供了两个 GET 请求:一个用来获取参会者的名牌信息,另外一个用来列出演讲者。 这个教程不会深刻讲解服务端的代码。尽管如此,你须要了解到它用到了由 protocol buffer 编译器生成的 contact_pb2.py 模型文件。若是你感兴趣,能够看一看 RWServer.py 中的代码,不看也无妨(手动滑稽)。 打开终端并 cd 至 Starter/Server 目录,运行下面的命令:
$ python RWServer.py
复制代码
运行结果以下:
经过在浏览器中发起 HTTP 请求,你能够看到 protocol buffer 的原数据。 在浏览器中打开 http://127.0.0.1:5000/currentUser 你会看到:
再试试演讲者的接口,http://127.0.0.1:5000/speakers:
注意:测试 RWCards app的过程当中你能够退出、停止和重启本地服务以便调试。
如今你已经运行了本地服务器,它使用的是由 proto 文件生成的模型,是否是很cooool?
如今你已经把本地服务器跑起来了,是时候在 app 中发起服务请求了。**RWService.swift **文件中将 RWService 类替换成下面的代码:
class RWService {
static let shared = RWService() // 1
let url = "http://127.0.0.1:5000"
private init() { }
func getCurrentUser(_ completion: @escaping (Contact?) -> ()) { // 2
let path = "/currentUser"
Alamofire.request("\(url)\(path)").responseData { response in
if let data = response.result.value { // 3
let contact = try? Contact(protobuf: data) // 4
completion(contact)
}
completion(nil)
}
}
}
复制代码
这个类将用来与你的 Python 服务器进行交互。你已经实现了获取当前用户的请求:
解码数据只须要把 protocol buffer 的数据传递给对象的构造器便可,不须要其余的解析。 Swift 的 protocol buffer 库帮你处理了全部的事情。 如今请求已经完成,能够展现数据了。
打开 CardViewController.swift 文件并在 viewWillAppear(_:) 以后添加下面这些代码:
func fetchCurrentUser() { // 1
RWService.shared.getCurrentUser { contact in
if let contact = contact {
self.configure(contact)
}
}
}
func configure(_ contact: Contact) { // 2
self.attendeeNameLabel.attributedText = NSAttributedString.attributedString(for: contact.firstName, and: contact.lastName)
self.twitterLabel.text = contact.twitterName
self.emailLabel.text = contact.email
self.githubLabel.text = contact.githubLink
self.profileImageView.image = UIImage(named: contact.imageName)
}
复制代码
这些方法会帮你取得服务端传过来的数据,并用来配置名片:
用起来很简单,可是还须要拿到一个 ContactType 枚举用来区分参会者的类型。
你须要添加一个方法来把枚举类型转换成 string, 这样名片页面才能显示 SPEAKER 而不是一个数字0. 可是这有个问题,若是不从新生成 .proto 文件来更新 message,怎样才能往模型里添加新功能呢?
extension Contact {
func contactTypeToString() -> String {
switch type {
case .speaker:
return "SPEAKER"
case .attendant:
return "ATTENDEE"
case .volunteer:
return "VOLUNTEER"
default:
return "UNKNOWN"
}
}
}
复制代码
contactTypeToString() 方法将 ContactType
映射成了一个对应的显示用的字符串。 打开 CardViewController.swift 并添加下面的代码到 configure(_:):
self.attendeeTypeLabel.text = contact.contactTypeToString()
复制代码
将表明contact type的字符串传递给了 * attendeeTypeLabel*。 最后在 viewWillAppear(_:) 中,applyBusinessCardAppearance() 以后添加下面代码:
if isCurrentUser {
fetchCurrentUser()
} else {
// TODO: handle speaker
}
复制代码
My Badge 选项卡完成后,咱们来看看 Speakers 选项卡。 打开 RWService.swift 并添加下面的代码:
func getSpeakers(_ completion: @escaping (Speakers?) -> ()) { // 1
let path = "/speakers"
Alamofire.request("\(url)\(path)").responseData { response in
if let data = response.result.value { // 2
let speakers = try? Speakers(protobuf: data) // 3
completion(speakers)
}
}
completion(nil)
}
复制代码
看上去很熟悉是吧,它和 getCurrentUser(_:) 相似,不过他获取的是 Speakers 对象,包含了一个 contact 的数组,用于表示回忆的演讲者。 打开 SpeakersViewModel.swift 并将代码替换为:
class SpeakersViewModel {
var speakers: Speakers!
var selectedSpeaker: Contact?
init(speakers: Speakers) {
self.speakers = speakers
}
func numberOfRows() -> Int {
return speakers.contacts.count
}
func numberOfSections() -> Int {
return 1
}
func getSpeaker(for indexPath: IndexPath) -> Contact {
return speakers.contacts[indexPath.item]
}
func selectSpeaker(for indexPath: IndexPath) {
selectedSpeaker = getSpeaker(for: indexPath)
}
}
复制代码
SpeakersListViewController 显示了一个参会者的列表,SpeakersViewModel中包含了这些数据:从 /speakers 接口中获取的contact对象组成的数组。 SpeakersListViewController将在每一行中显示一个speaker。 viewmodel建立好了以后,就该配置cell了。打开 SpeakerCell.swift,添加下面的代码到 SpeakerCell:
func configure(with contact: Contact) {
profileImageView.image = UIImage(named: contact.imageName)
nameLabel.attributedText = NSAttributedString.attributedString(for: contact.firstName, and: contact.lastName)
}
复制代码
传入了一个contact对象而且经过其属性来配置cell的 image 和 label。这个cell会显示演讲者的照片,和他的名字。 接下来,打开 SpeakersListViewController.swift
并添加下面的代码到 *viewWillAppear(_:)*中:
RWService.shared.getSpeakers { [unowned self] speakers in
if let speakers = speakers {
self.speakersModel = SpeakersViewModel(speakers: speakers)
self.tableView.reloadData()
}
}
复制代码
getSpeakers(_:)发起了一个请求去获取演讲者列表的数据,建立了一个 * SpeakersViewModel 的对象,并返回 speakers。 tableview 接下来会更新这些获取到的数据。 你须要给 tableview 的每一行指定一个speaker用于显示。替换tableView(_:cellForRowAt:)*的代码:
let cell = tableView.dequeueReusableCell(withIdentifier: "SpeakerCell", for: indexPath) as! SpeakerCell
if let speaker = speakersModel?.getSpeaker(for: indexPath) {
cell.configure(with: speaker)
}
return cell
复制代码
getSpeaker(for:) 根据当前列表的 indexPath返回 speaker数据,经过cell的*configure(with:)*配置cell。 当点击列表中的一个cell时,你须要跳转到 CardViewController 展现选择的演讲者信息,打开 CardViewController.swift 并在类中添加这些属性:
var speaker: Contact?
复制代码
后面会用到这个属性用来传递选择的演讲者。将*// TODO: handle speaker*替换为:
if let speaker = speaker {
configure(speaker)
}
复制代码
这个判断用来肯定 speaker 是否已经填充过了,若是是,调用 configure(),在名片上更新演讲者的信息。 回到 SpeakersListViewController.swift 传递选择的 speaker。在 *tableView(_:didSelectRowAt:)*中, performSegue(withIdentifier:sender:) 上方添加:
speakersModel?.selectSpeaker(for: indexPath)
复制代码
将 speakersModel 中的对应 speaker 标记为选中。 接下来,在*prepare(for:sender:)*的 vc.isCurrentUser = false: 以后添加下面的代码:
vc.speaker = speakersModel?.selectedSpeaker
复制代码
这里讲 selectedSpeaker 传递给了 * CardViewController* 来显示。 确保你的本地服务还在运行当中,build & run Xcode。你会看到 app 已经集成了用户名片,同时显示了演讲者的信息。
你能够从 这里下载到完成的工程。 在这篇教程中,你已经学习到了 protocol buffer 的基本特征, 怎样定义一个 .proto 文件并经过编译器生成 Swift 文件。还学习了如何使用Flask 建立一个简单的本地服务器,并使用这个服务发送 protocol buffer 的二进制数据给客户端,以及如何轻松地去反序列化数据。 protocol buffers 还有更多的特性,好比说在 message 中定义映射和处理向后兼容。若是你对这些感兴趣,能够查看 Google 的文档。
最后值得一提的是,Remote Procedure Calls这个项目使用了 protocol buffers 而且看起来很是不错,访问GRPC了解更多吧。