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 SwiftUI
import DynamicSDKSwift
import Combine

@MainActor
class SessionViewModel: ObservableObject {
    @Published var user: UserProfile?
    @Published var isAuthenticated = false
    
    private let sdk = DynamicSDK.instance()
    private var cancellables = Set<AnyCancellable>()
    
    func startListening() {
        // Set initial state
        user = sdk.auth.authenticatedUser
        isAuthenticated = user != nil
        
        // Listen for authentication changes
        sdk.auth.authenticatedUserChanges
            .receive(on: DispatchQueue.main)
            .sink { [weak self] newUser in
                self?.user = newUser
                self?.isAuthenticated = newUser != nil
            }
            .store(in: &cancellables)
    }
}

struct ContentView: View {
    @StateObject private var session = SessionViewModel()
    
    var body: some View {
        Group {
            if session.isAuthenticated {
                HomeView()
            } else {
                LoginView()
            }
        }
        .onAppear {
            session.startListening()
        }
    }
}

2. Complete Session Manager

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

@MainActor
class SessionManager: ObservableObject {
    @Published var user: UserProfile?
    @Published var token: String?
    @Published var wallets: [BaseWallet] = []
    @Published var isAuthenticated = false
    @Published var isCreatingWallets = false
    
    private let sdk = DynamicSDK.instance()
    private var cancellables = Set<AnyCancellable>()
    
    func startListening() {
        // Initialize with current state
        user = sdk.auth.authenticatedUser
        token = sdk.auth.token
        wallets = sdk.wallets.userWallets
        isAuthenticated = user != nil
        
        // Check if wallets are being created
        if user != nil && wallets.isEmpty {
            isCreatingWallets = true
        }
        
        // Listen for user changes
        sdk.auth.authenticatedUserChanges
            .receive(on: DispatchQueue.main)
            .sink { [weak self] newUser in
                guard let self else { return }
                self.user = newUser
                self.isAuthenticated = newUser != nil
                
                if newUser == 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)
        
        // Listen for token changes
        sdk.auth.tokenChanges
            .receive(on: DispatchQueue.main)
            .sink { [weak self] newToken in
                self?.token = newToken
            }
            .store(in: &cancellables)
        
        // Listen for wallet changes
        sdk.wallets.userWalletsChanges
            .receive(on: DispatchQueue.main)
            .sink { [weak self] newWallets in
                guard let self else { return }
                self.wallets = newWallets
                
                // Wallets appeared, stop showing loading
                if !newWallets.isEmpty {
                    self.isCreatingWallets = false
                }
            }
            .store(in: &cancellables)
    }
    
    func logout() async {
        do {
            try await sdk.auth.logout()
            // State updates handled by authenticatedUserChanges
        } catch {
            print("Logout failed: \(error)")
        }
    }
    
    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()
                }
            }
        }
        .onAppear {
            session.startListening()
        }
    }
}

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)
    }
}

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. 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)

Troubleshooting

Common Issues

State not updating
  • Ensure you’re calling .receive(on: DispatchQueue.main) before .sink
  • Verify the cancellable is stored in cancellables set
  • Check that startListening() is called in onAppear
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

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