iOS SDK Setup Guide (Swift)
---
sidebar_label: iOS SDK (Swift) sidebar_category: Gaming SDK Guides
iOS SDK Setup Guide (Swift)
This guide covers integrating Gaming Intelligence into an iOS app using Swift. There is no dedicated CocoaPods or Swift Package Manager package for iOS — integration is done by calling the Gaming Intelligence REST API directly using Swift's built-in URLSession. This approach gives you full control over request timing and network configuration.
Prerequisites
Before you begin, have the following ready:
| Requirement | Where to Find It |
|---|---|
| SDK Key | Linkzly Console → Gaming → [Your Game] → Settings → SDK Configuration → SDK Key |
| Organization ID | Linkzly Console → Organization Settings |
| Game ID | Linkzly Console → Gaming → [Your Game] → Settings → General Settings |
| iOS Deployment Target | iOS 13.0 or later |
| Xcode | Xcode 14 or later |
Your SDK key is masked by default in the console. Use the eye icon to reveal it or the copy button to copy it directly.
Setup
Create a new Swift file in your project named LinkzlyGamingClient.swift. This helper class wraps the REST API call and handles the required authentication headers.
import Foundation
class LinkzlyGamingClient {
private let sdkKey: String
private let organizationId: String
private let gameId: String
private let baseURL: String
private let session: URLSession
init(
sdkKey: String,
organizationId: String,
gameId: String,
baseURL: String = "https://gaming.linkzly.com"
) {
self.sdkKey = sdkKey
self.organizationId = organizationId
self.gameId = gameId
self.baseURL = baseURL
let config = URLSessionConfiguration.default
config.timeoutIntervalForRequest = 30
config.timeoutIntervalForResource = 60
self.session = URLSession(configuration: config)
}
/// Send an array of events to the Linkzly ingestion endpoint.
func sendEvents(_ events: [[String: Any]], completion: ((Result<BatchResponse, Error>) -> Void)? = nil) {
guard let url = URL(string: "\(baseURL)/api/v1/gaming/events") else { return }
let payload: [String: Any] = [
"batch_id": UUID().uuidString,
"batch_timestamp": ISO8601DateFormatter().string(from: Date()),
"events": events
]
guard let body = try? JSONSerialization.data(withJSONObject: payload) else { return }
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.httpBody = body
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
request.setValue("Bearer \(sdkKey)", forHTTPHeaderField: "Authorization")
request.setValue(organizationId, forHTTPHeaderField: "X-Organization-ID")
request.setValue(gameId, forHTTPHeaderField: "X-Game-ID")
session.dataTask(with: request) { data, response, error in
if let error = error {
completion?(.failure(error))
return
}
guard let data = data,
let result = try? JSONDecoder().decode(BatchResponse.self, from: data) else {
completion?(.failure(URLError(.cannotParseResponse)))
return
}
completion?(.success(result))
}.resume()
}
}
Event Payload Struct
Define a Codable struct for type-safe event construction:
struct GameEvent: Encodable {
let event_id: String
let event_type: String
let timestamp: String
let platform: String
let player_id: String
let session_id: String
let data: [String: AnyCodable]?
init(
eventType: String,
playerId: String,
sessionId: String,
data: [String: Any]? = nil
) {
self.event_id = UUID().uuidString
self.event_type = eventType
self.timestamp = ISO8601DateFormatter().string(from: Date())
self.platform = "ios"
self.player_id = playerId
self.session_id = sessionId
self.data = data?.mapValues { AnyCodable($0) }
}
}
struct BatchResponse: Decodable {
let batch_id: String
let events_received: Int
let events_valid: Int
let events_dropped: Int
let trace_id: String
let server_timestamp: String
}
Note on
AnyCodable: Thedatafield is a free-form dictionary. To encode it cleanly, use a third-partyAnyCodabletype-eraser (available as a lightweight package) or serializedataseparately withJSONSerializationand merge it into the payload dictionary before sending.
Sending Events
Instantiate the client once (for example, as a property on your AppDelegate or a singleton) and call sendEvents(_:) with an array of event dictionaries:
// Instantiate once (e.g., in AppDelegate or a GameSession manager)
let gaming = LinkzlyGamingClient(
sdkKey: "YOUR_SDK_KEY",
organizationId: "YOUR_ORG_ID",
gameId: "YOUR_GAME_ID"
)
// Track session start
let sessionId = UUID().uuidString
gaming.sendEvents([[
"event_id": UUID().uuidString,
"event_type": "session_start",
"timestamp": ISO8601DateFormatter().string(from: Date()),
"platform": "ios",
"player_id": "player-001",
"session_id": sessionId,
"data": [:]
]]) { result in
switch result {
case .success(let response):
print("Batch accepted: \(response.batch_id), valid: \(response.events_valid)")
case .failure(let error):
print("Send failed: \(error.localizedDescription)")
}
}
// Track a custom event
gaming.sendEvents([[
"event_id": UUID().uuidString,
"event_type": "level_complete",
"timestamp": ISO8601DateFormatter().string(from: Date()),
"platform": "ios",
"player_id": "player-001",
"session_id": sessionId,
"data": [
"level_id": "level-3",
"score": 8500,
"duration_seconds": 95,
"stars": 2
]
]])
Batching Multiple Events
Send multiple events in a single request to reduce network overhead. The ingestion endpoint accepts up to 1,000 events per batch:
gaming.sendEvents([
[
"event_id": UUID().uuidString,
"event_type": "currency_earned",
"timestamp": ISO8601DateFormatter().string(from: Date()),
"platform": "ios",
"player_id": "player-001",
"session_id": sessionId,
"data": ["amount": 100, "currency_type": "gold", "source": "level_reward"]
],
[
"event_id": UUID().uuidString,
"event_type": "item_purchased",
"timestamp": ISO8601DateFormatter().string(from: Date()),
"platform": "ios",
"player_id": "player-001",
"session_id": sessionId,
"data": ["item_id": "shield-basic", "cost": 50, "currency_type": "gold"]
]
])
Session Management
There is no automatic session lifecycle management in the direct URLSession approach. Handle session start and end by hooking into the UIApplicationDelegate lifecycle:
// AppDelegate.swift
class AppDelegate: UIResponder, UIApplicationDelegate {
var gaming: LinkzlyGamingClient!
var currentSessionId: String = UUID().uuidString
let playerId = "player-001" // Replace with your actual player ID after login
func application(_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
gaming = LinkzlyGamingClient(
sdkKey: "YOUR_SDK_KEY",
organizationId: "YOUR_ORG_ID",
gameId: "YOUR_GAME_ID"
)
sendSessionStart()
return true
}
func applicationDidEnterBackground(_ application: UIApplication) {
sendSessionEnd()
}
func applicationWillEnterForeground(_ application: UIApplication) {
currentSessionId = UUID().uuidString // New session on foreground
sendSessionStart()
}
private func sendSessionStart() {
gaming.sendEvents([[
"event_id": UUID().uuidString,
"event_type": "session_start",
"timestamp": ISO8601DateFormatter().string(from: Date()),
"platform": "ios",
"player_id": playerId,
"session_id": currentSessionId,
"data": [:]
]])
}
private func sendSessionEnd() {
gaming.sendEvents([[
"event_id": UUID().uuidString,
"event_type": "session_end",
"timestamp": ISO8601DateFormatter().string(from: Date()),
"platform": "ios",
"player_id": playerId,
"session_id": currentSessionId,
"data": [:]
]])
}
}
SwiftUI apps: If your app uses the
@mainlifecycle withoutAppDelegate, usescenePhasein your rootAppstruct instead:@Environment(\.scenePhase) private var scenePhase .onChange(of: scenePhase) { phase in if phase == .active { sendSessionStart() } if phase == .background { sendSessionEnd() } }
HMAC Request Signing
If HMAC Signing is enabled for your game (the default for new games), every request must include a valid signature. Compute it in Swift using CryptoKit:
import CryptoKit
extension LinkzlyGamingClient {
func sendEventsWithHMAC(
_ events: [[String: Any]],
signingSecret: String,
completion: ((Result<BatchResponse, Error>) -> Void)? = nil
) {
guard let url = URL(string: "\(baseURL)/api/v1/gaming/events") else { return }
let payload: [String: Any] = [
"batch_id": UUID().uuidString,
"batch_timestamp": ISO8601DateFormatter().string(from: Date()),
"events": events
]
guard let body = try? JSONSerialization.data(withJSONObject: payload) else { return }
// Compute HMAC signature
let timestamp = String(Int64(Date().timeIntervalSince1970 * 1000))
let nonce = UUID().uuidString
guard let bodyString = String(data: body, encoding: .utf8) else { return }
let signingString = "\(timestamp).\(nonce).\(bodyString)"
let key = SymmetricKey(data: Data(signingSecret.utf8))
let signature = HMAC<SHA256>.authenticationCode(
for: Data(signingString.utf8),
using: key
)
let hexSignature = signature.map { String(format: "%02x", $0) }.joined()
// Build the request
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.httpBody = body
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
request.setValue("Bearer \(sdkKey)", forHTTPHeaderField: "Authorization")
request.setValue(organizationId, forHTTPHeaderField: "X-Organization-ID")
request.setValue(gameId, forHTTPHeaderField: "X-Game-ID")
request.setValue(hexSignature, forHTTPHeaderField: "X-Signature-256")
request.setValue(timestamp, forHTTPHeaderField: "X-Timestamp")
request.setValue(nonce, forHTTPHeaderField: "X-Nonce")
session.dataTask(with: request) { data, response, error in
if let error = error { completion?(.failure(error)); return }
guard let data = data,
let result = try? JSONDecoder().decode(BatchResponse.self, from: data) else {
completion?(.failure(URLError(.cannotParseResponse))); return
}
completion?(.success(result))
}.resume()
}
}
How the signature is computed:
- Build the signing string:
"{timestamp}.{nonce}.{rawBodyString}" - Compute
HMAC-SHA256of that string using your signing secret as the key. - Hex-encode the resulting bytes.
- Send the result in the
X-Signature-256header.
The replay window is fixed at 300 seconds. Requests with a timestamp more than 5 minutes out of sync with the server are rejected with 401 Unauthorized. Ensure the device clock is synchronized.
Security note: Do not hardcode your signing secret in source files committed to version control. Store it in your app's keychain or inject it at build time using Xcode build settings.
Error Handling
Check the HTTP status code in the URLSession completion handler before processing the response:
session.dataTask(with: request) { data, response, error in
if let error = error {
// Network error (no connectivity, timeout, etc.)
print("Network error: \(error.localizedDescription)")
return
}
guard let httpResponse = response as? HTTPURLResponse else { return }
switch httpResponse.statusCode {
case 202:
// Success — batch accepted
break
case 401:
// Invalid SDK key, wrong org/game ID, or HMAC signature failure
print("Authentication error — check your SDK key and signing secret")
case 429:
// Rate limited — back off and retry
let retryAfter = httpResponse.value(forHTTPHeaderField: "Retry-After") ?? "60"
print("Rate limited — retry after \(retryAfter) seconds")
case 400:
// Malformed request — check event payload structure
if let data = data, let body = String(data: data, encoding: .utf8) {
print("Bad request: \(body)")
}
default:
print("Unexpected status: \(httpResponse.statusCode)")
}
}.resume()
Background Sending
For greater reliability — especially for session_end events sent when the app enters the background — use a background URLSession configuration. Background sessions continue their tasks even after the app is suspended:
let backgroundConfig = URLSessionConfiguration.background(
withIdentifier: "com.yourapp.linkzly-gaming"
)
backgroundConfig.timeoutIntervalForRequest = 30
let backgroundSession = URLSession(configuration: backgroundConfig, delegate: self, delegateQueue: nil)
Implement URLSessionDelegate to handle completion events when the system wakes your app to deliver results.
Troubleshooting
No events appearing in the dashboard
- Confirm
sdkKey,organizationId, andgameIdare all correct (no trailing whitespace). - Verify your app has network permission — check
NSAllowsArbitraryLoadsis not blocking HTTPS inInfo.plist. - Use Charles Proxy or the Xcode Network Instruments tool to inspect the outgoing request and response.
401 Unauthorized with HMAC enabled
- Confirm the signing secret matches the value in the Linkzly console under Game Settings → SDK Configuration.
- Check that the device clock is within 5 minutes of UTC. The replay window is fixed at 300 seconds.
- Ensure the body is serialized to bytes before computing the signature, and that the same byte representation is sent as the HTTP body.
Events dropped (events_dropped > 0)
Check that each event includes all required fields: event_id, event_type, timestamp (ISO 8601), platform, player_id, and session_id. Events with missing required fields are dropped silently.
Was this helpful?
Help us improve our documentation