This codelab demonstrates how to implement Session Management flow using the RELID iOS SDK. Session management provides critical security features including automatic session timeout handling, idle session warnings with extension capabilities, and seamless session lifecycle management to prevent unexpected user logouts.
The code to get started is stored in a GitHub repository.
You can clone the repository using the following command:
git clone https://github.com/uniken-public/codelab-ios.git
Navigate to the relid-MFA-session-management folder in the repository you cloned earlier
The sample app provides a complete session management implementation. Let's examine the key components:
Component | Purpose | Sample App Reference |
SessionManager | Global session state management |
|
SessionAlertViewController | UI with countdown and extension |
|
RDNADelegateManager | SDK callback handler |
|
RDNAService | SDK API wrapper |
|
The RELID SDK triggers three main session management events:
Event Type | Description | User Action Required |
Hard session timeout - session already expired | User must acknowledge and app navigates to home | |
Idle session warning - session will expire soon | User can extend session or let it expire | |
Response from session extension API call | Handle success/failure of extension attempt |
The session management flow follows this pattern:
onSessionTimeOutNotification triggers with countdown and extension optionextendSessionIdleTimeout() APIonSessionExtensionResponse provides success/failure resultonSessionTimeout forces app navigation when session expiresExtend your existing RDNADelegateManager to handle session management callbacks. The delegate manager conforms to the SDK's RDNACallbacks protocol and dispatches events via closure properties:
// Sources/Uniken/Services/RDNADelegateManager.swift
class RDNADelegateManager: NSObject, RDNACallbacks {
static let shared = RDNADelegateManager()
// MARK: - Session Management Callback Closures
/// Callback for hard session timeout (session already expired)
var onSessionTimeout: ((_ message: String) -> Void)?
/// Callback for idle session timeout notification (session expiring soon)
var onSessionTimeOutNotification: ((_ userID: String,
_ message: String,
_ timeLeftInSeconds: Int,
_ sessionCanBeExtended: Int,
_ info: RDNAGeneralInfo) -> Void)?
/// Callback for session extension response
var onSessionExtensionResponse: ((_ userID: String,
_ info: RDNAGeneralInfo,
_ status: RDNARequestStatus,
_ error: RDNAError) -> Void)?
// MARK: - RDNACallbacks Protocol Implementation
func onSessionTimeout(_ message: String) {
print("RDNADelegateManager - Hard session timeout: \(message)")
// Dispatch to main thread for UI updates
DispatchQueue.main.async { [weak self] in
self?.onSessionTimeout?(message)
}
}
func onSessionTimeOutNotification(_ userID: String,
message: String,
timeLeftInSecond: Int32,
sessionCanBeExtended: Int32,
info: RDNAGeneralInfo) {
print("RDNADelegateManager - Session timeout notification: \(timeLeftInSecond)s remaining")
// Convert Int32 to Int for Swift convenience
DispatchQueue.main.async { [weak self] in
self?.onSessionTimeOutNotification?(
userID,
message,
Int(timeLeftInSecond),
Int(sessionCanBeExtended),
info
)
}
}
func onSessionExtensionResponse(_ userID: String,
info: RDNAGeneralInfo,
status: RDNARequestStatus,
error: RDNAError) {
let isSuccess = error.longErrorCode == 0 && status.statusCode == 100
print("RDNADelegateManager - Session extension response: \(isSuccess ? "success" : "failure")")
DispatchQueue.main.async { [weak self] in
self?.onSessionExtensionResponse?(userID, info, status, error)
}
}
private override init() {
super.init()
}
}
Key iOS Patterns:
RDNACallbacks protocol defines 51 methods; implement only session-related onesDispatchQueue.main.asyncInt32, converted to Int for Swift convenienceAdd session extension capability to your RDNAService wrapper:
// Sources/Uniken/Services/RDNAService.swift
class RDNAService {
static let shared = RDNAService()
private let rdna = RDNA.sharedInstance()
private init() {}
/**
* Extends the idle session timeout
*
* This method extends the current idle session timeout when the session is eligible for extension.
* Should be called in response to onSessionTimeOutNotification events when sessionCanBeExtended = 1.
* After calling this method, the SDK will trigger an onSessionExtensionResponse event with the result.
*
* @see https://developer.uniken.com/docs/extend-session-timeout
*
* Response Validation Logic:
* 1. Check error.longErrorCode: 0 = success, > 0 = error
* 2. An onSessionExtensionResponse callback will be triggered with detailed response
* 3. The extension success/failure will be determined by the async callback response
*
* @returns RDNAError with synchronous validation result
*/
func extendSessionIdleTimeout() -> RDNAError {
print("RDNAService - Extending session idle timeout")
let error = rdna.extendSessionIdleTimeout()
if error.longErrorCode == 0 {
print("RDNAService - Extension API call successful, waiting for onSessionExtensionResponse")
} else {
print("RDNAService - Extension API call failed: \(error.errorString ?? "")")
}
return error
}
}
When handling session extension, two response layers must be considered:
Response Layer | Purpose | Success Criteria | Failure Handling |
Sync Response | API call validation |
| Immediate error alert |
Async Callback | Extension result |
| Display error message |
Note: The synchronous response only indicates the API call was accepted. The actual extension success/failure is communicated through the onSessionExtensionResponse callback.
Create a singleton SessionManager to handle all session-related state and UI:
// Sources/Uniken/Session/SessionManager.swift
class SessionManager {
static let shared = SessionManager()
// MARK: - State Properties
private weak var presentedAlert: SessionAlertViewController?
private var isProcessingExtension = false
// Operation tracking to prevent duplicate requests
private enum Operation {
case none
case extending
}
private var currentOperation: Operation = .none
private init() {}
// MARK: - Setup
func setupSessionHandlers() {
let delegateManager = RDNADelegateManager.shared
// Register hard timeout handler
delegateManager.onSessionTimeout = { [weak self] message in
self?.handleHardTimeout(message: message)
}
// Register idle timeout notification handler
delegateManager.onSessionTimeOutNotification = { [weak self] userID, message, timeLeft, canExtend, info in
self?.handleIdleTimeoutNotification(
userID: userID,
message: message,
timeLeftInSeconds: timeLeft,
sessionCanBeExtended: canExtend,
info: info
)
}
// Register session extension response handler
delegateManager.onSessionExtensionResponse = { [weak self] userID, info, status, error in
self?.handleSessionExtensionResponse(
userID: userID,
info: info,
status: status,
error: error
)
}
}
// MARK: - Event Handlers
private func handleHardTimeout(message: String) {
print("SessionManager - Hard session timeout")
showTimeoutAlert(
type: .hardTimeout,
message: message,
timeLeftInSeconds: 0,
canExtend: false
)
}
private func handleIdleTimeoutNotification(userID: String,
message: String,
timeLeftInSeconds: Int,
sessionCanBeExtended: Int,
info: RDNAGeneralInfo) {
print("SessionManager - Idle session warning: \(timeLeftInSeconds)s remaining, canExtend: \(sessionCanBeExtended == 1)")
showTimeoutAlert(
type: .idleTimeout,
message: message,
timeLeftInSeconds: timeLeftInSeconds,
canExtend: sessionCanBeExtended == 1
)
}
private func handleSessionExtensionResponse(userID: String,
info: RDNAGeneralInfo,
status: RDNARequestStatus,
error: RDNAError) {
// Only process if we're currently extending
guard currentOperation == .extending else {
print("SessionManager - Extension response received but no extend operation in progress, ignoring")
return
}
let isSuccess = error.longErrorCode == 0 && status.statusCode == 100
if isSuccess {
print("SessionManager - Session extension successful")
dismissAlert()
} else {
print("SessionManager - Session extension failed: \(error.errorString ?? "")")
let errorMessage = error.longErrorCode != 0
? error.errorString ?? "Unknown error"
: status.statusMessage
showErrorAlert(title: "Extension Failed", message: errorMessage)
presentedAlert?.setProcessing(false)
resetState()
}
}
// MARK: - Extension Logic
private func handleExtendSession() {
print("SessionManager - User chose to extend session")
// Prevent duplicate requests
guard currentOperation == .none else {
print("SessionManager - Operation already in progress, ignoring extend request")
return
}
isProcessingExtension = true
currentOperation = .extending
presentedAlert?.setProcessing(true)
// Call SDK extension API
let error = RDNAService.shared.extendSessionIdleTimeout()
if error.longErrorCode != 0 {
// Immediate sync error
showErrorAlert(
title: "Extension Failed",
message: error.errorString ?? "Unknown error"
)
presentedAlert?.setProcessing(false)
resetState()
}
// Success case: wait for async onSessionExtensionResponse callback
}
private func handleDismissAlert(isHardTimeout: Bool) {
print("SessionManager - User dismissed alert")
dismissAlert()
if isHardTimeout {
// Navigate to home screen
AppCoordinator.shared.showTutorialHome()
}
}
// MARK: - UI Presentation
private func showTimeoutAlert(type: SessionAlertViewController.AlertType,
message: String,
timeLeftInSeconds: Int,
canExtend: Bool) {
// Ensure we're on main thread
guard Thread.isMainThread else {
DispatchQueue.main.async {
self.showTimeoutAlert(
type: type,
message: message,
timeLeftInSeconds: timeLeftInSeconds,
canExtend: canExtend
)
}
return
}
// Dismiss existing alert if any
dismissAlert()
// Instantiate from storyboard
guard let alert = UIStoryboard(name: "SessionAlert", bundle: nil)
.instantiateViewController(withIdentifier: "SessionAlertViewController")
as? SessionAlertViewController else {
print("SessionManager - Failed to instantiate SessionAlertViewController")
return
}
// Configure alert
alert.configure(
type: type,
message: message,
timeLeftInSeconds: timeLeftInSeconds,
canExtend: canExtend,
onExtend: { [weak self] in
self?.handleExtendSession()
},
onDismiss: { [weak self] in
self?.handleDismissAlert(isHardTimeout: type == .hardTimeout)
}
)
// Present modally
if let topViewController = getTopViewController() {
alert.modalPresentationStyle = .overFullScreen
alert.modalTransitionStyle = .crossDissolve
topViewController.present(alert, animated: true)
presentedAlert = alert
}
}
private func dismissAlert() {
presentedAlert?.dismiss(animated: true) { [weak self] in
self?.presentedAlert = nil
self?.resetState()
}
}
private func resetState() {
isProcessingExtension = false
currentOperation = .none
}
private func showErrorAlert(title: String, message: String) {
guard let topVC = getTopViewController() else { return }
let alert = UIAlertController(title: title, message: message, preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "OK", style: .default))
topVC.present(alert, animated: true)
}
private func getTopViewController() -> UIViewController? {
guard let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene,
let window = windowScene.windows.first(where: { $0.isKeyWindow }),
let rootVC = window.rootViewController else {
return nil
}
var topVC = rootVC
while let presented = topVC.presentedViewController {
topVC = presented
}
return topVC
}
}
Key Features:
Operation enum[weak self] to prevent cyclesCreate the modal UI for displaying session information and handling user interactions:
// Sources/Uniken/Session/SessionAlertViewController.swift
import UIKit
class SessionAlertViewController: UIViewController {
// MARK: - IBOutlets
@IBOutlet weak var containerView: UIView!
@IBOutlet weak var headerView: UIView!
@IBOutlet weak var titleLabel: UILabel!
@IBOutlet weak var subtitleLabel: UILabel!
@IBOutlet weak var messageLabel: UILabel!
@IBOutlet weak var countdownContainerView: UIView!
@IBOutlet weak var countdownLabel: UILabel!
@IBOutlet weak var extendButton: UIButton!
@IBOutlet weak var dismissButton: UIButton!
@IBOutlet weak var activityIndicator: UIActivityIndicatorView!
// MARK: - Alert Types
enum AlertType {
case hardTimeout // Red, session expired - mandatory
case idleTimeout // Orange, session expiring - optional extend
}
// MARK: - Properties
private var alertType: AlertType = .idleTimeout
private var message: String = ""
private var timeLeftInSeconds: Int = 0
private var canExtend: Bool = false
private var onExtend: (() -> Void)?
private var onDismiss: (() -> Void)?
// Countdown management
private var countdown: Int = 0
private var countdownTimer: Timer?
private var backgroundTime: Date?
// MARK: - Configuration
func configure(type: AlertType,
message: String,
timeLeftInSeconds: Int,
canExtend: Bool,
onExtend: @escaping () -> Void,
onDismiss: @escaping () -> Void) {
self.alertType = type
self.message = message
self.timeLeftInSeconds = timeLeftInSeconds
self.canExtend = canExtend
self.onExtend = onExtend
self.onDismiss = onDismiss
self.countdown = timeLeftInSeconds
// Update UI if view is loaded
loadViewIfNeeded()
updateUIForAlertType()
startCountdown()
}
func setProcessing(_ processing: Bool) {
extendButton.isEnabled = !processing
dismissButton.isEnabled = !processing
if processing {
extendButton.setTitle("Extending...", for: .normal)
activityIndicator.startAnimating()
} else {
extendButton.setTitle("Extend Session", for: .normal)
activityIndicator.stopAnimating()
}
}
// MARK: - Lifecycle
override func viewDidLoad() {
super.viewDidLoad()
setupUI()
updateUIForAlertType()
setupBackgroundHandling()
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
startCountdown()
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
stopCountdown()
}
deinit {
stopCountdown()
NotificationCenter.default.removeObserver(self)
}
// MARK: - UI Setup
private func setupUI() {
// Container styling
containerView.layer.cornerRadius = 16
containerView.layer.shadowColor = UIColor.black.cgColor
containerView.layer.shadowOffset = CGSize(width: 0, height: 4)
containerView.layer.shadowOpacity = 0.3
containerView.layer.shadowRadius = 8
// Header styling
headerView.layer.cornerRadius = 16
headerView.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner]
// Button styling
extendButton.layer.cornerRadius = 8
dismissButton.layer.cornerRadius = 8
dismissButton.layer.borderWidth = 1
dismissButton.layer.borderColor = UIColor.systemGray3.cgColor
// Countdown container
countdownContainerView.layer.cornerRadius = 8
countdownContainerView.backgroundColor = UIColor.systemGray6
}
private func updateUIForAlertType() {
switch alertType {
case .hardTimeout:
// Red theme for hard timeout
headerView.backgroundColor = UIColor(red: 0.9, green: 0.2, blue: 0.2, alpha: 1.0)
titleLabel.text = "🔐 Session Expired"
subtitleLabel.text = "Your session has expired. You will be redirected to the home screen."
countdownContainerView.isHidden = true
extendButton.isHidden = true
dismissButton.setTitle("Close", for: .normal)
case .idleTimeout:
// Orange theme for idle timeout
headerView.backgroundColor = UIColor(red: 1.0, green: 0.6, blue: 0.0, alpha: 1.0)
titleLabel.text = "⚠️ Session Timeout Warning"
if canExtend {
subtitleLabel.text = "Your session will expire soon. You can extend it or let it timeout."
extendButton.isHidden = false
} else {
subtitleLabel.text = "Your session will expire soon."
extendButton.isHidden = true
}
countdownContainerView.isHidden = false
dismissButton.setTitle("Close", for: .normal)
}
messageLabel.text = message
updateCountdownDisplay()
}
// MARK: - Countdown Management
private func startCountdown() {
stopCountdown()
guard countdown > 0 else { return }
countdownTimer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { [weak self] _ in
guard let self = self else { return }
if self.countdown > 0 {
self.countdown -= 1
self.updateCountdownDisplay()
} else {
self.stopCountdown()
}
}
}
private func stopCountdown() {
countdownTimer?.invalidate()
countdownTimer = nil
}
private func updateCountdownDisplay() {
let minutes = countdown / 60
let seconds = countdown % 60
countdownLabel.text = String(format: "%d:%02d", minutes, seconds)
}
// MARK: - Background/Foreground Handling
private func setupBackgroundHandling() {
NotificationCenter.default.addObserver(
self,
selector: #selector(appDidEnterBackground),
name: UIApplication.didEnterBackgroundNotification,
object: nil
)
NotificationCenter.default.addObserver(
self,
selector: #selector(appWillEnterForeground),
name: UIApplication.willEnterForegroundNotification,
object: nil
)
}
@objc private func appDidEnterBackground() {
// Record the time when app goes to background
backgroundTime = Date()
print("SessionAlertViewController - App going to background, recording time")
}
@objc private func appWillEnterForeground() {
// Calculate elapsed time and adjust countdown
guard let backgroundTime = backgroundTime else { return }
let elapsedSeconds = Int(Date().timeIntervalSince(backgroundTime))
print("SessionAlertViewController - App returning to foreground, elapsed: \(elapsedSeconds)s")
// Update countdown based on actual elapsed time
countdown = max(0, countdown - elapsedSeconds)
print("SessionAlertViewController - Countdown updated to: \(countdown)s")
updateCountdownDisplay()
self.backgroundTime = nil
}
// MARK: - Actions
@IBAction func extendButtonTapped(_ sender: UIButton) {
print("SessionAlertViewController - Extend button tapped")
onExtend?()
}
@IBAction func dismissButtonTapped(_ sender: UIButton) {
print("SessionAlertViewController - Dismiss button tapped")
onDismiss?()
}
}
Key iOS Patterns:
Timer.scheduledTimer() with proper cleanup in deinitNotificationCenter for app lifecycle eventsThe following images showcase screens from the sample application:
|
|
Initialize the session management system in your SceneDelegate:
// SceneDelegate.swift
import UIKit
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
var window: UIWindow?
func scene(_ scene: UIScene,
willConnectTo session: UISceneSession,
options connectionOptions: UIScene.ConnectionOptions) {
guard let windowScene = scene as? UIWindowScene else { return }
// Create window and navigation controller
let window = UIWindow(windowScene: windowScene)
let navigationController = UINavigationController()
window.rootViewController = navigationController
self.window = window
window.makeKeyAndVisible()
// Configure app coordinator with navigation
AppCoordinator.shared.configure(with: navigationController)
// Setup session management handlers (SESSION-SPECIFIC)
SessionManager.shared.setupSessionHandlers()
// Start app flow
AppCoordinator.shared.start()
}
}
The AppCoordinator handles navigation based on SDK callbacks:
// Sources/Tutorial/Navigation/AppCoordinator.swift
class AppCoordinator {
static let shared = AppCoordinator()
private weak var navigationController: UINavigationController?
func configure(with navigationController: UINavigationController) {
self.navigationController = navigationController
setupGlobalCallbackNavigation()
}
private func setupGlobalCallbackNavigation() {
let delegateManager = RDNADelegateManager.shared
// MFA flow callbacks
delegateManager.onGetUser = { [weak self] userNames, recentUser, response, error in
self?.showCheckUser(userNames: userNames,
recentlyLoggedInUser: recentUser,
response: response,
error: error)
}
delegateManager.onUserLoggedIn = { [weak self] userID, response, error in
self?.showDashboard(userID: userID, response: response, error: error)
}
}
func showTutorialHome() {
guard let vc = storyboard.instantiateViewController(
withIdentifier: "TutorialHomeViewController"
) as? TutorialHomeViewController else { return }
navigationController?.setViewControllers([vc], animated: true)
}
}
This architecture provides several advantages:
Session Type | Test Case | Expected Behavior | Validation Points |
Hard Timeout | Session expires | Modal with Close button only | Navigate to home screen |
Idle Warning | Session expiring soon | Modal with countdown and Extend button | Extension API call works |
Extension Success | Extend session API succeeds | Modal dismisses, session continues | No navigation occurs |
Extension Failure | Extend session API fails | Error alert, modal remains | User can retry or close |
Background Timer | App goes to background during countdown | Timer accurately reflects elapsed time | Countdown resumes correctly |
Critical test for production reliability:
// Test background/foreground timer accuracy
func testBackgroundTimer() {
// 1. Trigger idle session timeout notification
// 2. Note the countdown time (e.g., 60 seconds)
// 3. Background the app for 30 seconds (press Home button)
// 4. Foreground the app (tap app icon)
// 5. Verify countdown shows ~30 seconds remaining
print("Testing background timer accuracy")
print("Initial countdown: \(initialCountdown)")
print("Expected remaining after 30s background: \(initialCountdown - 30)")
}
Use these debugging techniques to verify session functionality:
// Verify callback registration in SessionManager.setupSessionHandlers()
print("Session callbacks registered:")
print("- Hard timeout handler: \(RDNADelegateManager.shared.onSessionTimeout != nil)")
print("- Idle notification handler: \(RDNADelegateManager.shared.onSessionTimeOutNotification != nil)")
print("- Extension response handler: \(RDNADelegateManager.shared.onSessionExtensionResponse != nil)")
// Log session event data
print("Session timeout notification received:")
print("- User ID: \(userID)")
print("- Time left: \(timeLeftInSeconds)s")
print("- Can extend: \(sessionCanBeExtended == 1)")
print("- Message: \(message)")
Build and Run:
# Command line build
xcodebuild -workspace YourApp.xcworkspace -scheme YourApp -sdk iphonesimulator
# Or press Cmd+R in Xcode
Debugging Tools:
onSessionTimeOutNotification and onSessionExtensionResponsepo RDNADelegateManager.shared.onSessionTimeout to inspect closuresCause: Session callbacks not properly registered Solution: Verify SessionManager.shared.setupSessionHandlers() is called in SceneDelegate
// In SceneDelegate.swift
func scene(_ scene: UIScene, willConnectTo session: UISceneSession,
options connectionOptions: UIScene.ConnectionOptions) {
// ... window setup ...
SessionManager.shared.setupSessionHandlers() // CRITICAL
}
Cause: Closures not set on RDNADelegateManager Solution: Check that closure properties are assigned in setupSessionHandlers()
Cause: Countdown doesn't account for background time Solution: Implement NotificationCenter observers for app lifecycle
private func setupBackgroundHandling() {
NotificationCenter.default.addObserver(
self,
selector: #selector(appDidEnterBackground),
name: UIApplication.didEnterBackgroundNotification,
object: nil
)
NotificationCenter.default.addObserver(
self,
selector: #selector(appWillEnterForeground),
name: UIApplication.willEnterForegroundNotification,
object: nil
)
}
@objc private func appDidEnterBackground() {
backgroundTime = Date()
}
@objc private func appWillEnterForeground() {
guard let backgroundTime = backgroundTime else { return }
let elapsedSeconds = Int(Date().timeIntervalSince(backgroundTime))
countdown = max(0, countdown - elapsedSeconds)
updateCountdownDisplay()
self.backgroundTime = nil
}
Cause: Calling extension API when sessionCanBeExtended is false Solution: Check extension eligibility before API call
private func handleExtendSession() {
guard canExtend else {
showErrorAlert(title: "Extension Not Available",
message: "This session cannot be extended.")
return
}
let error = RDNAService.shared.extendSessionIdleTimeout()
// ... handle error
}
Cause: Multiple concurrent extension requests Solution: Use operation tracking to prevent duplicates
private enum Operation {
case none
case extending
}
private var currentOperation: Operation = .none
private func handleExtendSession() {
guard currentOperation == .none else {
print("Extension already in progress")
return
}
currentOperation = .extending
// ... perform extension
}
Cause: Weak reference to alert view controller becomes nil Solution: Store strong reference during presentation, clear after dismissal
private weak var presentedAlert: SessionAlertViewController? // Weak to avoid cycles
private func showTimeoutAlert(...) {
// ... configure alert ...
topViewController.present(alert, animated: true)
presentedAlert = alert // Store weak reference
}
private func dismissAlert() {
presentedAlert?.dismiss(animated: true) { [weak self] in
self?.presentedAlert = nil // Clear reference
self?.resetState()
}
}
Cause: Retain cycles from closures Solution: Always use [weak self] in closure captures
// CORRECT
delegateManager.onSessionTimeout = { [weak self] message in
self?.handleHardTimeout(message: message)
}
// WRONG - creates retain cycle
delegateManager.onSessionTimeout = { message in
self.handleHardTimeout(message: message)
}
Best Practice: Test session management behavior on both physical devices and simulators with different timeout scenarios. Use Instruments' Leaks tool to verify no memory leaks.
os_logExtension Scenario | Recommended Action | Implementation |
Frequent Extensions | Set reasonable limits | Track extension count per session in SessionManager |
Critical Operations | Allow extensions during important tasks | Context-aware extension logic based on current screen |
Inactive Sessions | Enforce timeouts | Don't extend completely idle sessions |
deinitdeinit {
// CRITICAL: Clean up observers
NotificationCenter.default.removeObserver(self)
// CRITICAL: Invalidate timers
stopCountdown()
// CRITICAL: Clear closures
onExtend = nil
onDismiss = nil
print("SessionAlertViewController deallocated")
}
os_log with appropriate levels)import os.log
private let logger = Logger(subsystem: "com.yourapp.session", category: "SessionManager")
func handleSessionEvent() {
// Use appropriate log levels
logger.info("Session event received") // Info only
logger.debug("Session details: \(privateData, privacy: .private)") // Redacted in release
}
Congratulations! You've successfully learned how to implement comprehensive session management functionality with:
os_log