A native Swift SDK for integrating Sequence onboarding flows into your iOS apps with pixel-perfect WYSIWYG rendering.
- iOS 15.0+
- Xcode 14.0+
- Swift 5.9+
Add Sequence to your project via SPM:
- In Xcode, go to File → Add Packages
- Enter the repository URL:
https://github.com/Musgrav/sequence-swift - IMPORTANT: For the Dependency Rule, select Branch and type
main- Do NOT use a version number - always use branch
mainto get the latest fixes
- Do NOT use a version number - always use branch
- Click Add Package and add to your target
Or add to your Package.swift:
dependencies: [
.package(url: "https://github.com/Musgrav/sequence-swift", branch: "main")
]
⚠️ Important: Always usebranch: "main"instead of a version number to ensure you have the latest rendering fixes and features.
Initialize Sequence in your App's init or AppDelegate:
import Sequence
@main
struct MyApp: App {
init() {
Sequence.shared.configure(
appId: "YOUR_APP_ID",
apiKey: "YOUR_API_KEY",
baseURL: "https://screensequence.com" // Optional for self-hosted
)
}
var body: some Scene {
WindowGroup {
ContentView()
}
}
}Use WebViewOnboardingView for pixel-perfect WYSIWYG rendering that matches exactly what you see in the editor:
import SwiftUI
import Sequence
struct ContentView: View {
@StateObject private var sequence = Sequence.shared
var body: some View {
Group {
if sequence.isOnboardingCompleted {
// Your main app content
MainAppView()
} else {
WebViewOnboardingView {
// Called when onboarding completes
print("Welcome to the app!")
}
}
}
}
}
⚠️ Important: Always useWebViewOnboardingView(notOnboardingView). The WebView version renders your flows exactly as designed in the editor with proper scaling and styling.
Associate analytics events with your user IDs:
// After user authentication
Sequence.shared.identify(userId: "user_12345")
// On logout
Sequence.shared.reset()If your onboarding includes sign-in buttons (Apple, Google, or Email), you must implement the SequenceDelegate protocol. This is a prerequisite for any app using authentication in their onboarding flow.
| Provider | SDK Behavior | Your Responsibility |
|---|---|---|
| Apple | Fully handled by SDK | None - works automatically |
| Delegates to your app | Integrate Google Sign-In SDK, call completion method | |
| Delegates to your app | Implement your auth flow, call completion method |
Create a class that implements SequenceDelegate and set it during configuration:
import Sequence
import GoogleSignIn // If using Google
@main
struct MyApp: App {
init() {
// Configure SDK
Sequence.shared.configure(
appId: "YOUR_APP_ID",
apiKey: "YOUR_API_KEY"
)
// Set delegate for auth handling
Sequence.shared.delegate = AuthHandler.shared
// Configure Google Sign-In (if using Google auth)
GIDSignIn.sharedInstance.configuration = GIDConfiguration(
clientID: "YOUR_GOOGLE_CLIENT_ID.apps.googleusercontent.com"
)
}
}
class AuthHandler: SequenceDelegate {
static let shared = AuthHandler()
private init() {}
// Implement delegate methods below...
}Apple Sign-In is fully handled by the SDK. No implementation required.
When a user taps "Sign in with Apple":
- SDK presents the Apple Sign-In sheet
- User authenticates with Face ID/Touch ID
- SDK verifies the credential with your Sequence backend
- SDK determines if user is new or returning
- Flow routes accordingly (new users continue onboarding, returning users exit)
The delegate method is called for your information only:
func sequence(_ sequence: Sequence, didCompleteAppleSignIn credential: AppleAuthCredential) {
// Optional: Store user info in your app
print("Apple Sign-In completed for user: \(credential.userIdentifier)")
// Available properties:
// - credential.userIdentifier (stable user ID - always available)
// - credential.email (only provided on FIRST sign-in)
// - credential.fullName?.givenName (only on first sign-in)
// - credential.fullName?.familyName (only on first sign-in)
}
func sequence(_ sequence: Sequence, didFailAppleSignIn error: Error) {
print("Apple Sign-In failed: \(error)")
}Google Sign-In requires the Google Sign-In SDK which you must integrate separately.
Add to your Package.swift:
.package(url: "https://github.com/google/GoogleSignIn-iOS", from: "7.0.0")Or via Xcode: File → Add Packages → https://github.com/google/GoogleSignIn-iOS
- Go to Google Cloud Console
- Create OAuth 2.0 Client ID for iOS
- Add your bundle ID
- Download and add
GoogleService-Info.plistto your project
import Sequence
import GoogleSignIn
class AuthHandler: SequenceDelegate {
static let shared = AuthHandler()
// Handle the auth.google custom action
func sequence(_ sequence: Sequence, didReceiveCustomAction identifier: String, awaitingResult: Bool) -> Bool {
if identifier == "auth.google" {
performGoogleSignIn()
return true // We're handling this action
}
return false
}
private func performGoogleSignIn() {
// Get the root view controller
guard let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene,
let rootViewController = windowScene.windows.first?.rootViewController else {
Sequence.shared.sendFailureResult(error: "No root view controller")
return
}
// Find the topmost presented controller
var topController = rootViewController
while let presented = topController.presentedViewController {
topController = presented
}
// Present Google Sign-In
GIDSignIn.sharedInstance.signIn(withPresenting: topController) { result, error in
// Handle cancellation
if let error = error {
if (error as NSError).code == GIDSignInError.canceled.rawValue {
Sequence.shared.sendCancelledResult()
} else {
Sequence.shared.sendFailureResult(error: error.localizedDescription)
}
return
}
// Get the ID token
guard let user = result?.user,
let idToken = user.idToken?.tokenString else {
Sequence.shared.sendFailureResult(error: "Failed to get Google ID token")
return
}
// Create credential and complete sign-in
let credential = GoogleAuthCredential(
idToken: idToken,
accessToken: user.accessToken.tokenString,
email: user.profile?.email,
displayName: user.profile?.name
)
// This verifies with backend and sends result to WebView
Task {
await Sequence.shared.completeGoogleSignIn(credential: credential)
}
}
}
// Optional: Called after completion for your own tracking
func sequence(_ sequence: Sequence, didCompleteGoogleSignIn credential: GoogleAuthCredential) {
print("Google Sign-In completed for: \(credential.email ?? "unknown")")
}
func sequence(_ sequence: Sequence, didFailGoogleSignIn error: Error) {
print("Google Sign-In failed: \(error)")
}
}- User taps "Sign in with Google" in onboarding
- SDK calls
sequence(_:didReceiveCustomAction:awaitingResult:)with"auth.google" - Your app presents Google Sign-In UI
- User authenticates with Google
- Your app calls
Sequence.shared.completeGoogleSignIn(credential:) - SDK verifies the ID token with your Sequence backend
- Backend checks if user exists (by
google_user_id) - SDK sends
isNewUserflag back to the onboarding flow - Flow routes: new users continue onboarding, returning users exit to main app
Email authentication is fully customizable. Implement whatever auth method your app uses (password, magic link, OTP, etc.), then tell the SDK the result.
import Sequence
class AuthHandler: SequenceDelegate {
static let shared = AuthHandler()
// Called when email sign-in is requested
func sequenceDidRequestEmailSignIn(_ sequence: Sequence) {
presentEmailAuth()
}
private func presentEmailAuth() {
guard let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene,
let rootViewController = windowScene.windows.first?.rootViewController else {
Sequence.shared.sendFailureResult(error: "No root view controller")
return
}
// Find topmost controller
var topController = rootViewController
while let presented = topController.presentedViewController {
topController = presented
}
// Present your email auth UI
let emailAuthVC = YourEmailAuthViewController()
emailAuthVC.onSuccess = { email, firstName, lastName in
self.completeEmailAuth(email: email, givenName: firstName, familyName: lastName)
}
emailAuthVC.onCancel = {
Sequence.shared.sendCancelledResult()
}
emailAuthVC.onError = { errorMessage in
Sequence.shared.sendFailureResult(error: errorMessage)
}
topController.present(emailAuthVC, animated: true)
}
private func completeEmailAuth(email: String, givenName: String?, familyName: String?) {
Task {
do {
// Verify with backend to check if new or returning user
let result = try await Sequence.shared.verifyEmailAuth(
email: email,
givenName: givenName,
familyName: familyName
)
// Send success with isNewUser flag
Sequence.shared.sendSuccessResult(data: [
"email": email,
"givenName": givenName as Any,
"familyName": familyName as Any,
"isNewUser": result.isNewUser
])
} catch {
Sequence.shared.sendFailureResult(error: error.localizedDescription)
}
}
}
}private func authenticateWithPassword(email: String, password: String) {
// Call your backend to authenticate
YourAuthService.signIn(email: email, password: password) { [weak self] result in
switch result {
case .success(let user):
// User authenticated - now verify with Sequence
self?.completeEmailAuth(
email: user.email,
givenName: user.firstName,
familyName: user.lastName
)
case .failure(let error):
Sequence.shared.sendFailureResult(error: error.localizedDescription)
}
}
}// Step 1: Send magic link or OTP
func sendMagicLink(email: String) {
YourAuthService.sendMagicLink(email: email) { error in
if let error = error {
Sequence.shared.sendFailureResult(error: error.localizedDescription)
} else {
// Show code entry UI
self.showCodeEntry(email: email)
}
}
}
// Step 2: Verify code
func verifyCode(email: String, code: String) {
YourAuthService.verifyCode(email: email, code: code) { result in
switch result {
case .success(let user):
// Verify with Sequence backend
Task {
do {
let result = try await Sequence.shared.verifyEmailAuth(
email: email,
givenName: user.firstName,
familyName: user.lastName
)
Sequence.shared.sendSuccessResult(data: [
"email": email,
"isNewUser": result.isNewUser
])
} catch {
Sequence.shared.sendFailureResult(error: error.localizedDescription)
}
}
case .failure(let error):
Sequence.shared.sendFailureResult(error: error.localizedDescription)
}
}
}- User taps "Sign in with Email" in onboarding
- SDK calls
sequenceDidRequestEmailSignIn(_:) - Your app presents your email auth UI
- User authenticates through your system
- Your app calls
Sequence.shared.verifyEmailAuth(email:givenName:familyName:) - SDK sends the email to your Sequence backend
- Backend checks if user exists with that email (and
auth_provider = 'email') - Your app calls
sendSuccessResult(data:)withisNewUserflag - Flow routes: new users continue onboarding, returning users exit to main app
All auth methods return an isNewUser flag that determines the flow:
isNewUser |
Behavior |
|---|---|
true |
User continues through onboarding flow |
false |
Flow completes immediately, user goes to main app |
Configure this in your Sequence dashboard by setting "New User Screen" and "Returning User Screen" on auth button elements.
The backend determines isNewUser by checking:
- Apple: Does a user with this
apple_user_idexist for this app? - Google: Does a user with this
google_user_idexist for this app? - Email: Does a user with this email and
auth_provider = 'email'exist for this app?
Here's a complete example implementing all three auth methods:
import UIKit
import Sequence
import GoogleSignIn
class AuthHandler: SequenceDelegate {
static let shared = AuthHandler()
private init() {}
// MARK: - Apple Sign-In (automatic)
func sequence(_ sequence: Sequence, didCompleteAppleSignIn credential: AppleAuthCredential) {
print("Apple Sign-In: \(credential.userIdentifier)")
// Store locally if needed
UserDefaults.standard.set(credential.userIdentifier, forKey: "appleUserId")
}
func sequence(_ sequence: Sequence, didFailAppleSignIn error: Error) {
print("Apple Sign-In failed: \(error)")
}
// MARK: - Google Sign-In (manual)
func sequence(_ sequence: Sequence, didReceiveCustomAction identifier: String, awaitingResult: Bool) -> Bool {
if identifier == "auth.google" {
performGoogleSignIn()
return true
}
return false
}
func sequence(_ sequence: Sequence, didCompleteGoogleSignIn credential: GoogleAuthCredential) {
print("Google Sign-In: \(credential.email ?? "unknown")")
}
func sequence(_ sequence: Sequence, didFailGoogleSignIn error: Error) {
print("Google Sign-In failed: \(error)")
}
// MARK: - Email Sign-In (manual)
func sequenceDidRequestEmailSignIn(_ sequence: Sequence) {
presentEmailAuth()
}
// MARK: - Private Implementation
private func performGoogleSignIn() {
guard let topVC = getTopViewController() else {
Sequence.shared.sendFailureResult(error: "No view controller")
return
}
GIDSignIn.sharedInstance.signIn(withPresenting: topVC) { result, error in
if let error = error {
if (error as NSError).code == GIDSignInError.canceled.rawValue {
Sequence.shared.sendCancelledResult()
} else {
Sequence.shared.sendFailureResult(error: error.localizedDescription)
}
return
}
guard let user = result?.user,
let idToken = user.idToken?.tokenString else {
Sequence.shared.sendFailureResult(error: "Failed to get ID token")
return
}
let credential = GoogleAuthCredential(
idToken: idToken,
accessToken: user.accessToken.tokenString,
email: user.profile?.email,
displayName: user.profile?.name
)
Task {
await Sequence.shared.completeGoogleSignIn(credential: credential)
}
}
}
private func presentEmailAuth() {
guard let topVC = getTopViewController() else {
Sequence.shared.sendFailureResult(error: "No view controller")
return
}
let emailVC = EmailAuthViewController()
emailVC.onComplete = { result in
switch result {
case .success(let email, let firstName, let lastName):
Task {
do {
let verifyResult = try await Sequence.shared.verifyEmailAuth(
email: email,
givenName: firstName,
familyName: lastName
)
Sequence.shared.sendSuccessResult(data: [
"email": email,
"isNewUser": verifyResult.isNewUser
])
} catch {
Sequence.shared.sendFailureResult(error: error.localizedDescription)
}
}
case .cancelled:
Sequence.shared.sendCancelledResult()
case .error(let message):
Sequence.shared.sendFailureResult(error: message)
}
}
topVC.present(emailVC, animated: true)
}
private func getTopViewController() -> UIViewController? {
guard let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene,
let rootVC = windowScene.windows.first?.rootViewController else {
return nil
}
var topVC = rootVC
while let presented = topVC.presentedViewController {
topVC = presented
}
return topVC
}
}
// Helper enum for email auth results
enum EmailAuthResult {
case success(email: String, firstName: String?, lastName: String?)
case cancelled
case error(String)
}Google Sign-In not working:
- Verify Google Sign-In SDK is added to your project
- Check your OAuth client ID is correct
- Ensure bundle ID matches Google Cloud Console
- Confirm you return
truefromdidReceiveCustomActionfor"auth.google"
Email auth result not routing correctly:
- Ensure you call
verifyEmailAuth()beforesendSuccessResult() - Check that
isNewUseris included in the success data - Verify your backend logs for errors
User always treated as new:
- Check Sequence backend logs
- Verify API key is correct
- Ensure user ID/email is sent correctly
The SDK automatically tracks:
onboarding_started- When user begins onboardingscreen_viewed- When each screen is displayedscreen_completed- When user completes a screenscreen_skipped- When user skips a screenbutton_tapped- When user taps a buttononboarding_completed- When user finishes onboarding
Events are batched and sent every 10 seconds or when the batch reaches 20 events.
You can also track custom events:
Sequence.shared.track(
eventType: .buttonTapped,
screenId: "screen_123",
properties: ["button_text": "Subscribe"]
)For screens that require native functionality, use the onNativeScreen callback:
OnboardingView(
onComplete: { /* ... */ },
onNativeScreen: { screen in
// Return your custom SwiftUI view
if screen.content.identifier == "custom_permissions" {
return AnyView(CustomPermissionsView())
}
return AnyView(EmptyView())
}
)For testing or showing onboarding again:
Sequence.shared.resetOnboarding()| Parameter | Type | Description |
|---|---|---|
appId |
String | Your Sequence App ID |
apiKey |
String | Your Sequence API Key |
baseURL |
String? | Custom API URL (for self-hosted) |
Find your credentials in the Sequence dashboard under Settings.
Main singleton class for SDK configuration and event tracking.
// Configuration
Sequence.shared.configure(appId:apiKey:baseURL:)
// User identification
Sequence.shared.identify(userId:)
Sequence.shared.reset()
// Event tracking
Sequence.shared.track(eventType:screenId:properties:)
Sequence.shared.trackScreenViewed(screenId:screenName:)
Sequence.shared.trackScreenCompleted(screenId:screenName:)
Sequence.shared.trackScreenSkipped(screenId:screenName:)
Sequence.shared.trackButtonTapped(screenId:buttonText:)
// Onboarding state
Sequence.shared.isOnboardingCompleted
Sequence.shared.markOnboardingCompleted()
Sequence.shared.resetOnboarding()
// Manual flush
Sequence.shared.flush()SwiftUI view that renders your onboarding flow using a WebView for pixel-perfect WYSIWYG rendering. This is the recommended view to use.
WebViewOnboardingView(
onComplete: (() -> Void)? = nil,
onDataCollected: (([String: Any]) -> Void)? = nil
)Why use WebViewOnboardingView:
- Pixel-perfect rendering that matches the web editor exactly
- Proper scaling on all device sizes
- Full support for all styling options (shadows, gradients, etc.)
- Consistent behavior across iOS versions
Native SwiftUI view that renders an approximation of your onboarding flow. Not recommended for production use as it may not match the editor exactly.
OnboardingView(
onComplete: () -> Void,
onNativeScreen: ((Screen) -> AnyView)?
)
⚠️ Warning:OnboardingViewuses native SwiftUI components which may not render identically to what you see in the editor. Always useWebViewOnboardingViewfor production apps.
welcome- Welcome/intro screensfeature- Feature highlightscarousel- Multi-slide carouselspermission- Permission requestscelebration- Completion celebrationsnative- Custom native screens
screenViewedscreenCompletedscreenSkippedbuttonTappedonboardingStartedonboardingCompleted
The SDK provides detailed error types:
do {
let config = try await Sequence.shared.fetchConfig()
} catch SequenceError.notConfigured {
print("SDK not configured")
} catch SequenceError.invalidCredentials {
print("Invalid API key")
} catch SequenceError.networkError(let message) {
print("Network error: \(message)")
}- Documentation: (https://www.screensequence.com/docs#mg-step-by-step)
- Issues: GitHub Issues
MIT License - see LICENSE file for details.