shenghaiwang/googlesheetsswift
A modern, Swift-native SDK for the Google Sheets API v4. Built with async/await, comprehensive error handling, and developer experience in mind.
Features
- ✅ Modern Swift: Built with async/await, Codable, and Swift concurrency
- ✅ Comprehensive API Coverage: Full support for Google Sheets API v4
- ✅ Type Safety: Strongly-typed models and responses
- ✅ OAuth2 & API Key Authentication: Flexible authentication options
- ✅ Performance Optimized: Built-in caching, batch operations, and memory efficiency
- ✅ Error Handling: Comprehensive error types with recovery suggestions
- ✅ Logging & Debugging: Configurable logging for development and production
- ✅ Zero Dependencies: Lightweight with no external dependencies
- ✅ Extensive Testing: Unit tests, integration tests, and performance tests
Requirements
- iOS 13.0+ / macOS 10.15+ / tvOS 13.0+ / watchOS 6.0+
- Swift 5.7+
- Xcode 14.0+
Installation
Swift Package Manager
Add GoogleSheetsSwift to your project using Xcode:
- File → Add Package Dependencies
- Enter the repository URL:
https://github.com/ShenghaiWang/GoogleSheetsSwift.git - Select the version you want to use
Or add it to your Package.swift:
dependencies: [
.package(url: "https://github.com/ShenghaiWang/GoogleSheetsSwift.git", from: "1.0.0")
]Quick Start
### 1. Authentication Setup
#### OAuth2 Authentication (Recommended)
```swift
import GoogleSheetsSwift
// Create OAuth2 token manager
let tokenManager = GoogleOAuth2TokenManager(
clientId: "your-client-id",
clientSecret: "your-client-secret",
redirectURI: "your-redirect-uri"
)
// Authenticate with required scopes
let scopes = ["https://www.googleapis.com/auth/spreadsheets"]
let authResult = try await tokenManager.authenticate(scopes: scopes)
// Create client
let client = GoogleSheetsClient(tokenManager: tokenManager)
```
#### API Key Authentication (Read-only)
```swift
import GoogleSheetsSwift
// Create client with API key for read-only operations
let client = GoogleSheetsClient(apiKey: "your-api-key")
```
#### Service Account Authentication (Server-to-Server)
Service account authentication is ideal for server-side applications, automation scripts, and scenarios where you need to access Google Sheets without user interaction.
##### Setup
1. **Create a Service Account** in the [Google Cloud Console](https://console.cloud.google.com/)
2. **Download the JSON key file** for your service account
3. **Share your spreadsheet** with the service account email address (found in the JSON file)
4. **Enable the Google Sheets API** for your project
##### Authentication Methods
**Method 1: Load from JSON file**
```swift
import GoogleSheetsSwift
// Load service account from JSON file
let tokenManager = try ServiceAccountTokenManager.loadFromFile("/path/to/service-account.json")
let client = GoogleSheetsClient(tokenManager: tokenManager)
// Now you can perform operations
let values = try await client.readRange("spreadsheet-id", range: "Sheet1!A1:C10")
```
**Method 2: Load from environment variable**
```swift
import GoogleSheetsSwift
// Set environment variable: GOOGLE_APPLICATION_CREDENTIALS=/path/to/service-account.json
let tokenManager = try ServiceAccountTokenManager.loadFromEnvironment()
let client = GoogleSheetsClient(tokenManager: tokenManager)
```
**Method 3: Initialize directly with ServiceAccountKey**
```swift
import GoogleSheetsSwift
// Load and parse the service account JSON
let serviceAccountData = try Data(contentsOf: URL(fileURLWithPath: "/path/to/service-account.json"))
let serviceAccountKey = try JSONDecoder().decode(ServiceAccountKey.self, from: serviceAccountData)
// Create token manager
let tokenManager = ServiceAccountTokenManager(serviceAccountKey: serviceAccountKey)
let client = GoogleSheetsClient(tokenManager: tokenManager)
```
##### Domain-Wide Delegation (G Suite/Google Workspace)
If you need to impersonate users in your organization:
```swift
// Set up service account with domain-wide delegation
let tokenManager = try ServiceAccountTokenManager.loadFromFile("/path/to/service-account.json")
// Impersonate a specific user
tokenManager.setImpersonationUser("user@yourdomain.com")
let client = GoogleSheetsClient(tokenManager: tokenManager)
// Operations will be performed as the impersonated user
let values = try await client.readRange("spreadsheet-id", range: "Sheet1!A1:C10")
// Clear impersonation to return to service account identity
tokenManager.clearImpersonationUser()
```
##### Service Account JSON Structure
Your service account JSON file should contain:
```json
{
"type": "service_account",
"project_id": "your-project-id",
"private_key_id": "key-id",
"private_key": "-----BEGIN PRIVATE KEY-----\n...\n-----END PRIVATE KEY-----\n",
"client_email": "your-service-account@your-project.iam.gserviceaccount.com",
"client_id": "123456789012345678901",
"auth_uri": "https://accounts.google.com/o/oauth2/auth",
"token_uri": "https://oauth2.googleapis.com/token",
"auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
"client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/your-service-account%40your-project.iam.gserviceaccount.com"
}
```
##### Error Handling
```swift
do {
let tokenManager = try ServiceAccountTokenManager.loadFromFile("/path/to/service-account.json")
let client = GoogleSheetsClient(tokenManager: tokenManager)
let values = try await client.readRange("spreadsheet-id", range: "A1:B2")
// Process values
} catch GoogleSheetsError.authenticationFailed(let message) {
print("Service account authentication failed: \(message)")
// Check your service account JSON file and permissions
} catch CocoaError.fileReadNoSuchFile {
print("Service account JSON file not found")
// Verify the file path
} catch DecodingError.keyNotFound(let key, _) {
print("Invalid service account JSON: missing key '\(key.stringValue)'")
// Check your JSON file format
} catch {
print("Unexpected error: \(error)")
}
```
##### Best Practices
1. **Secure Storage**: Never commit service account JSON files to version control
2. **Environment Variables**: Use `GOOGLE_APPLICATION_CREDENTIALS` in production
3. **Minimal Permissions**: Only grant necessary scopes to your service account
4. **Share Spreadsheets**: Remember to share spreadsheets with the service account email
5. **Key Rotation**: Regularly rotate service account keys for security
##### Example: Automated Data Processing
```swift
import GoogleSheetsSwift
class SpreadsheetProcessor {
private let client: GoogleSheetsClient
init() throws {
// Load service account from environment
let tokenManager = try ServiceAccountTokenManager.loadFromEnvironment()
self.client = GoogleSheetsClient(tokenManager: tokenManager)
}
func processMonthlyData() async throws {
let spreadsheetId = "your-spreadsheet-id"
// Read raw data
let rawData = try await client.readStringValues(
spreadsheetId,
range: "RawData!A2:E1000"
)
// Process data (example: calculate totals)
var processedData: [[String]] = [["Category", "Total", "Average"]]
// Group by category and calculate totals
let groupedData = Dictionary(grouping: rawData) { row in
row[0] ?? "Unknown" // Group by first column
}
for (category, rows) in groupedData {
let values = rows.compactMap { row in
Double(row[1] ?? "0") // Sum second column
}
let total = values.reduce(0, +)
let average = values.isEmpty ? 0 : total / Double(values.count)
processedData.append([
category,
String(format: "%.2f", total),
String(format: "%.2f", average)
])
}
// Write processed data to summary sheet
try await client.writeRange(
spreadsheetId,
range: "Summary!A1:C\(processedData.count)",
values: processedData
)
print("Processed \(rawData.count) rows into \(processedData.count - 1) categories")
}
}
// Usage
do {
let processor = try SpreadsheetProcessor()
try await processor.processMonthlyData()
} catch {
print("Processing failed: \(error)")
}
```
### 2. Basic Operations
#### Reading Data
```swift
// Read values from a range
let values = try await client.readRange(
"your-spreadsheet-id",
range: "Sheet1!A1:C10"
)
// Get string values directly
let stringValues = try await client.readStringValues(
"your-spreadsheet-id",
range: "Sheet1!A1:C10"
)
print("First cell: \(stringValues[0][0] ?? "Empty")")
```
#### Writing Data
```swift
// Write data to a range
let data = [
["Name", "Age", "City"],
["John", "25", "New York"],
["Jane", "30", "San Francisco"]
]
let response = try await client.writeRange(
"your-spreadsheet-id",
range: "Sheet1!A1:C3",
values: data
)
print("Updated \(response.updatedCells ?? 0) cells")
```
#### Creating Spreadsheets
```swift
// Create a new spreadsheet
let spreadsheet = try await client.createSpreadsheet(
title: "My New Spreadsheet",
sheetTitles: ["Data", "Analysis"]
)
print("Created spreadsheet: \(spreadsheet.spreadsheetId ?? "unknown")")
```Advanced Usage
Batch Operations
// Batch read multiple ranges
let operations = [
BatchReadOperation(range: "Sheet1!A1:C10"),
BatchReadOperation(range: "Sheet2!A1:D5")
]
let results = try await client.batchRead(
"your-spreadsheet-id",
operations: operations
)
// Batch write to multiple ranges
let writeOperations = [
BatchWriteOperation(range: "Sheet1!A1:B2", values: [["A1", "B1"], ["A2", "B2"]]),
BatchWriteOperation(range: "Sheet1!D1:E2", values: [["D1", "E1"], ["D2", "E2"]])
]
let writeResponse = try await client.batchWrite(
"your-spreadsheet-id",
operations: writeOperations
)Custom Configuration
// Create client with custom configuration
let logger = ConsoleGoogleSheetsLogger(minimumLevel: .debug)
let cache = InMemoryResponseCache()
let cacheConfig = CacheConfiguration(ttl: 300, maxSize: 100)
let client = GoogleSheetsClient(
tokenManager: tokenManager,
logger: logger,
cache: cache,
cacheConfiguration: cacheConfig
)
// Enable debug mode
client.setDebugMode(true)Error Handling
do {
let values = try await client.readRange("spreadsheet-id", range: "A1:B2")
// Process values
} catch GoogleSheetsError.authenticationFailed(let message) {
print("Authentication failed: \(message)")
} catch GoogleSheetsError.rateLimitExceeded(let retryAfter) {
print("Rate limited. Retry after: \(retryAfter ?? 0) seconds")
} catch GoogleSheetsError.invalidSpreadsheetId(let id) {
print("Invalid spreadsheet ID: \(id)")
} catch {
print("Unexpected error: \(error)")
}A1 Notation Utilities
// Validate A1 notation
let isValid = GoogleSheetsClient.isValidA1Range("Sheet1!A1:B10")
// Convert column numbers to letters
let columnLetter = GoogleSheetsClient.columnNumberToLetters(27) // "AA"
// Convert column letters to numbers
let columnNumber = try GoogleSheetsClient.columnLettersToNumber("AA") // 27
// Build A1 ranges programmatically
let range = GoogleSheetsClient.buildA1Range(
sheetName: "Data",
startColumn: 1, startRow: 1,
endColumn: 5, endRow: 100
) // "Data!A1:E100"API Reference
Core Classes
GoogleSheetsClient
The main entry point for all Google Sheets operations.
Initialization:
// OAuth2 authentication
init(tokenManager: OAuth2TokenManager, httpClient: HTTPClient? = nil, logger: GoogleSheetsLogger? = nil)
// API key authentication
init(apiKey: String, httpClient: HTTPClient? = nil, logger: GoogleSheetsLogger? = nil)Properties:
spreadsheets: SpreadsheetsServiceProtocol- Spreadsheet management operationsvalues: ValuesServiceProtocol- Values read/write operations
Key Methods:
readRange(_:range:majorDimension:valueRenderOption:)- Read values from a rangewriteRange(_:range:values:majorDimension:valueInputOption:)- Write values to a rangecreateSpreadsheet(title:sheetTitles:)- Create a new spreadsheetbatchRead(_:operations:)- Batch read operationsbatchWrite(_:operations:valueInputOption:)- Batch write operations
Data Models
ValueRange
Represents a range of values in a spreadsheet.
public struct ValueRange: Codable {
public let range: String?
public let majorDimension: MajorDimension?
public let values: [[AnyCodable]]?
// Convenience methods
public func getStringValues() -> [[String?]]
public func getDoubleValues() -> [[Double?]]
}Spreadsheet
Represents a Google Sheets spreadsheet.
public struct Spreadsheet: Codable {
public let spreadsheetId: String?
public let properties: SpreadsheetProperties?
public let sheets: [Sheet]?
public let namedRanges: [NamedRange]?
public let spreadsheetUrl: String?
}Enums
MajorDimension
Specifies how data should be interpreted.
public enum MajorDimension: String, Codable {
case rows = "ROWS"
case columns = "COLUMNS"
}ValueRenderOption
Specifies how values should be rendered.
public enum ValueRenderOption: String, Codable {
case formattedValue = "FORMATTED_VALUE"
case unformattedValue = "UNFORMATTED_VALUE"
case formula = "FORMULA"
}ValueInputOption
Specifies how input data should be interpreted.
public enum ValueInputOption: String, Codable {
case raw = "RAW"
case userEntered = "USER_ENTERED"
}Error Handling
GoogleSheetsError
Comprehensive error types for different failure scenarios.
public enum GoogleSheetsError: Error, LocalizedError {
case authenticationFailed(String)
case invalidSpreadsheetId(String)
case invalidRange(String)
case networkError(Error)
case apiError(code: Int, message: String)
case rateLimitExceeded(retryAfter: TimeInterval?)
case quotaExceeded
case invalidResponse
case tokenExpired
}Performance Features
Caching
The SDK includes built-in response caching for read operations:
// Configure caching
let cacheConfig = CacheConfiguration(
ttl: 300, // 5 minutes
maxSize: 100, // Maximum 100 cached responses
enabled: true
)
let cache = InMemoryResponseCache()
let client = GoogleSheetsClient(
tokenManager: tokenManager,
cache: cache,
cacheConfiguration: cacheConfig
)Batch Optimization
The SDK automatically optimizes batch operations:
// The SDK will optimize these operations for better performance
let batchOptimizer = BatchOptimizer()
let client = GoogleSheetsClient(
tokenManager: tokenManager,
batchOptimizer: batchOptimizer
)Memory Efficiency
For large datasets, the SDK provides memory-efficient handling:
let memoryHandler = MemoryEfficientDataHandler()
let client = GoogleSheetsClient(
tokenManager: tokenManager,
memoryHandler: memoryHandler
)Logging and Debugging
Console Logging
let logger = ConsoleGoogleSheetsLogger(
minimumLevel: .debug,
includeTimestamp: true,
includeMetadata: true
)
let client = GoogleSheetsClient(tokenManager: tokenManager, logger: logger)File Logging
let documentsPath = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]
let logFileURL = documentsPath.appendingPathComponent("sheets-sdk.log")
if let fileLogger = FileGoogleSheetsLogger(fileURL: logFileURL) {
let client = GoogleSheetsClient(tokenManager: tokenManager, logger: fileLogger)
}Custom Logging
class CustomLogger: GoogleSheetsLogger {
func log(level: LogLevel, message: String, metadata: [String: Any]?) {
// Your custom logging implementation
}
func isEnabled(for level: LogLevel) -> Bool {
return level >= .info
}
}
let client = GoogleSheetsClient(tokenManager: tokenManager, logger: CustomLogger())Testing
The SDK includes comprehensive testing utilities:
Mock Objects
import GoogleSheetsSwift
// Use mock objects for testing
let mockTokenManager = MockOAuth2TokenManager()
let mockHTTPClient = MockHTTPClient()
let client = GoogleSheetsClient(
tokenManager: mockTokenManager,
httpClient: mockHTTPClient
)
// Configure mock responses
mockHTTPClient.responses["spreadsheets/test-id/values/A1:B2"] = ValueRange(
range: "A1:B2",
values: [["A1", "B1"], ["A2", "B2"]]
)Integration Tests
The SDK includes integration tests that can run against the real Google Sheets API:
// Set environment variables for integration tests
// GOOGLE_SHEETS_CLIENT_ID=your-client-id
// GOOGLE_SHEETS_CLIENT_SECRET=your-client-secret
// GOOGLE_SHEETS_TEST_SPREADSHEET_ID=your-test-spreadsheet-id
swift test --filter IntegrationTestsExamples
Example 1: Simple Data Import
import GoogleSheetsSwift
func importDataToSheet() async throws {
let client = GoogleSheetsClient(apiKey: "your-api-key")
let csvData = [
["Product", "Price", "Stock"],
["iPhone", "999", "50"],
["iPad", "599", "30"],
["MacBook", "1299", "20"]
]
let response = try await client.writeRange(
"your-spreadsheet-id",
range: "Products!A1:C4",
values: csvData
)
print("Imported \(response.updatedRows ?? 0) rows")
}Example 2: Data Analysis
import GoogleSheetsSwift
func analyzeSheetData() async throws {
let client = GoogleSheetsClient(tokenManager: tokenManager)
// Read sales data
let salesData = try await client.readStringValues(
"your-spreadsheet-id",
range: "Sales!B2:B100" // Assuming column B contains sales amounts
)
// Calculate total sales
let totalSales = salesData.compactMap { row in
row.first?.flatMap(Double.init)
}.reduce(0, +)
// Write result back to sheet
try await client.writeRange(
"your-spreadsheet-id",
range: "Summary!A1:B1",
values: [["Total Sales", String(totalSales)]]
)
print("Total sales: $\(totalSales)")
}Example 3: Automated Report Generation
import GoogleSheetsSwift
func generateMonthlyReport() async throws {
let client = GoogleSheetsClient(tokenManager: tokenManager)
// Create new spreadsheet for the report
let spreadsheet = try await client.createSpreadsheet(
title: "Monthly Report - \(DateFormatter().string(from: Date()))",
sheetTitles: ["Summary", "Details", "Charts"]
)
guard let spreadsheetId = spreadsheet.spreadsheetId else {
throw GoogleSheetsError.invalidResponse
}
// Add headers
let headers = [["Metric", "Value", "Change"]]
try await client.writeRange(
spreadsheetId,
range: "Summary!A1:C1",
values: headers
)
// Add sample data
let reportData = [
["Revenue", "50000", "+5%"],
["Customers", "1250", "+12%"],
["Orders", "890", "+8%"]
]
try await client.writeRange(
spreadsheetId,
range: "Summary!A2:C4",
values: reportData
)
print("Report created: \(spreadsheet.spreadsheetUrl ?? "Unknown URL")")
}Best Practices
1. Authentication
- Use OAuth2 for applications that modify data
- Use API keys only for read-only operations
- Store credentials securely (use Keychain on iOS/macOS)
- Handle token refresh automatically
2. Error Handling
- Always handle
GoogleSheetsErrorcases specifically - Implement retry logic for rate limiting
- Provide user-friendly error messages
- Log errors for debugging
3. Performance
- Use batch operations for multiple ranges
- Enable caching for frequently accessed data
- Use appropriate value render options
- Consider memory usage for large datasets
4. Rate Limiting
- Respect Google's API quotas
- Implement exponential backoff
- Use batch operations to reduce API calls
- Monitor your usage
Troubleshooting
Common Issues
Authentication Errors
// Check if tokens are valid
if !tokenManager.isAuthenticated {
try await tokenManager.authenticate(scopes: scopes)
}Rate Limiting
// Handle rate limiting gracefully
do {
let result = try await client.readRange(spreadsheetId, range: range)
} catch GoogleSheetsError.rateLimitExceeded(let retryAfter) {
// Wait and retry
try await Task.sleep(nanoseconds: UInt64((retryAfter ?? 1) * 1_000_000_000))
// Retry the operation
}Invalid Ranges
// Validate ranges before using
if GoogleSheetsClient.isValidA1Range(range) {
let result = try await client.readRange(spreadsheetId, range: range)
} else {
print("Invalid range format: \(range)")
}Debug Mode
Enable debug mode to see detailed API interactions:
client.setDebugMode(true)This will log all HTTP requests and responses, helping you debug issues.
Contributing
We welcome contributions! Please see our Contributing Guide for details.
Development Setup
- Clone the repository
- Open in Xcode or use Swift Package Manager
- Run tests:
swift test - Run integration tests:
swift test --filter IntegrationTests
Running Tests
# Run all tests
swift test
# Run specific test suite
swift test --filter GoogleSheetsClientTests
# Run integration tests (requires credentials)
swift test --filter IntegrationTestsLicense
This project is licensed under the MIT License - see the LICENSE file for details.
Support
Acknowledgments
- Google Sheets API v4 documentation
- Swift community for best practices
- Contributors and testers
Made with ❤️ by the GoogleSheetsSwift team
Package Metadata
Repository: shenghaiwang/googlesheetsswift
Default branch: master
README: README.md