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.

Leave a rating/review
Save for later
Share
You are currently viewing page 3 of 4 of this article. Click here to view the first page.

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:

protocol buffers

Next try the speaker call, http://127.0.0.1:5000/speakers:

protocol buffers

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:

  1. shared is a singleton you’ll use to access network calls.
  2. 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.
  3. The if let unwraps the response value.
  4. The data returned is a binary representation of the protocol buffer. The Contact initializer takes this data 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:

  1. fetchCurrentUser() calls the service to fetch the current user’s info, and configures CardViewController with the contact.
  2. configure(_:) takes a Contact 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?

protocol buffers

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!

protocol buffers

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.

protocol buffers

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!