Swift
Swift support in bufrnix enables generation of Swift Protocol Buffer code for iOS, macOS, and server-side Swift applications using SwiftProtobuf.
Configuration
Section titled “Configuration”{ languages = { swift = { enable = true; }; };}
{ languages = { swift = { enable = true; outputPath = "proto/gen/swift"; packageName = "MyProtoLibrary"; options = [ "Visibility=Public" "FileNaming=FullPath" ]; }; };}
Options
Section titled “Options”swift.enable
Section titled “swift.enable”- Type:
bool
- Default:
false
- Description: Enable Swift code generation
swift.outputPath
Section titled “swift.outputPath”- Type:
string
- Default:
"proto/gen/swift"
- Description: Output directory for generated Swift files
swift.packageName
Section titled “swift.packageName”- Type:
string
- Default:
""
- Description: Name of the Swift package to create
swift.options
Section titled “swift.options”- Type:
list of strings
- Default:
[]
- Description: Additional options to pass to protoc-gen-swift
Examples
Section titled “Examples”iOS App Integration
Section titled “iOS App Integration”{ description = "iOS app with protobuf support";
inputs = {nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";bufrnix.url = "github:your-org/bufrnix";};
outputs = { self, nixpkgs, bufrnix }:letsystem = "aarch64-darwin";pkgs = import nixpkgs { inherit system; };
protoGen = bufrnix.lib.mkBufrnixPackage { inherit pkgs; inherit (pkgs) lib; config = { languages = { swift = { enable = true; outputPath = "Generated/Proto"; options = [ "Visibility=Public" "FileNaming=FullPath" ]; }; }; }; }; in { packages.${system} = { default = protoGen; proto = protoGen; };
devShells.${system}.default = pkgs.mkShell { packages = with pkgs; [ protoGen swift protoc-gen-swift ]; }; };
}
import PackageDescription
let package = Package( name: "MyApp", platforms: [ .iOS(.v13), .macOS(.v10_15) ], products: [ .library( name: "MyApp", targets: ["MyApp"]), ], dependencies: [ .package( url: "https://github.com/apple/swift-protobuf.git", from: "1.20.0" ), ], targets: [ .target( name: "MyApp", dependencies: [ .product(name: "SwiftProtobuf", package: "swift-protobuf"), ], path: "Sources" ), .testTarget( name: "MyAppTests", dependencies: ["MyApp"]), ])
import Foundationimport SwiftProtobuf
// Using generated protobuf messagesstruct UserService {func createUser(name: String, email: String) -> User_V1_User {var user = User_V1_User()user.id = UUID().uuidStringuser.name = nameuser.email = emailuser.createdAt = Google_Protobuf_Timestamp(date: Date())return user}
func serializeUser(_ user: User_V1_User) throws -> Data { return try user.serializedData() }
func deserializeUser(from data: Data) throws -> User_V1_User { return try User_V1_User(serializedData: data) }
func toJSON(_ user: User_V1_User) throws -> String { let jsonData = try user.jsonUTF8Data() return String(data: jsonData, encoding: .utf8) ?? "" }
}
// iOS SwiftUI integrationimport SwiftUI
struct UserDetailView: View {let user: User_V1_User
var body: some View { VStack(alignment: .leading, spacing: 8) { Text(user.name) .font(.title) Text(user.email) .font(.subheadline) .foregroundColor(.secondary) Text("Created: \(user.createdAt.date.formatted())") .font(.caption) } .padding() }
}
Server-Side Swift
Section titled “Server-Side Swift”{ languages = { swift = { enable = true; outputPath = "Sources/ProtoModels"; packageName = "APIModels"; options = [ "Visibility=Public" "FileNaming=PathToUnderscores" ]; }; };}
import Vaporimport SwiftProtobuf
// Vapor route handlers using protobuffunc routes(\_ app: Application) throws {app.post("users") { req -> EventLoopFuture<Response> inreturn req.body.collect(max: 1024 \* 1024).flatMapThrowing { buffer inlet data = Data(buffer: buffer)let createRequest = try User_V1_CreateUserRequest(serializedData: data)
// Process the request var user = User_V1_User() user.id = UUID().uuidString user.name = createRequest.name user.email = createRequest.email user.createdAt = Google_Protobuf_Timestamp(date: Date())
// Return protobuf response let responseData = try user.serializedData() return Response( status: .created, headers: ["Content-Type": "application/x-protobuf"], body: .init(data: responseData) ) } }
app.get("users", ":id") { req -> EventLoopFuture<User_V1_User> in guard let userID = req.parameters.get("id") else { throw Abort(.badRequest) }
// Fetch user and return as protobuf var user = User_V1_User() user.id = userID user.name = "John Doe" user.email = "john@example.com"
return req.eventLoop.makeSucceededFuture(user) }
}
// Custom content configuration for protobufextension Application.ContentConfiguration {func configureProtobuf() {// Add protobuf content type supportself.use(encoder: ProtobufEncoder(), for: .init(type: "application", subType: "x-protobuf"))self.use(decoder: ProtobufDecoder(), for: .init(type: "application", subType: "x-protobuf"))}}
struct ProtobufEncoder: ContentEncoder {func encode<E: Encodable>(\_ encodable: E, to body: inout ByteBuffer, headers: inout HTTPHeaders) throws {if let message = encodable as? SwiftProtobuf.Message {let data = try message.serializedData()body.writeBytes(data)}}}
Cross-Platform Data Models
Section titled “Cross-Platform Data Models”import Foundationimport SwiftProtobuf
// Shared data models across iOS, macOS, and serverextension User_V1_User { var displayName: String { return name.isEmpty ? "Unknown User" : name }
var isValidEmail: Bool { return email.contains("@") && email.contains(".") }
static func create(name: String, email: String) -> User_V1_User { var user = User_V1_User() user.id = UUID().uuidString user.name = name user.email = email user.createdAt = Google_Protobuf_Timestamp(date: Date()) return user }}
// API client using protobufclass APIClient { private let baseURL: URL
init(baseURL: URL) { self.baseURL = baseURL }
func createUser(name: String, email: String) async throws -> User_V1_User { var request = User_V1_CreateUserRequest() request.name = name request.email = email
let data = try request.serializedData()
var urlRequest = URLRequest(url: baseURL.appendingPathComponent("users")) urlRequest.httpMethod = "POST" urlRequest.setValue("application/x-protobuf", forHTTPHeaderField: "Content-Type") urlRequest.httpBody = data
let (responseData, _) = try await URLSession.shared.data(for: urlRequest) return try User_V1_User(serializedData: responseData) }
func getUser(id: String) async throws -> User_V1_User { let url = baseURL.appendingPathComponent("users/\(id)") let (data, _) = try await URLSession.shared.data(from: url) return try User_V1_User(serializedData: data) }}
import XCTestimport SwiftProtobuf@testable import MyApp
class ProtobufTests: XCTestCase {
func testUserSerialization() throws { // Create a user let user = User_V1_User.create(name: "Alice", email: "alice@example.com")
// Test binary serialization let binaryData = try user.serializedData() let deserializedUser = try User_V1_User(serializedData: binaryData)
XCTAssertEqual(user.name, deserializedUser.name) XCTAssertEqual(user.email, deserializedUser.email) XCTAssertEqual(user.id, deserializedUser.id) }
func testJSONSerialization() throws { let user = User_V1_User.create(name: "Bob", email: "bob@example.com")
// Test JSON serialization let jsonData = try user.jsonUTF8Data() let jsonUser = try User_V1_User(jsonUTF8Data: jsonData)
XCTAssertEqual(user.name, jsonUser.name) XCTAssertEqual(user.email, jsonUser.email) }
func testValidation() { let validUser = User_V1_User.create(name: "Charlie", email: "charlie@example.com") XCTAssertTrue(validUser.isValidEmail)
var invalidUser = User_V1_User() invalidUser.email = "invalid-email" XCTAssertFalse(invalidUser.isValidEmail) }
}
Swift Options
Section titled “Swift Options”The Swift plugin supports several options that can be passed via the options
configuration:
Visibility Options
Section titled “Visibility Options”Visibility=Public
- Generate public classes (default: internal)Visibility=Package
- Generate package-level visibility
File Naming
Section titled “File Naming”FileNaming=FullPath
- Use full proto path in generated file namesFileNaming=PathToUnderscores
- Convert path separators to underscoresFileNaming=DropPath
- Drop path from file names
Generation Options
Section titled “Generation Options”ProtoPathModuleMappings=path/to/mapping.txt
- Custom module mappingsSwiftProtobufModuleName=CustomProtobuf
- Custom SwiftProtobuf module name
Best Practices
Section titled “Best Practices”Project Structure
Section titled “Project Structure”MySwiftProject/├── Package.swift├── Sources/│ ├── MyApp/│ │ ├── main.swift│ │ └── Services/│ └── ProtoModels/ # Generated protobuf code│ ├── user/│ │ └── v1/│ │ └── user.pb.swift│ └── common/├── Tests/├── proto/ # Source proto files│ ├── user/│ │ └── v1/│ │ └── user.proto│ └── buf.yaml└── flake.nix
Performance Considerations
Section titled “Performance Considerations”- Use
Any
types sparingly for better performance - Prefer binary serialization over JSON when possible
- Consider using
UnknownStorage
preservation for forward compatibility - Use
MessageSet
for extensible message types
Platform-Specific Notes
Section titled “Platform-Specific Notes”iOS/macOS
Section titled “iOS/macOS”- SwiftProtobuf integrates well with Core Data
- Works with Combine for reactive programming
- Supports Codable for JSON APIs
Server-Side Swift
Section titled “Server-Side Swift”- Excellent performance with Vapor and Hummingbird
- Works well with async/await
- Good integration with database ORMs
Memory Management
Section titled “Memory Management”- Protobuf messages are value types (structs)
- Automatic memory management
- Efficient copying with copy-on-write semantics
- No retain cycles to worry about
Integration Examples
Section titled “Integration Examples”Core Data Integration
Section titled “Core Data Integration”extension User_V1_User { func toCoreDataUser(context: NSManagedObjectContext) -> CoreDataUser { let user = CoreDataUser(context: context) user.id = self.id user.name = self.name user.email = self.email user.createdAt = self.createdAt.date return user }
static func fromCoreDataUser(_ user: CoreDataUser) -> User_V1_User { var protoUser = User_V1_User() protoUser.id = user.id ?? "" protoUser.name = user.name ?? "" protoUser.email = user.email ?? "" if let createdAt = user.createdAt { protoUser.createdAt = Google_Protobuf_Timestamp(date: createdAt) } return protoUser }}
Combine Integration
Section titled “Combine Integration”import Combine
class UserRepository: ObservableObject { @Published var users: [User_V1_User] = [] private let apiClient: APIClient
init(apiClient: APIClient) { self.apiClient = apiClient }
func fetchUsers() -> AnyPublisher<[User_V1_User], Error> { return apiClient.getUsers() .receive(on: DispatchQueue.main) .handleEvents(receiveOutput: { [weak self] users in self?.users = users }) .eraseToAnyPublisher() }}
Complete Flake Configuration Example
Section titled “Complete Flake Configuration Example”{ inputs = { nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable"; flake-utils.url = "github:numtide/flake-utils"; bufrnix = { url = "github:conneroisu/bufrnix"; inputs.nixpkgs.follows = "nixpkgs"; }; };
outputs = { nixpkgs, bufrnix, flake-utils, ... }: flake-utils.lib.eachDefaultSystem (system: let pkgs = import nixpkgs {inherit system;};
# Create our bufrnix package with Swift language support bufrnixPkg = bufrnix.lib.mkBufrnixPackage { inherit pkgs; config = { languages = { swift = { enable = true; outputPath = "proto/gen/swift"; }; }; }; }; in { packages.default = bufrnixPkg;
devShells.default = pkgs.mkShell { packages = with pkgs; [ bufrnixPkg swift protoc-gen-swift ];
shellHook = '' echo "Swift protobuf example development environment" echo "Available commands:" echo " bufrnix_init - Initialize proto structure" echo " bufrnix - Generate Swift code from proto files" echo " bufrnix_lint - Lint proto files" ''; }; });}