Getting Started
Using AI to Code? Copy This Prompt
Paste this prompt into ChatGPT, Claude, Cursor, or any AI coding assistant to automatically integrate the Sequence SDK into your app.
Overview
Sequence lets you build beautiful onboarding screens in our visual editor, then show them natively in your iOS app. No design skills needed.
Installation
Open Xcode Package Manager
File β Add Packages
Paste this URL:
https://github.com/Musgrav/sequence-swiftImportant: Use Branch, Not Version
main. Do not use a version number.Quick Start
How It Works
Get Your API Credentials
From your Sequence dashboard
Go to Settings in your dashboard to find:
appIdapiKeyInitialize the SDK
Add to your App's init()
import Sequence
@main
struct MyApp: App {
init() {
Sequence.shared.configure(
appId: "your-app-id",
apiKey: "your-api-key",
baseURL: "https://screensequence.com"
)
}
var body: some Scene {
WindowGroup {
ContentView()
}
}
}Show the Onboarding
Add WebViewOnboardingView to your app
import SwiftUI
import Sequence
struct ContentView: View {
@StateObject private var sequence = Sequence.shared
var body: some View {
Group {
if sequence.isOnboardingCompleted {
// Show your paywall or main app
MainAppView()
} else {
// Sequence handles everything with pixel-perfect rendering
WebViewOnboardingView {
print("Onboarding finished!")
}
}
}
}
}Use WebViewOnboardingView
WebViewOnboardingView (not OnboardingView) for pixel-perfect WYSIWYG rendering that matches the editor exactly.Create Your Flow in the Dashboard
Design your screens visually
Go to Flows in your dashboard, add screens, drag in blocks, and hit Publish. Your app will automatically show the new onboarding.
You're all set!
Your app will now fetch and display your onboarding flow. Changes you make in the dashboard appear instantlyβno app update needed.
Core Concepts
Architecture
Sequence follows a decoupled architecture where your onboarding configuration lives on our servers and is fetched by the SDK at runtime:
βββββββββββββββββββ βββββββββββββββββββ βββββββββββββββββββ
β Your Mobile β β Sequence β β Sequence β
β App ββββββΆβ API βββββββ Dashboard β
βββββββββββββββββββ βββββββββββββββββββ βββββββββββββββββββ
β β
β 1. Fetch config β
βββββββββββββββββββββββββ
β β
β 2. Render screens β
β 3. Collect data β
β 4. Track events β
ββββββββββββββββββββββββΆβ
β βKey benefits of this architecture:
- Instant updates: Change your onboarding without releasing a new app version
- A/B testing: The API serves different variants to different users
- Analytics: All events flow back to the dashboard for analysis
- Offline support: Configurations are cached locally for reliability
Screens & Flows
A Flow is a collection of Screens that users navigate through. Each screen has a type that determines its purpose:
| Screen Type | Purpose | Common Use Cases |
|---|---|---|
welcome | First impression screen | App intro, value proposition |
feature | Highlight a feature | Feature tours, benefits |
permission | Request permissions | Notifications, location, camera |
carousel | Multi-slide content | Feature showcase, testimonials |
celebration | Success/completion | Signup complete, achievement |
native | Custom native screen | Login, complex forms |
filler | Transitional content | Loading states, progress |
Screen Transitions
Configure how screens animate when navigating:
// Available transitions
type ScreenTransition =
| 'none' // No animation
| 'fade' // Opacity fade
| 'slide-left' // Slide from right to left
| 'slide-right' // Slide from left to right
| 'slide-up' // Slide from bottom
| 'slide-down' // Slide from top
| 'fade-slide-left' // Fade + slide left
| 'fade-slide-right' // Fade + slide right
| 'scale'; // Scale up from centerContent Blocks
Screens are composed of Content Blocks β reusable UI components that you arrange to build your screens. Sequence provides 11 block types:
| Block | Description | Data Collection |
|---|---|---|
text | Headings, body text, captions | No |
image | Images with various fit modes | No |
button | Interactive buttons with presets | No |
input | Text fields (email, phone, password, etc.) | Yes |
checklist | Single/multi-select options | Yes |
slider | Numeric range selection | Yes |
progress | Progress indicators (bar, dots, ring) | No |
spacer | Vertical spacing | No |
divider | Horizontal line separator | No |
icon | Icons and emojis | No |
feature-card | Card with headline and body | No |
Block Styling
Data Collection
Input blocks (input, checklist, slider) collect user data during onboarding. Each block has a unique id that becomes the key in the collected data object:
// When onboarding completes, you receive:
{
"user_name": "John Doe", // From input block with id="user_name"
"interests": ["fitness", "music"], // From checklist block with id="interests"
"experience_level": 3, // From slider block with id="experience_level"
}
// Handle in your onComplete callback:
<OnboardingModal
onComplete={(collectedData) => {
// Save to your backend
saveUserPreferences(collectedData);
// Or use locally
if (collectedData.interests?.includes('fitness')) {
showFitnessContent();
}
}}
/>Validation
Configure validation rules to ensure users provide required information:
- Input blocks: Mark as required, set input types for format validation
- Checklist blocks: Set minimum/maximum selections
- Navigation: Users cannot proceed until validation passes
Variable System
The variable system lets you create personalized onboarding experiences by collecting user data and displaying it dynamically throughout your flow. Ask for a user's name on screen 1, then greet them by name on screen 2.
Overview
How Variables Work
Variables are created when users interact with data-collecting blocks (input, checklist, slider, etc.). Each block has a fieldName property that defines the variable name. You can then reference this variable anywhere using {fieldName} syntax.
Assigning Variables
Variables are created using these data-collecting blocks. Each block stores its value under the specified fieldName.
| Block Type | Creates Variable | Value Type | Example |
|---|---|---|---|
input | Yes | String | "Alex" |
checklist | Yes | String[] | ["fitness", "nutrition"] |
slider | Yes | Number | 5 |
quiz-question | Yes | String | "option_a" |
goal-selector | Yes | String[] | ["goal_1", "goal_2"] |
Example: Creating an Input Variable
In the editor, drag an Input block onto your screen. In the properties panel, set the Field Name to a snake_case identifier:
{
"type": "input",
"content": {
"fieldName": "user_name", // This creates the variable
"label": "What's your name?",
"placeholder": "Enter your name",
"inputType": "text"
}
}Variable Naming Rules
snake_case for all field names (e.g., user_name, daily_hours). Names must be unique across your entire flow.Referencing Variables
Once a variable is created, reference it in any text content using curly braces:
// In any text block, button, or feature card:
"Welcome back, {user_name}!"
// The variable gets replaced with the actual value:
"Welcome back, Alex!"Where Variables Work
You can use {fieldName} syntax in:
- Text blocks: Headlines, body text, captions
- Button text: Dynamic button labels
- Feature cards: Headlines and body content
- Any text content in the editor
Math Expressions & Formatting
The variable system supports math expressions and number formatting for creating compelling, calculated content.
Math Expressions
Use standard math operators inside curly braces:
// Basic multiplication
"That's {hours_per_day * 365} hours per year!"
// With hours_per_day = 2 β "That's 730 hours per year!"
// Division
"You'll save {minutes_saved * 365 / 60} hours annually"
// With minutes_saved = 30 β "You'll save 182.5 hours annually"
// Complex expressions
"{daily_calories * 7} weekly calories"
"{monthly_savings * 12} saved per year"Number Formatting
Use the | number filter to format large numbers with commas:
// Without formatting
"You've taken {total_steps} steps"
// Shows: "You've taken 2458 steps"
// With | number formatting
"You've taken {total_steps | number} steps"
// Shows: "You've taken 2,458 steps"| Expression | With value = 1234567 | Result |
|---|---|---|
{value} | 1234567 | 1234567 |
{value | number} | 1234567 | 1,234,567 |
{value * 2} | 1234567 | 2469134 |
{value * 2 | number} | 1234567 | 2,469,134 |
Live Preview in Editor
The editor canvas shows live previews of your variables with mock data. Instead of seeing raw {name} text, you'll see interpolated values like "Alex".
What You See in the Editor
Mock data is automatically generated
Hi {name}, welcome!The properties panel still shows the raw template text so you can edit it. The canvas preview helps you visualize the final user experience.
Mock Data Generation
email shows an email address, name shows a name, hours_per_dayshows a number, etc.Data Storage (API)
User-submitted data is stored incrementally as users progress through your flow. This allows you to capture partial data even if users drop off before completing.
How It Works
- Incremental saves: Data is saved after each input (debounced 500ms)
- Session tracking: Each onboarding session has a unique ID
- Completion tracking:
completed_atis set when flow finishes - Partial data capture: Even incomplete flows save their progress
Submissions API
Access collected data via the API:
// Response
{
"submissions": [
{
"id": "sub_123",
"flow_id": "flow_abc",
"user_id": "user_456",
"session_id": "session_789",
"data": {
"name": "Alex",
"email": "alex@example.com",
"goals": ["fitness", "nutrition"]
},
"last_screen_id": "screen_3",
"last_screen_index": 3,
"completed_at": "2024-01-15T10:30:00Z",
"created_at": "2024-01-15T10:25:00Z"
}
],
"total": 1
}Query Parameters
| Parameter | Type | Description |
|---|---|---|
flow_id | string | Filter by specific flow |
user_id | string | Filter by user ID |
completed | boolean | Only completed submissions |
incomplete | boolean | Only incomplete submissions |
limit | number | Max results (default: 100) |
offset | number | Pagination offset |
Database Migration
Run this migration to create the submissions table:
CREATE TABLE IF NOT EXISTS public.onboarding_submissions (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
app_id UUID NOT NULL REFERENCES public.apps(id) ON DELETE CASCADE,
flow_id UUID NOT NULL REFERENCES public.flows(id) ON DELETE CASCADE,
user_id TEXT NOT NULL,
device_id TEXT,
session_id TEXT NOT NULL,
data JSONB NOT NULL DEFAULT '{}',
last_screen_id TEXT,
last_screen_index INT,
completed_at TIMESTAMPTZ,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
);
-- Indexes
CREATE INDEX idx_onboarding_submissions_app_user
ON public.onboarding_submissions(app_id, user_id);
CREATE INDEX idx_onboarding_submissions_flow
ON public.onboarding_submissions(flow_id);
CREATE INDEX idx_onboarding_submissions_session
ON public.onboarding_submissions(session_id);Complete Example Flow
Here's a complete example of a personalized onboarding flow:
Screen 1: Collect Name
Input with fieldName: "name"
{
"type": "input",
"content": {
"fieldName": "name",
"label": "What should we call you?",
"placeholder": "Your name"
}
}Screen 2: Use Name + Collect Hours
Personalized greeting with slider
// Text block using the name variable
{
"type": "text",
"content": {
"text": "Great to meet you, {name}!",
"variant": "h1"
}
}
// Slider to collect hours
{
"type": "slider",
"content": {
"fieldName": "hours_daily",
"label": "Hours per day",
"min": 1,
"max": 8,
"defaultValue": 2
}
}Screen 3: Calculated Impact
Math expression with both variables
{
"type": "text",
"content": {
"text": "{name}, you'll reclaim {hours_daily * 365 | number} hours this year!",
"variant": "h2"
}
}
// With name="Alex" and hours_daily=2:
// "Alex, you'll reclaim 730 hours this year!"Pro Tips
- β’ Collect important data (like name, email) early in the flow
- β’ Use math expressions to show impact and create urgency
- β’ Format large numbers with
| numberfor readability - β’ Test your flow with the live preview to see personalization in action
Auth & Permissions
Sequence supports native Sign In with Apple, permission requests, and app review prompts directly from your onboarding flow. This guide walks you through setting up each feature.
Overview
The SDK uses namespaced action identifiers to trigger native functionality. When a user taps a button, the WebView sends the action to the native SDK, which handles the authentication or permission request, then sends the result back to advance the flow.
How It Works
Available Action Identifiers
| Category | Identifier | Description |
|---|---|---|
| Auth | auth.apple | Sign In with Apple (built-in) |
| Auth | auth.google | Sign In with Google (delegate) |
| Auth | auth.email | Email authentication (delegate) |
| Permission | permission.notifications | Push notification permission |
| Permission | permission.location | Location when in use |
| Permission | permission.locationAlways | Background location |
| Permission | permission.camera | Camera access |
| Permission | permission.photos | Photo library access |
| Permission | permission.tracking | App Tracking Transparency |
| System | system.review | Request App Store review |
| System | system.openSettings | Open app settings |
Sign In with Apple
Sign In with Apple is built into the SDK. The SDK handles the native Apple Sign In UIβyou receive the credential in your delegate and send the identityToken to your backend for verification.
How Returning Users Work
userIdentifier that's the same every time a user signs in with your app. Your backend should store this ID to recognize returning users. On first sign-in, Apple also provides email and name (if user consents). On subsequent sign-ins, only the userIdentifier and identityToken are providedβyour backend uses the token to verify the user and look them up in your database.Add Sign In with Apple Capability
In Xcode project settings
- Open your project in Xcode
- Select your target β Signing & Capabilities tab
- Click + Capability β Add "Sign in with Apple"
Apple Developer Account Required
Set Up the Delegate
To receive authentication credentials
The SDK handles the Apple Sign In UI automatically. You just need to set up a delegate to receive the credentials and send them to your backend:
import UIKit
import Sequence
@main
class AppDelegate: UIResponder, UIApplicationDelegate, SequenceDelegate {
func application(_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
// Configure Sequence
Sequence.shared.configure(
appId: "your-app-id",
apiKey: "your-api-key",
baseURL: "https://screensequence.com"
)
// Set the delegate to receive auth callbacks
Sequence.shared.delegate = self
return true
}
// MARK: - SequenceDelegate
func sequence(_ sequence: Sequence, didCompleteAppleSignIn credential: AppleAuthCredential) {
// credential contains:
// - userIdentifier: Unique user ID (stable across app reinstalls)
// - identityToken: JWT to verify with Apple's servers
// - authorizationCode: One-time code for your backend
// - email: User's email (only on first sign in)
// - fullName: User's name (only on first sign in)
guard let token = credential.identityTokenString else {
Sequence.shared.sendFailureResult(error: "No identity token")
return
}
// Send token to your backend for verification
// Your backend verifies with Apple, then creates/retrieves the user
Task {
do {
try await YourAuthService.signInWithApple(token: token)
// Success! Tell Sequence to advance the flow
Sequence.shared.sendSuccessResult()
} catch {
// Auth failed - tell Sequence so it can handle the error
Sequence.shared.sendFailureResult(error: error.localizedDescription)
}
}
}
func sequence(_ sequence: Sequence, didFailAppleSignIn error: Error) {
print("Apple Sign In failed: \(error.localizedDescription)")
// SDK already sends failure result, flow won't advance
}
}Add the Button in the Editor
Drag from the component palette
- In the Sequence editor, open the Components panel
- Drag the Sign in with Apple button onto your screen
- The button is pre-configured with the correct action identifier
- Publish your flow and test on a real device
Test on Real Device
That's it!
When users tap the button, the native Apple Sign In sheet appears. On success, your delegate receives the credentials and the flow automatically advances to the next screen.
Sign In with Google
Google Sign In requires the Google Sign-In SDK, which you must integrate separately. The Sequence SDK notifies your delegate when Google Sign In is requested.
Install Google Sign-In SDK
Add the GoogleSignIn package
https://github.com/google/GoogleSignIn-iOSFollow Google's setup instructions for configuring your OAuth client ID and adding the URL scheme to your Info.plist.
Handle in Your Delegate
Implement the custom action handler
import GoogleSignIn
extension AppDelegate: SequenceDelegate {
func sequence(_ sequence: Sequence,
didReceiveCustomAction identifier: String,
awaitingResult: Bool) -> Bool {
if identifier == "auth.google" {
// Present Google Sign In
guard let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene,
let rootVC = windowScene.windows.first?.rootViewController else {
Sequence.shared.sendFailureResult(error: "No root view controller")
return true
}
GIDSignIn.sharedInstance.signIn(withPresenting: rootVC) { result, error in
if let error = error {
Sequence.shared.sendFailureResult(error: error.localizedDescription)
return
}
guard let user = result?.user,
let idToken = user.idToken?.tokenString else {
Sequence.shared.sendFailureResult(error: "No ID token")
return
}
// Send credentials to your backend
self.handleGoogleCredentials(idToken: idToken, user: user)
// Tell Sequence to advance the flow
Sequence.shared.sendSuccessResult(data: [
"email": user.profile?.email ?? "",
"name": user.profile?.name ?? ""
])
}
return true // We handled this action
}
return false // Not handled
}
}Continue with Email
Email authentication gives users a password-based sign-in option. Unlike Apple Sign In, the SDK delegates this to your app since you need to handle password collection and storage with your own backend (e.g., Supabase Auth, Firebase Auth).
Important: New vs Returning Users
You cannot reliably check if an email exists before authentication (most auth providers don't expose this for security reasons). Instead, ask the user if they have an existing account, then handle errors gracefully if they choose wrong.
Handle the Delegate Callback
Ask users if they have an account
extension AppDelegate: SequenceDelegate {
func sequenceDidRequestEmailSignIn(_ sequence: Sequence) {
guard let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene,
let rootVC = windowScene.windows.first?.rootViewController else {
Sequence.shared.sendFailureResult(error: "No root view controller")
return
}
// Ask user if they have an account (don't try to auto-detect!)
let alert = UIAlertController(
title: "Continue with Email",
message: "Do you have an existing account?",
preferredStyle: .alert
)
alert.addAction(UIAlertAction(title: "Sign In", style: .default) { _ in
self.showSignInFlow(on: rootVC)
})
alert.addAction(UIAlertAction(title: "Create Account", style: .default) { _ in
self.showSignUpFlow(on: rootVC)
})
alert.addAction(UIAlertAction(title: "Cancel", style: .cancel) { _ in
Sequence.shared.sendCancelledResult()
})
rootVC.present(alert, animated: true)
}
}Handle Sign In (Returning Users)
Skip onboarding for existing users
private func showSignInFlow(on viewController: UIViewController) {
// Collect email and password from user...
Task {
do {
// 1. Authenticate with your backend
try await AuthService.shared.signIn(email: email, password: password)
// 2. Verify with Sequence backend
let result = try await Sequence.shared.verifyEmailAuth(
email: email,
givenName: nil,
familyName: nil
)
// 3. For returning users, exit onboarding entirely
// RootView should then check subscription status and navigate appropriately
if !result.isNewUser {
Sequence.shared.markOnboardingCompleted()
}
// 4. Send success to advance the flow
Sequence.shared.sendSuccessResult(data: [
"email": email,
"isNewUser": result.isNewUser
])
} catch {
// Show error, let user retry
Sequence.shared.sendFailureResult(error: error.localizedDescription)
}
}
}Calling markOnboardingCompleted() for returning users ensures they go directly to your main app (or paywall) instead of continuing through onboarding screens meant for new users.
Handle Sign Up (New Users)
Gracefully handle "already exists" errors
private func showSignUpFlow(on viewController: UIViewController) {
// Collect email and password from user...
Task {
do {
try await AuthService.shared.signUp(email: email, password: password)
// Verify with Sequence
let result = try await Sequence.shared.verifyEmailAuth(
email: email,
givenName: username,
familyName: nil
)
Sequence.shared.sendSuccessResult(data: [
"email": email,
"isNewUser": true
])
} catch {
let errorMsg = error.localizedDescription.lowercased()
// If account already exists, redirect to sign in
if errorMsg.contains("already") ||
errorMsg.contains("registered") ||
errorMsg.contains("exists") {
showSignInFlow(on: viewController) // Redirect to sign in
} else {
// Other error - let user retry sign up
Sequence.shared.sendFailureResult(error: error.localizedDescription)
}
}
}
}When a user selects "Create Account" but their email already exists, redirect them to sign in instead of showing an error. This creates a smoother experience when users forget they already have an account.
Backend Auth API
Sequence provides backend API endpoints to verify authentication tokens and track new vs returning users. These endpoints verify tokens with Apple/Google, manage user records, and return whether a user is new or returning.
When to Use These Endpoints
verifyAppleAuth, verifyGoogleAuth, and verifyEmailAuth methods call these endpoints automatically.POST /api/v1/auth/apple
Verify Apple Sign In credentials and track user authentication.
POST /api/v1/auth/apple
Content-Type: application/json
x-api-key: your-api-key
{
"identity_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...",
"user_identifier": "001234.abcd1234efgh5678.1234",
"email": "user@example.com", // Optional (only on first sign-in)
"given_name": "John", // Optional (only on first sign-in)
"family_name": "Doe" // Optional (only on first sign-in)
}{
"is_new_user": true, // false for returning users
"user_id": "uuid-of-user-record"
}POST /api/v1/auth/google
Verify Google Sign In credentials and track user authentication.
POST /api/v1/auth/google
Content-Type: application/json
x-api-key: your-api-key
{
"id_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...",
"email": "user@gmail.com", // Optional
"given_name": "John", // Optional
"family_name": "Doe" // Optional
}{
"is_new_user": true, // false for returning users
"user_id": "uuid-of-user-record"
}POST /api/v1/auth/email
Track email-based authentication. Unlike Apple/Google, this endpoint does not verify tokensβyour app handles the actual authentication with your auth provider.
POST /api/v1/auth/email
Content-Type: application/json
x-api-key: your-api-key
{
"email": "user@example.com",
"given_name": "John", // Optional
"family_name": "Doe" // Optional
}{
"is_new_user": true, // false for returning users
"user_id": "uuid-of-user-record"
}Using the Response
The is_new_user field determines how to handle the user in your app:
// After receiving auth credentials...
let result = try await Sequence.shared.verifyAppleAuth(
identityToken: token,
userIdentifier: userId,
email: email,
givenName: givenName,
familyName: familyName
)
if result.isNewUser {
// Continue onboarding flow for new users
Sequence.shared.sendSuccessResult()
} else {
// Skip to main app for returning users
Sequence.shared.markOnboardingCompleted()
Sequence.shared.sendSuccessResult()
}Permissions
The SDK handles all iOS permission requests automatically. Just add the required Info.plist entries and use the appropriate button in your flow.
Info.plist Requirements
Add the usage description keys for each permission you request. iOS requires these strings to explain why your app needs access.
<!-- Notifications -->
<key>NSUserNotificationsUsageDescription</key>
<string>We'll send you reminders and updates about your progress.</string>
<!-- Location -->
<key>NSLocationWhenInUseUsageDescription</key>
<string>We use your location to show nearby content.</string>
<key>NSLocationAlwaysAndWhenInUseUsageDescription</key>
<string>We use your location to send you relevant notifications.</string>
<!-- Camera -->
<key>NSCameraUsageDescription</key>
<string>We need camera access to let you take photos.</string>
<!-- Photos -->
<key>NSPhotoLibraryUsageDescription</key>
<string>We need access to your photos to let you choose a profile picture.</string>
<!-- Tracking (iOS 14+) -->
<key>NSUserTrackingUsageDescription</key>
<string>We use this to provide personalized ads and improve your experience.</string>Permission Buttons
Each permission has a pre-configured button in the editor's Components panel:
permission.notifications
permission.location
permission.camera
permission.photos
Result Handling
App Review
Request an App Store review from within your onboarding flow. This uses Apple's StoreKit API, which may or may not show the review prompt depending on Apple's guidelines.
// The button uses this action identifier:
system.review
// Or create manually:
{
"type": "custom",
"identifier": "system.review",
"awaitResult": true,
"onSuccess": { "type": "next" }
}Apple Guidelines
Custom Handlers
For any action identifier not built into the SDK, implement the delegate method to handle it yourself:
extension AppDelegate: SequenceDelegate {
func sequence(_ sequence: Sequence,
didReceiveCustomAction identifier: String,
awaitingResult: Bool) -> Bool {
switch identifier {
case "myapp.connectFitness":
// Present your fitness app connection UI
connectToFitnessApp { success in
if success {
Sequence.shared.sendSuccessResult()
} else {
Sequence.shared.sendFailureResult(error: "Connection failed")
}
}
return true
case "myapp.skipForNow":
// Just advance without doing anything
Sequence.shared.sendSuccessResult()
return true
default:
return false // Not handled, let SDK handle or show error
}
}
}Result Methods
When handling custom actions that await a result, use these methods to send the outcome:
| Method | When to Use | Effect |
|---|---|---|
sendSuccessResult(data:) | Action completed successfully | Executes onSuccess action (default: next screen) |
sendFailureResult(error:) | Action failed | Executes onFailure action (if configured) |
sendCancelledResult() | User cancelled | Executes onCancel action (if configured) |
// Success with optional data
Sequence.shared.sendSuccessResult(data: [
"userId": "12345",
"planType": "premium"
])
// Success without data
Sequence.shared.sendSuccessResult()
// Failure with error message
Sequence.shared.sendFailureResult(error: "Network connection failed")
// User cancelled
Sequence.shared.sendCancelledResult()Migration Guide
This guide walks you through replacing your existing onboarding implementation with Sequence. Whether you have a simple tutorial or a complex multi-screen flow, we'll help you migrate smoothly.
Before You Start
Audit Your Current Onboarding
Document your existing onboarding to plan the migration:
- How many screens do you have?
- What data do you collect from users?
- What actions happen on each screen (permissions, API calls)?
- How do you track completion status?
- Do you have any A/B tests running?
Prepare Your Sequence Dashboard
- Create your app in the Sequence dashboard
- Copy your appId and apiKey from Settings
- Create a new Flow that mirrors your existing screens
Feature Parity
Step-by-Step Migration
Step 1: Install the SDK
Add Sequence alongside your existing onboarding (don't remove anything yet):
In Xcode, go to File β Add Packages and enter:
https://github.com/Musgrav/sequence-swiftStep 2: Initialize the SDK
Configure Sequence without changing existing functionality:
import Sequence
@main
struct MyApp: App {
init() {
Sequence.shared.configure(
appId: "your-app-id",
apiKey: "your-api-key"
)
}
var body: some Scene {
WindowGroup {
// Your existing app code - unchanged
ExistingAppWithOldOnboarding()
}
}
}Step 3: Create a Feature Flag
Use a feature flag to gradually roll out the new onboarding:
import Foundation
enum OnboardingType: String {
case legacy
case sequence
}
class OnboardingTypeManager: ObservableObject {
@Published var type: OnboardingType = .legacy
init() {
checkOnboardingType()
}
private func checkOnboardingType() {
// Check if user should see new onboarding
// Options: remote config, random assignment, user cohort, etc.
if let assigned = UserDefaults.standard.string(forKey: "onboarding_type"),
let onboardingType = OnboardingType(rawValue: assigned) {
self.type = onboardingType
return
}
// Random 50/50 assignment for new users
let newType: OnboardingType = Bool.random() ? .sequence : .legacy
UserDefaults.standard.set(newType.rawValue, forKey: "onboarding_type")
self.type = newType
}
}Step 4: Implement Side-by-Side
Show either the old or new onboarding based on the flag:
import SwiftUI
import Sequence
struct OnboardingGate<Content: View>: View {
@StateObject private var typeManager = OnboardingTypeManager()
@StateObject private var sequence = Sequence.shared
@State private var showLegacy = true
let content: () -> Content
var body: some View {
Group {
if typeManager.type == .legacy && showLegacy {
LegacyOnboarding { data in
handleOnboardingData(data)
showLegacy = false
}
} else if typeManager.type == .sequence && !sequence.isOnboardingCompleted {
WebViewOnboardingView {
// Onboarding completed
}
} else {
content()
}
}
}
private func handleOnboardingData(_ data: [String: Any]) {
// Your existing data handling logic
// Works the same for both onboarding types
saveUserPreferences(data)
trackOnboardingComplete(data)
}
}Step 5: Handle Custom Actions
If your onboarding has custom screens (login, permissions), use the delegate pattern:
// Set up the delegate to handle custom actions
Sequence.shared.delegate = self
// Implement the delegate methods
extension YourViewController: SequenceDelegate {
func sequence(_ sequence: Sequence, didRequestNativeScreen screenId: String, data: [String: Any]) -> Bool {
if screenId == "login-screen" {
// Show your existing login UI
presentLoginScreen {
// Continue onboarding after login
Sequence.shared.track(.screenCompleted, screenId: screenId)
}
return true // Indicates you're handling this screen
}
return false
}
func sequence(_ sequence: Sequence, didTriggerCustomAction action: String, screenId: String) {
switch action {
case "request-notifications":
requestNotificationPermission()
case "connect-social":
showSocialConnect()
default:
break
}
}
}Step 6: Monitor and Iterate
Compare metrics between old and new onboarding:
- Completion rates (visible in Sequence Analytics)
- Time to complete
- Drop-off points
- User feedback
Step 7: Remove Legacy Code
Once you're confident in the new onboarding, remove the feature flag and legacy implementation:
// Final clean implementation
@main
struct MyApp: App {
init() {
Sequence.shared.configure(
appId: "your-app-id",
apiKey: "your-api-key"
)
}
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
struct ContentView: View {
@StateObject private var sequence = Sequence.shared
var body: some View {
Group {
if sequence.isOnboardingCompleted {
MainAppView()
} else {
WebViewOnboardingView {
// Onboarding completed
}
}
}
}
}Data Mapping
Map your existing data collection to Sequence block IDs:
| Your Current Field | Sequence Block Type | Block ID |
|---|---|---|
| Name input | input | user_name |
| Email input | [object Object] (email type) | user_email |
| Interest selection | checklist | interests |
| Experience slider | slider | experience_level |
Keep Block IDs Consistent
Testing Your Migration
Pre-Launch Checklist
- All screens render correctly on iOS and Android
- Navigation between screens works as expected
- All data is collected and passed to onComplete
- Custom actions (permissions, login) function properly
- Analytics events are appearing in the dashboard
- Offline behavior works (cached config loads)
- Completion state persists across app restarts
Testing Commands
import Sequence
// Clear completion status to re-show onboarding
Sequence.shared.reset()
// Or reset just the onboarding state
Sequence.shared.resetOnboarding()SDK Reference
Complete API reference for the Sequence Swift SDK. All classes and methods are available from the Sequence module.
import Sequence
// Core singleton
Sequence.shared
// Configuration
Sequence.shared.configure(appId: String, apiKey: String, baseURL: String?)
// User identification
Sequence.shared.identify(userId: String)
Sequence.shared.reset()
// Event tracking
Sequence.shared.track(_ eventType: EventType, screenId: String?, properties: [String: Any]?)
// Onboarding state
Sequence.shared.isOnboardingCompleted
Sequence.shared.resetOnboarding()Sequence Client
The Sequence.shared singleton manages SDK state, API communication, and event tracking. Initialize it in your app's entry point.
Methods
| Method | Parameters | Returns | Description |
|---|---|---|---|
configure() | appId: String, apiKey: String, baseURL: String? | Void | Initialize the SDK with your credentials |
fetchConfig() | none | async throws -> OnboardingConfig | Fetch onboarding configuration from API |
identify() | userId: String | Void | Associate events with a user ID |
reset() | none | Void | Clear user data and completion status |
track() | _ eventType: EventType, screenId: String?, properties: [String: Any]? | Void | Track a custom event |
Usage Examples
import Sequence
class AuthManager {
// Identify user after login
func handleLogin(userId: String) {
Sequence.shared.identify(userId: userId)
}
// Track custom event
func handleButtonPress() {
Sequence.shared.track(.buttonTapped, screenId: "welcome-screen", properties: [
"button_label": "Get Started"
])
}
// Reset on logout
func handleLogout() {
Sequence.shared.reset()
}
}Configuration
Configure the Sequence SDK in your app's entry point. The configuration must be called before using any other SDK methods.
Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
appId | String | Yes | Your app ID from the dashboard |
apiKey | String | Yes | Your API key (keep secret) |
baseURL | String? | No | Custom API URL (optional) |
Configuration Example
import Sequence
// Configure in your App init
Sequence.shared.configure(
appId: "your-app-id",
apiKey: "your-api-key",
baseURL: "https://screensequence.com"
)Properties
Observable Properties
Sequence.shared is an ObservableObject, so you can use @StateObject or @ObservedObject to observe changes.
import SwiftUI
import Sequence
struct MyView: View {
@StateObject private var sequence = Sequence.shared
var body: some View {
VStack {
// Observe configuration state
if sequence.isConfigured {
Text("SDK is configured")
}
// Observe loading state
if sequence.isLoading {
ProgressView()
}
// Observe onboarding completion
if sequence.isOnboardingCompleted {
MainAppView()
} else {
OnboardingView { }
}
// Access screens
Text("\(sequence.screens.count) screens in this flow")
// Access experiment info
if let experiment = sequence.experimentInfo {
Text("Variant: \(experiment.variantName)")
}
}
}
}Available Properties
| Property | Type | Description |
|---|---|---|
isConfigured | Bool | SDK has been initialized |
isLoading | Bool | Currently fetching config |
error | Error? | Last error encountered |
config | OnboardingConfig? | Current onboarding configuration |
screens | [Screen] | Array of onboarding screens |
isOnboardingCompleted | Bool | Whether onboarding is complete |
experimentInfo | ExperimentInfo? | A/B test variant information |
Views
WebViewOnboardingView (Recommended)
Full-screen SwiftUI view that renders your onboarding flow using a WebView for pixel-perfect WYSIWYG rendering. This is the recommended view to use for production apps.
| Parameter | Type | Required | Description |
|---|---|---|---|
onComplete | (() -> Void)? | No | Called when onboarding completes |
onDataCollected | (([String: Any]) -> Void)? | No | Called with collected user data |
Why WebViewOnboardingView?
import SwiftUI
import Sequence
struct ContentView: View {
@StateObject private var sequence = Sequence.shared
var body: some View {
Group {
if sequence.isOnboardingCompleted {
MainAppView()
} else {
WebViewOnboardingView(
onComplete: {
print("Onboarding completed!")
},
onDataCollected: { data in
print("User data:", data)
saveToBackend(data)
}
)
}
}
}
}OnboardingView (Legacy)
Native SwiftUI view that renders an approximation of your onboarding flow. Not recommended for production use as it may not match the editor exactly.
Avoid OnboardingView
Delegate Protocol
Implement SequenceDelegate to handle custom screens and actions.
import Sequence
class MyAppDelegate: SequenceDelegate {
// Handle custom native screens
func sequence(_ sequence: Sequence, didRequestNativeScreen screenId: String, data: [String: Any]) -> Bool {
if screenId == "auth-screen" {
showAuthFlow()
return true // We're handling this
}
return false // Let Sequence handle it
}
// Handle custom button actions
func sequence(_ sequence: Sequence, didTriggerCustomAction action: String, screenId: String) {
if action == "open-settings" {
if let url = URL(string: UIApplication.openSettingsURLString) {
UIApplication.shared.open(url)
}
}
}
}
// Set the delegate
Sequence.shared.delegate = MyAppDelegate()Swift Types
Key types used in the Swift SDK:
// Onboarding configuration from API
struct OnboardingConfig: Codable {
let version: Int
let screens: [Screen]
let progressIndicator: FlowProgressIndicator?
let transitions: [ScreenTransitionConfig]?
let experiment: ExperimentInfo?
}
// Individual screen
struct Screen: Codable, Identifiable {
let id: String
let appId: String
let flowId: String?
let name: String
let type: ScreenType
let order: Int
let content: ScreenContent
let transition: ScreenTransition?
let createdAt: String
let updatedAt: String
}
enum ScreenType: String, Codable {
case welcome
case feature
case carousel
case permission
case celebration
case native
case filler
}
// Event types for tracking
enum EventType {
case screenViewed
case screenFirstViewed
case screenCompleted
case screenSkipped
case screenDroppedOff
case buttonTapped
case onboardingStarted
case onboardingCompleted
case experimentVariantAssigned
}enum BlockType: String, Codable {
case text
case image
case button
case input
case checklist
case slider
case progress
case spacer
case divider
case icon
case featureCard = "feature-card"
}
// Experiment info
struct ExperimentInfo: Codable {
let id: String
let variantId: String
let variantName: String
}Full Type Definitions