Documentation Index
Fetch the complete documentation index at: https://docs.dynamic.xyz/docs/llms.txt
Use this file to discover all available pages before exploring further.
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:
- Authentication Guide - Implement user authentication flows
- Wallet Operations - Work with wallet balances and signing
- Networks - Configure blockchain networks