How to Use Protocol Buffers in Swift

A practical guide to working with Protocol Buffers in Swift for iOS and macOS

Published: January 2025 • 8 min read

Swift and Protocol Buffers are a great match for iOS and macOS development. Apple's swift-protobuf library generates idiomatic Swift code with full support for Swift's type system, optionals, and value semantics.

This guide walks you through using Protocol Buffers in Swift applications, from setup to working examples.

What You'll Need

  • Xcode: Version 14 or higher
  • Swift: Version 5.5 or higher
  • Protocol Buffer Compiler: We'll install with Homebrew

Step 1: Install Tools

Install Protocol Buffer compiler and Swift plugin:

brew install protobuf swift-protobuf

Verify installation:

protoc --version
protoc-gen-swift --version

Step 2: Set Up Your Project

For an iOS project, add SwiftProtobuf via Swift Package Manager:

  1. In Xcode, go to File → Add Package Dependencies
  2. Enter: https://github.com/apple/swift-protobuf.git
  3. Select version 1.25.0 or higher

For a command-line project:

mkdir ProtobufSwiftExample
cd ProtobufSwiftExample
swift package init --type executable
mkdir Protos

Step 3: Configure Package.swift

Update Package.swift:

// swift-tools-version: 5.9
import PackageDescription

let package = Package(
    name: "ProtobufSwiftExample",
    dependencies: [
        .package(
            url: "https://github.com/apple/swift-protobuf.git",
            from: "1.25.0"
        )
    ],
    targets: [
        .executableTarget(
            name: "ProtobufSwiftExample",
            dependencies: [
                .product(name: "SwiftProtobuf", package: "swift-protobuf")
            ]
        )
    ]
)

Step 4: Create Your .proto File

Create Protos/person.proto:

syntax = "proto3";

message Person {
  string name = 1;
  int32 id = 2;
  string email = 3;
  repeated string phone_numbers = 4;
  
  enum PhoneType {
    MOBILE = 0;
    HOME = 1;
    WORK = 2;
  }
  
  message PhoneNumber {
    string number = 1;
    PhoneType type = 2;
  }
  
  repeated PhoneNumber phones = 5;
  bool is_active = 6;
}

message AddressBook {
  repeated Person people = 1;
}

Step 5: Generate Swift Code

protoc --swift_out=Sources/ProtobufSwiftExample Protos/person.proto

This generates person.pb.swift with all Swift types.

Step 6: Use Protocol Buffers in Swift

Update Sources/ProtobufSwiftExample/main.swift:

import Foundation
import SwiftProtobuf

// Create a Person
var person = Person()
person.name = "John Doe"
person.id = 1234
person.email = "john.doe@example.com"
person.phoneNumbers = ["555-1234", "555-5678"]

var phoneNumber = Person.PhoneNumber()
phoneNumber.number = "555-9999"
phoneNumber.type = .mobile
person.phones = [phoneNumber]

person.isActive = true

print("Created Person:")
print("Name: \(person.name)")
print("ID: \(person.id)")
print("Email: \(person.email)")

do {
    // Serialize to Data
    let data = try person.serializedData()
    print("\nSerialized to \(data.count) bytes")
    
    // Deserialize from Data
    let decoded = try Person(serializedData: data)
    print("\nDeserialized Person:")
    print("Name: \(decoded.name)")
    print("Active: \(decoded.isActive)")
    
    // Save to file
    let fileURL = URL(fileURLWithPath: "person.bin")
    try data.write(to: fileURL)
    print("\nSaved to person.bin")
    
    // Read from file
    let fileData = try Data(contentsOf: fileURL)
    let fromFile = try Person(serializedData: fileData)
    print("Read from file: \(fromFile.name)")
    
    // Create AddressBook
    var addressBook = AddressBook()
    addressBook.people = [person]
    
    var person2 = Person()
    person2.name = "Jane Smith"
    person2.id = 5678
    person2.email = "jane@example.com"
    person2.isActive = true
    
    addressBook.people.append(person2)
    
    print("\nAddress book has \(addressBook.people.count) people")
    
    // Convert to JSON
    let jsonData = try person.jsonUTF8Data()
    let jsonString = String(data: jsonData, encoding: .utf8) ?? ""
    print("\nJSON representation:")
    print(jsonString)
    
} catch {
    print("Error: \(error)")
}

Step 7: Build and Run

swift build
swift run

Expected output:

Created Person:
Name: John Doe
ID: 1234
Email: john.doe@example.com

Serialized to 78 bytes

Deserialized Person:
Name: John Doe
Active: true

Saved to person.bin
Read from file: John Doe

Address book has 2 people

Swift-Specific Features

Value Semantics

// Protobuf messages are Swift structs (value types)
var person2 = person  // Copy, not reference
person2.name = "Different Name"  // Doesn't affect person

Codable Support

// Automatic Codable conformance for JSON
let encoder = JSONEncoder()
let jsonData = try encoder.encode(person)

let decoder = JSONDecoder()
let decoded = try decoder.decode(Person.self, from: jsonData)

Optional Handling

// Check if field was set
if person.hasEmail {
    print("Email: \(person.email)")
}

// Clear a field
person.clearEmail()

Equatable & Hashable

// Automatic conformance
if person1 == person2 {
    print("Same person")
}

// Use in Sets and Dictionaries
var peopleSet = Set<Person>()
peopleSet.insert(person)

Using in iOS Apps

Example with SwiftUI:

import SwiftUI
import SwiftProtobuf

struct PersonView: View {
    @State private var person = Person()
    
    var body: some View {
        Form {
            TextField("Name", text: $person.name)
            TextField("Email", text: $person.email)
            
            Button("Save") {
                savePerson()
            }
        }
    }
    
    func savePerson() {
        do {
            let data = try person.serializedData()
            UserDefaults.standard.set(data, forKey: "person")
        } catch {
            print("Error: \(error)")
        }
    }
    
    func loadPerson() {
        guard let data = UserDefaults.standard.data(forKey: "person"),
              let loaded = try? Person(serializedData: data) else {
            return
        }
        person = loaded
    }
}

Best Practices for Swift

Use Swift Error Handling

Always use do-try-catch when serializing or deserializing protobuf messages.

Leverage Value Semantics

Protobuf messages are structs. Take advantage of Swift's copy-on-write for efficient updates.

Use Extensions

Add custom functionality with Swift extensions rather than modifying generated code.

Common Issues

Issue: protoc-gen-swift not found

Solution: Install via Homebrew: brew install swift-protobuf. Make sure it's in your PATH.

Issue: SwiftProtobuf module not found

Solution: Add SwiftProtobuf package dependency in Xcode or update Package.swift.

Issue: Generated file errors

Solution: Make sure your protoc and swift-protobuf versions are compatible. Update both to the latest versions.

Related Tools

Conclusion

Protocol Buffers integrate beautifully with Swift. The swift-protobuf library generates idiomatic Swift code that feels natural to use. Combined with Swift's type safety and modern language features, you get efficient serialization with excellent developer experience.

Whether you're building iOS apps, macOS applications, or server-side Swift, Protocol Buffers provide a solid foundation for data exchange and storage.