Skip to main content
This guide will walk you through implementing session management in your Swift app using the Dynamic Swift SDK. You’ll learn how to manage authentication state, handle reactive UI updates with Combine, and create a seamless user experience.

Overview

Session management is a crucial part of any Web3 app. The Dynamic Swift SDK provides powerful Combine-based publishers for managing user sessions, authentication state, and wallet updates. This guide covers the practical implementation patterns you’ll need to build a robust session management system.

Key Concepts

Reactive State with Combine

The SDK provides Combine publishers that automatically emit updates when state changes:
  • authenticatedUserChanges - Emits when user logs in or out
  • tokenChanges - Emits when the auth token changes
  • userWalletsChanges - Emits when wallets are created or updated

Automatic UI Updates

By subscribing to these publishers in your SwiftUI views, the UI automatically updates when:
  • Users log in or out
  • Authentication tokens refresh
  • Wallets are connected or created
  • Network connections change

Implementation Patterns

1. Basic Session Management

Start with a simple session management setup using a ViewModel:
import DynamicSDKSwift
import SwiftUI
import Combine

@main
struct DynamicSDKExampleApp: App {
    init() {
        // Initialize SDK at app launch
        _ = DynamicSDK.initialize(
            props: ClientProps(
                environmentId: ProcessInfo.processInfo.environment["DYNAMIC_ENVIRONMENT_ID"] ?? "",
                appLogoUrl: "https://your-app.com/logo.png",
                appName: "Your App",
                redirectUrl: "yourapp://",
                appOrigin: "https://your-app.com"
            )
        )
    }

    var body: some Scene {
        WindowGroup {
            ContentView()
        }
    }
}

struct ContentView: View {
    @StateObject private var vm = SessionViewModel()

    var body: some View {
        Group {
            if vm.isLoading {
                LoadingView()
            } else if vm.isAuthenticated {
                MainAppView()
            } else {
                LoginView()
            }
        }
    }
}

@MainActor
class SessionViewModel: ObservableObject {
    @Published var isAuthenticated = false
    @Published var isLoading = true
    @Published var user: UserProfile?
    @Published var wallets: [BaseWallet] = []

    private let sdk = DynamicSDK.instance()
    private var cancellables = Set<AnyCancellable>()

    init() {
        setupObservers()
    }

    private func setupObservers() {
        // Observe authentication state
        sdk.auth.authenticatedUserChanges
            .receive(on: DispatchQueue.main)
            .sink { [weak self] user in
                self?.isAuthenticated = user != nil
                self?.user = user
                self?.isLoading = false
            }
            .store(in: &cancellables)

        // Observe wallet changes
        sdk.wallets.userWalletsChanges
            .receive(on: DispatchQueue.main)
            .sink { [weak self] wallets in
                self?.wallets = wallets
            }
            .store(in: &cancellables)
    }
}

2. Complete Session Manager

For production apps, implement a comprehensive session manager:
import DynamicSDKSwift
import SwiftUI
import Combine

@MainActor
class SessionManager: ObservableObject {
    @Published var isAuthenticated = false
    @Published var user: UserProfile?
    @Published var wallets: [BaseWallet] = []
    @Published var token: String?
    @Published var isCreatingWallets = false
    @Published var error: String?

    private let sdk = DynamicSDK.instance()
    private var cancellables = Set<AnyCancellable>()

    init() {
        setupObservers()
    }

    private func setupObservers() {
        // Initial values
        isAuthenticated = sdk.auth.authenticatedUser != nil
        user = sdk.auth.authenticatedUser
        wallets = sdk.wallets.userWallets
        token = sdk.auth.token

        // Check if wallets are being created
        if user != nil && wallets.isEmpty {
            isCreatingWallets = true
        }

        // Observe authentication state
        sdk.auth.authenticatedUserChanges
            .receive(on: DispatchQueue.main)
            .sink { [weak self] user in
                guard let self else { return }
                self.isAuthenticated = user != nil
                self.user = user

                if user == nil {
                    // User logged out
                    self.wallets = []
                    self.isCreatingWallets = false
                } else if self.wallets.isEmpty {
                    // User just authenticated, wallets being created
                    self.isCreatingWallets = true
                }
            }
            .store(in: &cancellables)

        // Observe wallet changes
        sdk.wallets.userWalletsChanges
            .receive(on: DispatchQueue.main)
            .sink { [weak self] wallets in
                guard let self else { return }
                self.wallets = wallets

                // Wallets appeared, stop showing loading
                if !wallets.isEmpty {
                    self.isCreatingWallets = false
                }
            }
            .store(in: &cancellables)

        // Observe token changes
        sdk.auth.tokenChanges
            .receive(on: DispatchQueue.main)
            .sink { [weak self] token in
                self?.token = token
            }
            .store(in: &cancellables)
    }

    func logout() async {
        do {
            try await sdk.auth.logout()
        } catch {
            self.error = "Logout failed: \(error.localizedDescription)"
        }
    }

    func showUserProfile() {
        sdk.ui.showUserProfile()
    }
}

3. Using Session Manager in SwiftUI

import SwiftUI
import DynamicSDKSwift

struct AppRootView: View {
    @StateObject private var session = SessionManager()

    var body: some View {
        NavigationStack {
            Group {
                if session.isAuthenticated {
                    HomeView(session: session)
                } else {
                    LoginView()
                }
            }
        }
        .onReceive(
            NotificationCenter.default.publisher(for: UIApplication.didBecomeActiveNotification)
        ) { _ in
            // SDK automatically refreshes state, no manual refresh needed
        }
    }
}

struct HomeView: View {
    @ObservedObject var session: SessionManager

    var body: some View {
        VStack(spacing: 16) {
            if let user = session.user {
                Text("Welcome, \(user.email ?? "User")!")
            }

            // Wallets section
            if session.isCreatingWallets {
                HStack {
                    ProgressView()
                    Text("Creating wallets...")
                }
            } else if session.wallets.isEmpty {
                Text("No wallets")
            } else {
                ForEach(session.wallets, id: \.address) { wallet in
                    WalletRow(wallet: wallet)
                }
            }

            Button("Show Profile") {
                session.showUserProfile()
            }

            Button("Logout") {
                Task {
                    await session.logout()
                }
            }
            .foregroundColor(.red)
        }
        .padding()
    }
}

struct WalletRow: View {
    let wallet: BaseWallet

    var body: some View {
        HStack {
            VStack(alignment: .leading) {
                Text(wallet.chain.uppercased())
                    .font(.caption)
                    .foregroundColor(.blue)
                Text(wallet.address)
                    .font(.caption2)
                    .lineLimit(1)
                    .truncationMode(.middle)
            }
            Spacer()
        }
        .padding()
        .background(Color(.systemGray6))
        .cornerRadius(8)
    }
}

4. Navigation Based on Auth State

Handle navigation when authentication state changes:
import SwiftUI
import DynamicSDKSwift
import Combine

struct NavigationRootView: View {
    @State private var isAuthenticated = false
    @State private var cancellables = Set<AnyCancellable>()

    private let sdk = DynamicSDK.instance()

    var body: some View {
        Group {
            if isAuthenticated {
                MainTabView()
            } else {
                LoginView()
            }
        }
        .onAppear {
            setupAuthListener()
        }
    }

    private func setupAuthListener() {
        // Check current state
        isAuthenticated = sdk.auth.authenticatedUser != nil

        // Listen for changes
        sdk.auth.authenticatedUserChanges
            .receive(on: DispatchQueue.main)
            .sink { user in
                withAnimation {
                    isAuthenticated = user != nil
                }
            }
            .store(in: &cancellables)
    }
}

4. Advanced Session Observer

For more complex apps, use a dedicated session observer:
@MainActor
class SessionObserver: ObservableObject {
    @Published var userProfile: UserProfile?
    @Published var connectedWallets: [BaseWallet] = []
    @Published var isWalletConnected = false

    private let sdk = DynamicSDK.instance()
    private var cancellables = Set<AnyCancellable>()

    init() {
        setupObservers()
    }

    private func setupObservers() {
        // Observe user changes
        sdk.auth.authenticatedUserChanges
            .receive(on: DispatchQueue.main)
            .sink { [weak self] user in
                self?.userProfile = user
            }
            .store(in: &cancellables)

        // Observe wallet connections
        sdk.wallets.userWalletsChanges
            .receive(on: DispatchQueue.main)
            .sink { [weak self] wallets in
                self?.connectedWallets = wallets
                self?.isWalletConnected = !wallets.isEmpty
            }
            .store(in: &cancellables)
    }
}

5. Callback-Based Navigation Pattern

An alternative pattern using callbacks:
import SwiftUI
import DynamicSDKSwift
import Combine

struct LoginScreen: View {
    let onNavigateToHome: () -> Void

    @StateObject private var viewModel = LoginViewModel()

    var body: some View {
        VStack {
            // Login UI...
            Button("Sign In") {
                DynamicSDK.instance().ui.showAuth()
            }
        }
        .onAppear {
            viewModel.startListening(onNavigateToHome: onNavigateToHome)
        }
    }
}

@MainActor
class LoginViewModel: ObservableObject {
    private let sdk = DynamicSDK.instance()
    private var cancellables = Set<AnyCancellable>()
    private var onNavigateToHome: (() -> Void)?

    func startListening(onNavigateToHome: @escaping () -> Void) {
        self.onNavigateToHome = onNavigateToHome

        // Already authenticated?
        if sdk.auth.authenticatedUser != nil {
            onNavigateToHome()
            return
        }

        // Listen for auth changes
        sdk.auth.authenticatedUserChanges
            .receive(on: DispatchQueue.main)
            .sink { [weak self] user in
                if user != nil {
                    self?.onNavigateToHome?()
                }
            }
            .store(in: &cancellables)
    }
}

Best Practices

1. Always Use Main Thread for UI Updates

sdk.auth.authenticatedUserChanges
    .receive(on: DispatchQueue.main)  // Important!
    .sink { user in
        // Safe to update UI
    }
    .store(in: &cancellables)

2. Initialization Order

Always initialize the SDK at app launch:
@main
struct YourApp: App {
    init() {
        // Initialize SDK before SwiftUI renders views
        _ = DynamicSDK.initialize(
            props: ClientProps(
                environmentId: "your-env-id",
                appLogoUrl: "https://your-app.com/logo.png",
                appName: "Your App",
                redirectUrl: "yourapp://",
                appOrigin: "https://your-app.com"
            )
        )
    }

    var body: some Scene {
        WindowGroup {
            ContentView()
        }
    }
}

// Then access the SDK instance anywhere:
let sdk = DynamicSDK.instance()

2. Store Cancellables Properly

// In a class (ViewModel)
private var cancellables = Set<AnyCancellable>()

// In a SwiftUI view with @State
@State private var cancellables = Set<AnyCancellable>()

3. Check Initial State

Always check the current state before setting up listeners:
func startListening() {
    // Check current state first
    user = sdk.auth.authenticatedUser
    wallets = sdk.wallets.userWallets

    // Then set up listeners
    sdk.auth.authenticatedUserChanges
        .sink { ... }
        .store(in: &cancellables)
}

4. Handle Wallet Creation Loading State

Wallets are created asynchronously after authentication:
// Show loading state when user is authenticated but wallets haven't appeared yet
if user != nil && wallets.isEmpty {
    isCreatingWallets = true
}

// Clear loading state when wallets appear
sdk.wallets.userWalletsChanges
    .sink { newWallets in
        if !newWallets.isEmpty {
            isCreatingWallets = false
        }
    }
    .store(in: &cancellables)

5. Persistent Session Management (Optional)

For apps that need session persistence:
@MainActor
class PersistentSessionManager: ObservableObject {
    @Published var isAuthenticated = false
    @Published var user: UserProfile?

    private let sdk = DynamicSDK.instance()
    private let userDefaults = UserDefaults.standard
    private let sessionKey = "dynamic_last_login"
    private var cancellables = Set<AnyCancellable>()

    init() {
        setupObservers()
    }

    private func setupObservers() {
        sdk.auth.authenticatedUserChanges
            .receive(on: DispatchQueue.main)
            .sink { [weak self] user in
                self?.isAuthenticated = user != nil
                self?.user = user
                if user != nil {
                    self?.saveLastLogin()
                }
            }
            .store(in: &cancellables)
    }

    func saveLastLogin() {
        userDefaults.set(Date(), forKey: sessionKey)
    }

    func getLastLoginDate() -> Date? {
        return userDefaults.object(forKey: sessionKey) as? Date
    }
}

6. App Lifecycle Management

Handle app lifecycle events properly:
struct AppRootView: View {
    @StateObject private var sessionManager = SessionManager()

    var body: some View {
        ContentView()
            .onReceive(
                NotificationCenter.default.publisher(for: UIApplication.didBecomeActiveNotification)
            ) { _ in
                // SDK automatically refreshes state, no manual refresh needed
            }
    }
}

Troubleshooting

Common Issues

State not updating
  • Ensure you’re calling .receive(on: DispatchQueue.main) before .sink
  • Verify the cancellable is stored in cancellables set
  • Ensure you’re subscribing to the reactive Combine publishers (authenticatedUserChanges, userWalletsChanges, etc.)
  • Verify your view models use @StateObject or @ObservedObject and store cancellables properly
  • Check that the SDK is initialized before accessing DynamicSDK.instance()
Navigation not working after login
  • Make sure you’re subscribed to authenticatedUserChanges before the user authenticates
  • Check that your navigation logic handles the case where user is already authenticated
Wallets not appearing
  • Wallets are created asynchronously after authentication
  • Subscribe to userWalletsChanges to receive updates
  • Check that embedded wallets are enabled in your Dynamic dashboard
  • Use @StateObject for view models that observe SDK state
  • Ensure Combine publishers are properly subscribed and cancellables are stored
  • Make sure to use @MainActor for view models that update UI

Debug Session State

Add logging to understand state changes:
sdk.auth.authenticatedUserChanges
    .receive(on: DispatchQueue.main)
    .sink { user in
        print("Auth state changed: \(user != nil ? "logged in" : "logged out")")
        if let user = user {
            print("User ID: \(user.userId)")
        }
    }
    .store(in: &cancellables)

sdk.wallets.userWalletsChanges
    .receive(on: DispatchQueue.main)
    .sink { wallets in
        print("Wallets updated: \(wallets.count) wallets")
        for wallet in wallets {
            print("  - \(wallet.chain): \(wallet.address)")
        }
    }
    .store(in: &cancellables)

What’s Next

Now that you have session management set up, you can:
  1. Authentication Guide - Implement user authentication flows
  2. Wallet Operations - Work with wallet balances and signing
  3. Networks - Configure blockchain networks