iOS
Native iOS SDK that integrates the Zing Fitness experience into your app. The SDK ships as an SPM package via GitHub.
Requirements
| Requirement | Value / Notes |
|---|---|
| Minimum iOS version | 16 |
| Xcode iOS SDK | 16+ |
| Swift Package Manager | 5.10 |
Setup
Add the dependency to your Package.swift:
dependencies: [
.package(url: "https://github.com/Muze-Fitness/zing-coach-sdk-ios.git", from: "2.3.0")
]Then add ZingCoachSDK to your target’s dependencies:
.target(
name: "YourTarget",
dependencies: ["ZingCoachSDK"]
)Initialization
The SDK instance owns internal state and resources. Retain it at app-level scope for the application’s lifetime; releasing it deallocates internal state and interrupts background sync.
Authentication is not part of initialization. Pass credentials later with login(with:) — see Authentication.
InitializationParameters
| Parameter | Type | Default | Notes |
|---|---|---|---|
configuration | Configuration | Configuration() | Coach availability, gender availability, HealthKit background delivery |
Theming
Assign sdk.theme after ZingSDK.initialize(with:). A later assignment replaces the theme on the same SDK instance.
Theme.default is the built-in light theme. Theme.defaultDark is the built-in dark theme.
sdk.theme = .defaultDarkTo override individual tokens, start from a built-in theme and call byApplying. Each closure receives the token and its current value. Return that current value for every token you leave unchanged.
let theme = Theme.default.byApplying(
colorsTransform: { token, color in
switch token {
case .brand(.primary):
UIColor(red: 0x00/255, green: 0x40/255, blue: 0x70/255, alpha: 1)
case .brand(.secondary):
UIColor(red: 0x00/255, green: 0x4F/255, blue: 0x91/255, alpha: 1)
default:
color
}
},
assetsTransform: { token, image in
switch token {
case .planBackground:
UIImage(named: "MyPlanBackground") ?? image
default:
image
}
}
)
sdk.theme = theme| Transform | Token type | Platform type |
|---|---|---|
colorsTransform | ColorToken | UIColor |
spacingsTransform | SizeToken | CGFloat |
cornersRoundingTransform | RadiusToken | RadiusAttribute |
typographyTransform | TypographyToken | TypographyAttributes |
assetsTransform | AssetToken | UIImage |
blurStyleTransform | BlurToken | UIBlurEffect.Style |
Color tokens are grouped as bg, border, brand, button, cardBackground, cv, fg, heading, overlay, overlayCard, and textBody. brand includes .primary, .secondary, .tertiary, and .text.
Configuration
Coach availability
| Case | Behaviour |
|---|---|
.allCoaches | All coaches are available to every user (default) |
.userGenderBased | Coaches are filtered based on the user’s gender |
Gender availability
| Case | Behaviour |
|---|---|
.all | All gender options are available (default) |
.binary | Only male and female options are presented |
HealthKit background delivery
Set ahBackgroundDeliveryEnabled to true to enable HealthKit background delivery so the SDK can sync Apple Health data while your app is suspended. Your app must include the HealthKit entitlement and request the appropriate read authorizations.
Initializing the SDK
do {
let sdk = try await ZingSDK.initialize(with: ZingSDK.InitializationParameters(
configuration: ZingSDK.Configuration(
coachesAvailability: .allCoaches,
genderAvailability: .all,
ahBackgroundDeliveryEnabled: false
)
))
sdk.theme = theme
sdk.criticalErrorHandler = self
self.sdk = sdk
} catch {
print("Initialization failed: \(error)")
}Authentication
Supply credentials when you call login(with:), not during initialization.
For the backend requirements behind these two modes — the JWKS your server must publish for JWT auth, the required claims, and why the API key is the weaker option — see Authorization.
Call login(with:) only when loginState is .loggedOut — typically only on the first session. On subsequent launches, ZingSDK.initialize(with:) automatically restores the previous session and loginState will already be .loggedIn. Calling login(with:) while already logged in throws .alreadyLoggedIn.
JWT token authentication
Use JWT token authentication when your backend manages user accounts and issues JWT tokens.
Token requirements:
- Valid JWT format
- Must contain
subclaim with the partner user ID - Recommended token lifetime: 15 minutes
Obtain a token from your backend, then pass it to login(with:):
do {
let token = try await yourAuthService.getToken()
try await sdk.login(with: .jwtToken(token: token))
} catch {
// Handle LoginError
}API Key Authentication
Use this mode for testing or when backend integration is not available. Pass a stable partnerUserID so the same user is restored across sessions. If you omit it (nil), the SDK generates a unique identifier.
try await sdk.login(with: .apiKey(key: "your-api-key", partnerUserID: "your-user-id"))Login
do {
try await sdk.login(with: authentication)
} catch {
// Handle LoginError
}Logout
Call logout() when your user signs out of your app:
do {
try await sdk.logout()
} catch {
// Handle LogoutError (e.g. .notLoggedIn)
}Logout clears the user’s token and removes all locally cached data. You can call login(with:) again to start a new session.
Login state
// Current state
let state: ZingSDK.LoginState = sdk.loginState // .loggedOut | .inProgress | .loggedIn(partnerUserID: String)
// Reactive updates
sdk.loginStatePublisher
.sink { state in /* handle state changes */ }
.store(in: &cancellables)
// Quick checks
let isLoggedIn: Bool = sdk.isLoggedIn
let isOnboardingCompleted: Bool = sdk.isOnboardingCompletedCritical errors
Assign a CriticalErrorHandler after initialization to be notified when a restored session can no longer be refreshed — typically HTTP 400 or 401 from the token refresh endpoint. Log the user out of your app and call login(with:) again with fresh credentials.
extension YourErrorHandler: CriticalErrorHandler {
func sdkDidFail(with error: Error) {
// Session is no longer valid — logout and re-authenticate
if let authError = error as? AuthError {
switch authError {
case .invalidRefreshCredentialsRequest,
.invalidCredentialsForRefreshRequest,
.refreshRequestedWithoutStoredCredentials,
.badToken:
break
}
}
}
}Errors
enum LoginError: Error, LocalizedError {
/// Login was attempted while a user is already logged in.
case alreadyLoggedIn
/// Login was attempted while another login operation is in progress.
case loginAlreadyInProgress
/// Token retrieval failed during the login process.
case failedToGetToken
/// Profile synchronisation failed after successful token retrieval.
case failedToGetProfile
/// Data synchronisation failed after successful login.
case failedToSyncDataAfterLogin
}
enum LogoutError: Error {
/// Logout was attempted when no user is currently logged in.
case notLoggedIn
}
enum AuthError: Error {
/// Token refresh was rejected with HTTP 400.
case invalidRefreshCredentialsRequest
/// Token refresh was rejected with HTTP 401.
case invalidCredentialsForRefreshRequest
/// The token is invalid: malformed JWT or missing required sub claim.
case badToken
/// A token refresh was requested but no stored credentials are available.
case refreshRequestedWithoutStoredCredentials
}
protocol CriticalErrorHandler {
func sdkDidFail(with error: Error)
}
extension ZingSDK {
enum ProfileUpdateError: Error, LocalizedError {
/// Profile parameters were set while no user is logged in.
case notLoggedIn
var errorDescription: String? { "Not logged in" }
}
enum ScreenPresentationError: Error {
/// A screen was requested while no user is logged in.
case notLoggedIn
}
}User profile
You can supply user profile data to personalise the SDK experience. Log in before setting profile parameters — setProfileParams(_:) throws ProfileUpdateError.notLoggedIn when no user is logged in.
If onboarding is not complete (isOnboardingCompleted == false), the values seed the onboarding flow. After onboarding is complete, they update the existing profile.
ProfileParameters
All fields are optional. Omit any field to leave the existing value unchanged.
| Field | Type | Notes |
|---|---|---|
name | String? | Display name |
gender | UserGender? | .male, .female, .other, .preferNotToSay |
height | Double? | In centimeters (cm). Values outside the valid range are ignored. |
weight | Double? | In kilograms (kg). Values outside the valid range are ignored. |
age | Int? | Age in full years |
measurementSystem | Unit? | .metric or .imperial |
Setting the profile
let params = ProfileParameters(
name: "Username",
gender: .female,
height: 178,
weight: 75,
age: 30,
measurementSystem: .metric
)
do {
try sdk.setProfileParams(params)
} catch {
// Handle ProfileUpdateError (e.g. .notLoggedIn)
}Primary location
Call setPrimaryLocationID(_:) after login to store the member’s primary location id from your organisation. The SDK writes it to the user profile and syncs it. The call throws ProfileUpdateError.notLoggedIn when no user is logged in.
do {
try sdk.setPrimaryLocationID("your-location-id")
} catch {
// Handle ProfileUpdateError (e.g. .notLoggedIn)
}SDK modules
Call sdk.makeScreen(_:) with a ZingSDK.Screen value to build a screen, then present the returned UIViewController. It throws ScreenPresentationError.notLoggedIn when no user is logged in — log the user in with sdk.login(with:) first (see Authentication), then try again.
If the user has not completed onboarding, the SDK shows the onboarding flow before the requested screen. To present onboarding on its own, use .onboarding.
do {
let viewController = try sdk.makeScreen(
.program(configuration: .init(showCloseButton: true))
)
present(viewController, animated: true)
} catch {
// error == .notLoggedIn — call sdk.login(with:) before opening a screen
}Available screens
Screen case | Description |
|---|---|
.program(configuration:) | Main screen with the personalized workout experience. Requires ProgramScreenConfiguration. |
.onboarding | Full onboarding flow. Completes and dismisses automatically. |
.assistantChat | Chat with the AI fitness coach |
.profileSettings | Profile and plan settings |
.fullSchedule | Full workout schedule |
.customWorkout | Custom workout builder |
.bodyScan(useFrontCamera:) | Body scan. Defaults to true (front camera). Pass false to use the rear camera. |
.flexibilityTest(useFrontCamera:) | Flexibility test. Defaults to true (front camera). |
.fitnessTest(useFrontCamera:) | Fitness test. Defaults to true (front camera). |
Program screen
Pass a ProgramScreenConfiguration when you open the Program screen.
| Property | Type | Default | Description |
|---|---|---|---|
showCloseButton | Bool | Required | Shows a close button that dismisses the screen. Set to false when you embed Program in your own navigation, such as a tab bar. |
showAskCoachButton | Bool | true | Shows the Ask Coach button. |
showBodyScanWidget | Bool | true | Shows the Body Scan card on the Program screen. The Fitness Test and Flexibility Test cards stay visible. |
do {
let viewController = try sdk.makeScreen(
.program(configuration: .init(
showCloseButton: true,
showAskCoachButton: true,
showBodyScanWidget: true
))
)
present(viewController, animated: true)
} catch {
// error == .notLoggedIn — call sdk.login(with:) before opening a screen
}Onboarding
Present the full onboarding flow explicitly:
do {
let viewController = try sdk.makeScreen(.onboarding)
present(viewController, animated: true)
} catch {
// error == .notLoggedIn — call sdk.login(with:) before opening a screen
}On completion, the SDK updates the user profile and dismisses the screen. You can open onboarding again later with the same call.