Introduction to Protocol Buffers on iOS
Protocol buffers are a language-agnostic method for serializing structured data that can be used as an alternative to XML or JSON in your iOS apps. By Vincent Ngo.
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
Introduction to Protocol Buffers on iOS
25 mins
- Getting Started
- The Client
- How do Protocol Buffers Work?
- Benefits
- Limitations
- Protocol Buffer Schema
- The Backend
- Environment Setup
- Defining a .proto File
- Generating Swift Structs
- Running the Local Server
- Testing GET Requests
- Making the Service Calls
- Integrate the Attendee’s Badge
- Customizing Protocol Buffer Objects
- Integrate the List of Speakers
- Where to Go From Here?
Running the Local Server
The sample project contains a prebuilt Python server. This server will provide two GET calls: one to retrieve the attendee’s badge information, and another to list the speakers.
This tutorial won’t get into the server code. However, it’s important to note that it leverages the contact_pb2.py model file you generated with the protocol buffer compiler. Feel free to take a closer look at RWServer.py if you are interested in the specifics of this, but it isn’t necessary to do this to follow along with this tutorial.
To run the server, open Terminal and navigate to Starter/Server. Now run the following command:
$ python RWServer.py
You should see the following:
Testing GET Requests
By running the HTTP request in a browser, you can see the protocol buffer’s raw data format.
Visit http://127.0.0.1:5000/currentUser and you’ll see the following:
Next try the speaker call, http://127.0.0.1:5000/speakers:
Note: You can either leave the local server running, or stop it and run it again when testing the RWCards app.
Note: You can either leave the local server running, or stop it and run it again when testing the RWCards app.
You’re now running a simple server that leverages models built from messages defined in your proto file. Pretty powerful stuff!
Making the Service Calls
Now that you have your local server up and running, it’s time to call the services within your app. In RWService.swift replace the existing RWService
class with the following:
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)
}
}
}
This class will be used for network interaction with your Python server. You’ve started off by implementing the currentUser
call. Here’s what this does:
-
shared
is a singleton you’ll use to access network calls. -
getCurrentUser(_:)
makes a request to get the current user’s data via the/currentUser
endpoint. This is a hard-coded user in the simple backend you have running. - The
if let
unwraps the response value. - The data returned is a binary representation of the protocol buffer. The
Contact
initializer takes thisdata
as input, decoding the received message.
Decoding an object with protocol buffer is straightforward as calling the object’s initializer and passing in data
. No parsing required. The Swift Protobuf library handles all of that for you!
Now that you have your service up, it’s time to display the information.
Integrate the Attendee’s Badge
Open CardViewController.swift and add the following methods after 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)
}
These methods will help fetch data from the server and use it to configure the badge. Here’s how they work:
-
fetchCurrentUser()
calls the service to fetch the current user’s info, and configuresCardViewController
with thecontact
. -
configure(_:)
takes aContact
and sets all the UI components in the controller.
You’ll use these shortly, but first you need to derive a readable representation of attendee type from the ContactType
enum.
Customizing Protocol Buffer Objects
You need to add a method to convert the enum type to a string so the badge can display SPEAKER rather than 0.
But there’s a problem. If you need to regenerate the .proto file every time you update the message, how do you add custom functionality to the model?
Swift extensions are well suited for this purpose. They allow you to add to a class without modifying its source code.
Create a new file named contact+extension.swift and add it in the Protocol Buffer Objects group folder. Add the following code to that file:
extension Contact {
func contactTypeToString() -> String {
switch type {
case .speaker:
return "SPEAKER"
case .attendant:
return "ATTENDEE"
case .volunteer:
return "VOLUNTEER"
default:
return "UNKNOWN"
}
}
}
contactTypeToString()
maps a ContactType
to a string representation for display.
Open CardViewController.swift and add the following line in configure(_:)
:
self.attendeeTypeLabel.text = contact.contactTypeToString()
This populates attendeeTypeLabel
with the string representation of the contact type.
Lastly, add the following after applyBusinessCardAppearance()
in viewWillAppear(_:)
:
if isCurrentUser {
fetchCurrentUser()
} else {
// TODO: handle speaker
}
isCurrentUser
is currently hard-coded to true
, and will be modified when support for speakers is added. fetchCurrentUser()
is called in this default case, fetching and populating the card with the current user’s information.
Build and run to see the attendee’s badge!
Integrate the List of Speakers
With the My Badge tab done, it’s time to turn your attention to the currently blank Speakers tab.
Open RWService.swift and add the following method to the class:
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)
}
This should look familiar; it’s similar to the way getCurrentUser(_:)
works, except it fetches Speakers
. Speakers
contain an array of Contact
objects, representing all of the conference speakers.
Open SpeakersViewModel.swift and replace the current implementation with the following:
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)
}
}
This acts as a data source for SpeakersListViewController
, which displays a list of conference speakers. speakers
consists of an array of Contacts
and will be populated by the response of the /speakers
endpoint. The datasource implementation returns a single Contact
for each row.
Now that the view model is set up, you will next configure the cell. Open SpeakerCell.swift and add the following code in SpeakerCell
:
func configure(with contact: Contact) {
profileImageView.image = UIImage(named: contact.imageName)
nameLabel.attributedText = NSAttributedString.attributedString(for: contact.firstName, and: contact.lastName)
}
This takes a Contact
and uses its properties to set the cell’s image and label. The cell will show an image of the speaker, as well as their first and last name.
Next, open SpeakersListViewController.swift and add the following code in viewWillAppear(_:)
, below the call to super
:
RWService.shared.getSpeakers { [unowned self] speakers in
if let speakers = speakers {
self.speakersModel = SpeakersViewModel(speakers: speakers)
self.tableView.reloadData()
}
}
getSpeakers(_:)
makes a request to get the list of speakers. An SpeakersViewModel
is initialized with the returned speakers
. The tableView
is then refreshed to update with the newly fetched data.
Now for every row in the table view, you need to assign a speaker to display. Replace the code in tableView(_:cellForRowAt:)
with the following:
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:)
returns the contact associated with the current table indexPath
. configure(with:)
is the method you defined on SpeakerCell
for setting the speaker’s image and name in the cell.
When a cell is tapped in the speaker list, you want to display the selected speaker in the CardViewController
. To start, open CardViewController.swift and add the following property to the top of the class:
var speaker: Contact?
You’ll eventually use this to pass along the selected speaker. Once you have it, you’ll want to display it. Replace // TODO: handle speaker
with:
if let speaker = speaker {
configure(speaker)
}
This checks to see if speaker
is populated, and if so, calls configure()
on it. That in turn updates the card with the speaker’s information.
Now head back to SpeakersListViewController.swift to pass along the selected speaker. First add the following code in tableView(_:didSelectRowAt:)
, above performSegue(withIdentifier:sender:)
:
speakersModel?.selectSpeaker(for: indexPath)
This flags the speaker the user selected in the speakersModel
.
Next, add the following in prepare(for:sender:)
under vc.isCurrentUser = false
:
vc.speaker = speakersModel?.selectedSpeaker
This passes the selectedSpeaker
into CardViewController
for display.
Make sure your local server is still running, and build and run Xcode. You should now see that the app is fully integrated with the user’s badge, and also shows the list of speakers.
You have successfully built an end-to-end application using a Python server and a Swift client. They also both share the same model generated with a proto file. If you ever need to modify the model, you simply run the compiler to generate it again, and you’re ready to go on both the client and server side!