Swift SDK
Build native Apple platform applications that integrate with Staqr Platform and Commerce APIs.
Security Notice
Never embed API keys directly in iOS applications.
Instead, use one of these patterns:
- OAuth2/OIDC user authentication
- Backend proxy for sensitive API calls
- Token-based authentication with refresh
See Mobile Security for detailed guidance.
Installation
Swift Package Manager
Add to your Package.swift:
dependencies: [
.package(url: "https://github.com/staqr/staqr-swift-sdk.git", from: "1.0.0")
]
Or in Xcode: File > Add Packages > Enter the repository URL.
CocoaPods
pod 'StaqrApiClient', '~> 1.0'
Quick Start
import StaqrApiClient
// Configuration - token from secure source (Keychain)
let token = KeychainService.retrieve(forKey: "staqr_token")
StaqrApiClient.basePath = "https://api.staqr.com"
StaqrApiClient.customHeaders["Authorization"] = "Bearer \(token ?? "")"
// List flows
FlowsAPI.listFlows(limit: 50) { data, error in
if let error = error {
print("Error: \(error.localizedDescription)")
return
}
if let flows = data {
print("Found \(flows.count) flows")
for flow in flows {
print(" - \(flow.name) (\(flow.id))")
}
}
}
Available Packages
| Package | API | Platform Support |
|---|---|---|
StaqrApiClient | Staqr Platform | iOS 13+, macOS 10.15+, tvOS 13+, watchOS 6+ |
StaqrCommerceV0 | Commerce v0 (Legacy) | iOS 13+, macOS 10.15+, tvOS 13+, watchOS 6+ |
StaqrCommerceV1 | Commerce v1 | iOS 13+, macOS 10.15+, tvOS 13+, watchOS 6+ |
StaqrCommerceV2 | Commerce v2 | iOS 13+, macOS 10.15+, tvOS 13+, watchOS 6+ |
Authentication
Keychain Storage
import Security
class KeychainService {
static func save(token: String, forKey key: String) -> Bool {
guard let data = token.data(using: .utf8) else { return false }
// Delete existing item first
delete(forKey: key)
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrAccount as String: key,
kSecValueData as String: data,
kSecAttrAccessible as String: kSecAttrAccessibleWhenUnlockedThisDeviceOnly
]
let status = SecItemAdd(query as CFDictionary, nil)
return status == errSecSuccess
}
static func retrieve(forKey key: String) -> String? {
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrAccount as String: key,
kSecReturnData as String: true,
kSecMatchLimit as String: kSecMatchLimitOne
]
var result: AnyObject?
let status = SecItemCopyMatching(query as CFDictionary, &result)
guard status == errSecSuccess,
let data = result as? Data,
let token = String(data: data, encoding: .utf8) else {
return nil
}
return token
}
static func delete(forKey key: String) {
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrAccount as String: key
]
SecItemDelete(query as CFDictionary)
}
}
OAuth2 Integration with ASWebAuthenticationSession
import AuthenticationServices
class AuthService: NSObject, ASWebAuthenticationPresentationContextProviding {
func authenticate() async throws -> String {
let authURL = URL(string: "https://auth.staqr.com/authorize?client_id=your-client-id&redirect_uri=your-app://callback&response_type=code&scope=openid profile email")!
let callbackURLScheme = "your-app"
return try await withCheckedThrowingContinuation { continuation in
let session = ASWebAuthenticationSession(
url: authURL,
callbackURLScheme: callbackURLScheme
) { callbackURL, error in
if let error = error {
continuation.resume(throwing: error)
return
}
guard let callbackURL = callbackURL,
let code = URLComponents(url: callbackURL, resolvingAgainstBaseURL: false)?
.queryItems?.first(where: { $0.name == "code" })?.value else {
continuation.resume(throwing: AuthError.noCode)
return
}
// Exchange code for token
Task {
do {
let token = try await self.exchangeCodeForToken(code)
continuation.resume(returning: token)
} catch {
continuation.resume(throwing: error)
}
}
}
session.presentationContextProvider = self
session.prefersEphemeralWebBrowserSession = false
session.start()
}
}
func presentationAnchor(for session: ASWebAuthenticationSession) -> ASPresentationAnchor {
return UIApplication.shared.windows.first { $0.isKeyWindow }!
}
private func exchangeCodeForToken(_ code: String) async throws -> String {
// Exchange authorization code for access token
// Implementation depends on your auth server
fatalError("Implement token exchange")
}
}
enum AuthError: Error {
case noCode
case tokenExchangeFailed
}
SwiftUI Integration
ViewModel Pattern
import SwiftUI
@MainActor
class FlowsViewModel: ObservableObject {
@Published var flows: [Flow] = []
@Published var isLoading = false
@Published var error: Error?
func loadFlows() async {
isLoading = true
error = nil
defer { isLoading = false }
do {
flows = try await withCheckedThrowingContinuation { continuation in
FlowsAPI.listFlows(limit: 100) { data, error in
if let error = error {
continuation.resume(throwing: error)
} else {
continuation.resume(returning: data ?? [])
}
}
}
} catch {
self.error = error
}
}
}
struct FlowsListView: View {
@StateObject private var viewModel = FlowsViewModel()
var body: some View {
NavigationView {
Group {
if viewModel.isLoading {
ProgressView()
} else if let error = viewModel.error {
VStack {
Text("Error loading flows")
.font(.headline)
Text(error.localizedDescription)
.font(.caption)
.foregroundColor(.secondary)
Button("Retry") {
Task { await viewModel.loadFlows() }
}
}
} else {
List(viewModel.flows, id: \.id) { flow in
NavigationLink(destination: FlowDetailView(flow: flow)) {
VStack(alignment: .leading) {
Text(flow.name)
.font(.headline)
Text(flow.id)
.font(.caption)
.foregroundColor(.secondary)
}
}
}
}
}
.navigationTitle("Flows")
}
.task {
await viewModel.loadFlows()
}
}
}
Using Async/Await Extensions
import Foundation
extension FlowsAPI {
static func listFlowsAsync(limit: Int? = nil) async throws -> [Flow] {
try await withCheckedThrowingContinuation { continuation in
listFlows(limit: limit) { data, error in
if let error = error {
continuation.resume(throwing: error)
} else {
continuation.resume(returning: data ?? [])
}
}
}
}
static func getFlowAsync(id: String) async throws -> Flow {
try await withCheckedThrowingContinuation { continuation in
getFlow(id: id) { data, error in
if let error = error {
continuation.resume(throwing: error)
} else if let flow = data {
continuation.resume(returning: flow)
} else {
continuation.resume(throwing: StaqrError.notFound)
}
}
}
}
}
enum StaqrError: Error {
case notFound
case unauthorized
case networkError
}
Combine Integration
import Combine
extension FlowsAPI {
static func listFlowsPublisher(limit: Int? = nil) -> AnyPublisher<[Flow], Error> {
Future { promise in
listFlows(limit: limit) { data, error in
if let error = error {
promise(.failure(error))
} else {
promise(.success(data ?? []))
}
}
}
.eraseToAnyPublisher()
}
}
// Usage
class FlowsStore: ObservableObject {
@Published var flows: [Flow] = []
private var cancellables = Set<AnyCancellable>()
func loadFlows() {
FlowsAPI.listFlowsPublisher(limit: 100)
.receive(on: DispatchQueue.main)
.sink(
receiveCompletion: { completion in
if case .failure(let error) = completion {
print("Error: \(error)")
}
},
receiveValue: { [weak self] flows in
self?.flows = flows
}
)
.store(in: &cancellables)
}
}
Error Handling
import Foundation
func handleAPIError(_ error: Error) {
if let urlError = error as? URLError {
switch urlError.code {
case .notConnectedToInternet:
showAlert("No Internet Connection", message: "Please check your network settings.")
case .timedOut:
showAlert("Request Timed Out", message: "The server took too long to respond.")
default:
showAlert("Network Error", message: "Please try again later.")
}
} else if let httpError = error as? ErrorResponse {
switch httpError {
case .error(let statusCode, _, _, _):
switch statusCode {
case 401:
// Token expired - trigger re-authentication
NotificationCenter.default.post(name: .tokenExpired, object: nil)
case 403:
showAlert("Access Denied", message: "You don't have permission to perform this action.")
case 404:
showAlert("Not Found", message: "The requested resource was not found.")
case 500...599:
showAlert("Server Error", message: "Something went wrong. Please try again later.")
default:
showAlert("Error", message: "An unexpected error occurred.")
}
}
} else {
showAlert("Error", message: error.localizedDescription)
}
}
extension Notification.Name {
static let tokenExpired = Notification.Name("tokenExpired")
}
App Transport Security
Ensure your Info.plist allows HTTPS connections:
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsArbitraryLoads</key>
<false/>
<key>NSExceptionDomains</key>
<dict>
<key>api.staqr.com</key>
<dict>
<key>NSExceptionRequiresForwardSecrecy</key>
<true/>
<key>NSIncludesSubdomains</key>
<true/>
</dict>
</dict>
</dict>
Requirements
- Swift 5.5+
- iOS 13.0+ / macOS 10.15+ / tvOS 13.0+ / watchOS 6.0+
- Xcode 13.0+
Dependencies
The SDK uses:
Alamofirefor HTTP networking- Foundation for JSON parsing
Next Steps
- Mobile Security Guide - Critical security patterns
- Authentication Guide - Token management
- API Reference - Full endpoint documentation