This codelab demonstrates how to implement the RELID Initialization flow using the RELID iOS SDK framework. The RELID SDK provides secure identity verification and session management for mobile applications.

What You'll Learn

What You'll Need

Get the Code from GitHub

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-initialize folder in the repository you cloned earlier

Before implementing your own RELID initialization, let's examine the sample app structure to understand the recommended architecture:

Component

Purpose

Sample App Reference

Connection Profile

Configuration data

Sources/Uniken/CP/agent_info.json

Profile Parser

Utility to parse connection data

Sources/Uniken/Utils/ConnectionProfileParser.swift

Delegate Manager

Implements SDK callbacks protocol

Sources/Uniken/Services/RDNADelegateManager.swift

RELID Service

Main SDK interface

Sources/Uniken/Services/RDNAService.swift

App Coordinator

Navigation management

Sources/Tutorial/Navigation/AppCoordinator.swift

View Controllers

User interface screens

Sources/Tutorial/Screens/TutorialHomeViewController.swift

Recommended Directory Structure

Create the following directory structure in your iOS project:

YourApp/
├── AppDelegate.swift              # App lifecycle
├── SceneDelegate.swift            # Scene lifecycle (iOS 13+)
├── Sources/
│   ├── Uniken/
│   │   ├── CP/
│   │   │   └── agent_info.json
│   │   ├── Services/
│   │   │   ├── RDNAService.swift
│   │   │   └── RDNADelegateManager.swift
│   │   └── Utils/
│   │       ├── ConnectionProfileParser.swift
│   │       └── ProgressHelper.swift
│   └── Tutorial/
│       ├── Navigation/
│       │   └── AppCoordinator.swift
│       └── Screens/
│           ├── TutorialHomeViewController.swift
│           ├── TutorialSuccessViewController.swift
│           └── TutorialErrorViewController.swift
├── Base.lproj/
│   └── Main.storyboard            # UI interface
├── Assets.xcassets/               # Images and colors
└── sdk/
    └── RELID.xcframework/         # SDK framework

Prerequisites

Ensure you have Xcode installed:

xcode-select --version

You need Xcode 13.0 or later for Swift 5.5+ support and iOS 13.0+ deployment.

Add SDK Framework

The RELID iOS SDK is distributed as an XCFramework that supports both physical devices and simulators.

Option 1: Manual Framework Integration (Used in Sample App)

  1. Add the RELID.xcframework to your project:
    • Drag and drop RELID.xcframework into your Xcode project
    • Select your target → General → Frameworks, Libraries, and Embedded Content
    • Set embed mode to "Embed & Sign"
  2. Verify the framework is linked:
    • Build Phases → Link Binary With Libraries should include RELID.xcframework
    • Build Phases → Embed Frameworks should include RELID.xcframework

Ensure pod installed:

pod install

agent_info.json

Create your connection profile JSON file in Sources/Uniken/CP/agent_info.json:

{
  "RelIds": [
    {
      "Name": "YourRELIDAgentName",
      "RelId": "your-rel-id-string-here"
    }
  ],
  "Profiles": [
    {
      "Name": "YourRELIDAgentName",
      "Host": "your-gateway-host.com",
      "Port": "443"
    }
  ]
}

Security Considerations

Create a parser to load and validate the connection profile from your app bundle:

// Sources/Uniken/Utils/ConnectionProfileParser.swift
import Foundation

// MARK: - Data Structures

struct ConnectionProfile {
    let relId: String
    let host: String
    let port: String  // Keep as String for SDK compatibility
}

// MARK: - JSON Structures (match agent_info.json)

private struct RelId: Decodable {
    let Name: String
    let RelId: String
}

private struct Profile: Decodable {
    let Name: String
    let Host: String
    let Port: String
}

private struct AgentInfo: Decodable {
    let RelIds: [RelId]
    let Profiles: [Profile]
}

// MARK: - Connection Profile Parser

class ConnectionProfileParser {

    /// Load and parse agent_info.json from bundle
    /// - Returns: ConnectionProfile with relId, host, and port
    /// - Throws: Error if parsing fails
    static func loadFromJSON() throws -> ConnectionProfile {

        // Locate agent_info.json in bundle
        guard let url = Bundle.main.url(
            forResource: "agent_info",
            withExtension: "json",
            subdirectory: ""
        ) else {
            throw ParsingError.fileNotFound("agent_info.json not found in bundle")
        }

        // Load JSON data
        let data = try Data(contentsOf: url)

        // Decode JSON
        let decoder = JSONDecoder()
        let agentInfo = try decoder.decode(AgentInfo.self, from: data)

        // Validate structure
        guard !agentInfo.RelIds.isEmpty else {
            throw ParsingError.invalidData("No RelIds found in agent info")
        }

        guard !agentInfo.Profiles.isEmpty else {
            throw ParsingError.invalidData("No Profiles found in agent info")
        }

        // Always pick the first array objects
        let firstRelId = agentInfo.RelIds[0]

        // Find matching profile by Name (1-1 mapping)
        guard let matchingProfile = agentInfo.Profiles.first(where: { $0.Name == firstRelId.Name }) else {
            throw ParsingError.invalidData("No matching profile found for RelId name: \(firstRelId.Name)")
        }

        // Validate required fields
        if firstRelId.RelId.isEmpty {
            throw ParsingError.invalidData("Invalid RelId object - missing RelId")
        }

        if matchingProfile.Host.isEmpty || matchingProfile.Port.isEmpty {
            throw ParsingError.invalidData("Invalid Profile object - missing Host or Port")
        }

        // Return parsed profile
        return ConnectionProfile(
            relId: firstRelId.RelId,
            host: matchingProfile.Host,
            port: matchingProfile.Port
        )
    }

    // MARK: - Error Types

    enum ParsingError: Error, LocalizedError {
        case fileNotFound(String)
        case invalidData(String)

        var errorDescription: String? {
            switch self {
            case .fileNotFound(let message):
                return "File not found: \(message)"
            case .invalidData(let message):
                return "Invalid data: \(message)"
            }
        }
    }
}

Implementation Details:

The delegate manager implements the SDK's callback protocol using a singleton pattern with closure-based dispatching:

// Sources/Uniken/Services/RDNADelegateManager.swift
import Foundation
import RELID

/// Singleton manager that implements the RDNACallbacks protocol
/// Handles all SDK callbacks with closure-based callback dispatching
class RDNADelegateManager: NSObject, RDNACallbacks {

    // MARK: - Singleton

    static let shared = RDNADelegateManager()

    private override init() {
        super.init()
    }

    // MARK: - Initialize Callback Closures

    /// Closure invoked during SDK initialization progress updates
    var onInitializeProgress: ((RDNAInitProgressStatus) -> Void)?

    /// Closure invoked when SDK initialization encounters an error
    var onInitializeError: ((RDNAError) -> Void)?

    /// Closure invoked when SDK initialization completes successfully
    var onInitialized: ((RDNAChallengeResponse) -> Void)?

    // MARK: - RDNACallbacks Protocol Implementation

    func onInitializeError(_ error: RDNAError) {
        DispatchQueue.main.async { [weak self] in
            self?.onInitializeError?(error)
        }
    }

    func onInitialized(_ response: RDNAChallengeResponse) {
        DispatchQueue.main.async { [weak self] in
            self?.onInitialized?(response)
        }
    }

    func onInitializeProgress(_ state: RDNAInitProgressStatus) {
        DispatchQueue.main.async { [weak self] in
            self?.onInitializeProgress?(state)
        }
    }

    // MARK: Other Protocol Methods (Stubs for future codelabs)

    func getUser(_ userNames: [String], recentlyLoggedInUser: String, response: RDNAChallengeResponse, error: RDNAError) {
       // Not used in this codelab. This may be used in a future one.
    }

    func getPassword(_ userID: String, challenge mode: RDNAChallengeOpMode, attemptsLeft: Int32, response: RDNAChallengeResponse, error: RDNAError) {
       // Not used in this codelab. This may be used in a future one.
    }

    // ... (48 more stub methods - see sample app for complete implementation)
}

Event Handling in iOS:

Key callback methods:

The RELID service provides the main interface for SDK operations:

// Sources/Uniken/Services/RDNAService.swift
import Foundation
import RELID

/// RDNAService - Simple wrapper for RELID SDK
/// Focus: SDK integration teaching
class RDNAService {

    // MARK: - Singleton

    static let shared = RDNAService()

    private let rdna = RDNA.sharedInstance()

    private init() {}

    // MARK: - SDK Methods

    /// Get SDK Version
    /// - Returns: SDK version string (e.g., "25.06.03")
    func getSDKVersion() -> String {
        return RDNA.getSDKVersion()
    }

    /// Initialize SDK (on background thread)
    /// Loads connection profile automatically from agent_info.json
    /// - Parameters:
    ///   - cipherSpec: Cipher specification (default: "")
    ///   - cipherSalt: Cipher salt (default: "")
    ///   - loggingLevel: SDK logging level (default: .NO_LOGS)
    ///   - completion: Completion handler called on main thread with RDNAError
    func initialize(
        cipherSpec: String = "",
        cipherSalt: String = "",
        loggingLevel: RDNALoggingLevel = .NO_LOGS,
        completion: @escaping (RDNAError) -> Void
    ) {
        // Run SDK initialization on background thread to avoid blocking UI
        DispatchQueue.global(qos: .userInitiated).async { [weak self] in
            guard let self = self else { return }

            // Load connection profile from agent_info.json
            let profile: ConnectionProfile
            do {
                profile = try ConnectionProfileParser.loadFromJSON()
                print("RDNAService - Connection profile loaded successfully")
                print("RelId: \(profile.relId)")
                print("Host: \(profile.host)")
                print("Port: \(profile.port)")
            } catch {
                // Connection profile parsing failed
                let rdnaError = RDNAError()
                rdnaError.longErrorCode = -1
                rdnaError.errorCode = .ERR_INVALID_AGENT_INFO
                rdnaError.errorString = "Failed to load connection profile: \(error.localizedDescription)"

                // Return error on main thread
                DispatchQueue.main.async {
                    completion(rdnaError)
                }
                return
            }

            // Port conversion: String → UInt16
            guard let portNumber = UInt16(profile.port) else {
                let rdnaError = RDNAError()
                rdnaError.longErrorCode = -1
                rdnaError.errorCode = .ERR_INVALID_PORTNUM
                rdnaError.errorString = "Invalid port number: \(profile.port)"

                // Return error on main thread
                DispatchQueue.main.async {
                    completion(rdnaError)
                }
                return
            }

            // Call SDK initialize method
            let error = self.rdna.initialize(
                profile.relId,
                callbacks: RDNADelegateManager.shared,
                gatewayHost: profile.host,
                gatewayPort: portNumber,
                cipherSpec: cipherSpec,
                cipherSalt: cipherSalt,
                proxySettings: nil,
                rdnasslCertificate: nil,
                dnsServerList: nil,
                rdnaLoggingLevel: loggingLevel,
                appContext: self
            )

            // Return result on main thread
            DispatchQueue.main.async {
                completion(error)
            }
        }
    }
}

Important Implementation Details

Background Thread Execution:

The

initialize()

call requires specific parameters:

Parameter

Purpose

Example

agentInfo

RelId

From connection profile Ex. "your-rel-id-string-here"

callbacks

Callback protocol

RDNADelegateManager.shared

gatewayHost

Server hostname

From connection profile Ex. "your-gateway-host.com"

gatewayPort

Server port

From connection profile Ex. 443 (as UInt16)

loggingLevel

Logging setting

.NO_LOGS for production

How iOS SDKs Work

iOS SDKs are distributed as frameworks that link with your application.

SDK Structure:

  1. Framework: Pre-compiled code packaged as .framework or .xcframework
  2. Public API: Swift/Objective-C interfaces you use in your code
  3. Native Implementation: Compiled binary code (Swift/Objective-C/C++)

Using the RELID iOS SDK

import RELID

// Get SDK instance (singleton)
let rdna = RDNA.sharedInstance()

// Configure via service wrapper
let rdnaService = RDNAService.shared

// Initialize SDK
rdnaService.initialize { error in
    if error.longErrorCode != 0 {
        print("Initialization failed: \(error.errorString)")
    } else {
        print("SDK initialized successfully")
    }
}

iOS Initialization Flow

User Taps Initialize Button
    ↓
RDNAService.initialize() [Main Thread]
    ↓
DispatchQueue.global() [Background Thread]
    ↓
ConnectionProfileParser.loadFromJSON()
    ↓
RDNA.sharedInstance().initialize() [SDK Call]
    ↓
RDNADelegateManager callbacks [Protocol Methods]
    ├─ onInitializeProgress → UI updates
    ├─ onInitializeError → Error screen
    └─ onInitialized → Success screen
    ↓
DispatchQueue.main [Main Thread]
    ↓
UI Updates & Navigation

Create a view controller that handles user interaction and displays progress:

// Sources/Tutorial/Screens/TutorialHomeViewController.swift
import UIKit
import RELID

/// TutorialHomeViewController - Main screen for SDK initialization
class TutorialHomeViewController: UIViewController {

    // MARK: - IBOutlets

    @IBOutlet weak var sdkVersionLabel: UILabel!
    @IBOutlet weak var initializeButton: UIButton!
    @IBOutlet weak var activityIndicator: UIActivityIndicatorView!
    @IBOutlet weak var progressContainerView: UIView!
    @IBOutlet weak var progressLabel: UILabel!

    // MARK: - Properties (View Controller State)

    private var sdkVersion: String = "Loading..." {
        didSet {
            sdkVersionLabel?.text = sdkVersion
        }
    }

    private var isInitializing: Bool = false {
        didSet {
            updateUI()
        }
    }

    private var progressMessage: String = "" {
        didSet {
            progressLabel?.text = progressMessage.isEmpty ? "RDNA initialization in progress..." : progressMessage
        }
    }

    // MARK: - Lifecycle

    override func viewDidLoad() {
        super.viewDidLoad()
        setupUI()
        loadSDKVersion()
        setupCallbackHandlers()
    }

    // MARK: - Setup

    private func setupUI() {
        view.backgroundColor = UIColor(hex: "#f8fafc")

        // Progress container initially hidden
        progressContainerView?.isHidden = true
        progressContainerView?.backgroundColor = UIColor(hex: "#eff6ff")
        progressContainerView?.layer.cornerRadius = 8
        progressContainerView?.layer.borderWidth = 4
        progressContainerView?.layer.borderColor = UIColor(hex: "#2563eb")?.cgColor

        // Initialize button styling
        initializeButton?.backgroundColor = UIColor(hex: "#16a34a")
        initializeButton?.layer.cornerRadius = 8

        // Activity indicator
        activityIndicator?.hidesWhenStopped = true
    }

    private func setupCallbackHandlers() {
        // onInitializeProgress
        RDNADelegateManager.shared.onInitializeProgress = { [weak self] status in
            let message = ProgressHelper.getProgressMessage(from: status)
            self?.progressMessage = message
        }

        // onInitializeError
        RDNADelegateManager.shared.onInitializeError = { [weak self] error in
            print("TutorialHomeViewController - Received initialize error: \(error.errorString)")

            // Update UI
            self?.isInitializing = false
            self?.progressMessage = ""

            // Navigate to error screen
            self?.navigateToErrorScreen(
                shortErrorCode: Int(error.errorCode.rawValue),
                longErrorCode: Int(error.longErrorCode),
                errorString: error.errorString
            )
        }
    }

    private func loadSDKVersion() {
        sdkVersion = RDNAService.shared.getSDKVersion()
    }

    private func updateUI() {
        // Update button and progress visibility based on isInitializing
        if isInitializing {
            initializeButton?.isEnabled = false
            initializeButton?.backgroundColor = UIColor(hex: "#9ca3af")
            activityIndicator?.startAnimating()
            progressContainerView?.isHidden = false
        } else {
            initializeButton?.isEnabled = true
            initializeButton?.backgroundColor = UIColor(hex: "#16a34a")
            activityIndicator?.stopAnimating()
            progressContainerView?.isHidden = true
        }
    }

    // MARK: - Actions

    @IBAction func initializeButtonTapped(_ sender: UIButton) {
        handleInitializePress()
    }

    // MARK: - Business Logic

    private func handleInitializePress() {
        if isInitializing { return }

        isInitializing = true
        progressMessage = "Starting RDNA initialization..."

        print("TutorialHomeViewController - User clicked Initialize - Starting RDNA...")

        // Call RDNAService.initialize()
        // Connection profile is loaded automatically inside RDNAService
        // Completion handler is called on main thread automatically
        RDNAService.shared.initialize() { [weak self] error in
            // Check error response
            if error.longErrorCode != 0 {
                print("TutorialHomeViewController - RDNA initialization failed")
                print("Error: \(error.errorString), Long: \(error.longErrorCode), Short: \(error.errorCode.rawValue)")

                self?.isInitializing = false
                self?.progressMessage = ""

                // Show alert for specific error codes
                self?.showAlert(
                    title: "Initialization Failed",
                    message: "\(error.errorString)\n\nError Codes:\nLong: \(error.longErrorCode)\nShort: \(error.errorCode.rawValue)"
                )
            } else {
                print("TutorialHomeViewController - RDNA initialization started successfully")
                // Success callback will be handled via RDNADelegateManager.onInitialized
            }
        }
    }

    // MARK: - Navigation

    private func navigateToErrorScreen(shortErrorCode: Int, longErrorCode: Int, errorString: String) {
        AppCoordinator.shared.showTutorialError(
            shortErrorCode: shortErrorCode,
            longErrorCode: longErrorCode,
            errorString: errorString
        )
    }

    // MARK: - Helper Methods

    private func showAlert(title: String, message: String) {
        let alert = UIAlertController(title: title, message: message, preferredStyle: .alert)
        alert.addAction(UIAlertAction(title: "OK", style: .default))
        present(alert, animated: true)
    }

    // MARK: - Deinit (cleanup)

    deinit {
        // Cleanup event handlers
        RDNADelegateManager.shared.onInitializeProgress = nil
        RDNADelegateManager.shared.onInitializeError = nil
    }
}

// MARK: - UIColor Extension (for hex colors)

extension UIColor {
    convenience init?(hex: String) {
        var hexSanitized = hex.trimmingCharacters(in: .whitespacesAndNewlines)
        hexSanitized = hexSanitized.replacingOccurrences(of: "#", with: "")

        var rgb: UInt64 = 0
        guard Scanner(string: hexSanitized).scanHexInt64(&rgb) else { return nil }

        let r = CGFloat((rgb & 0xFF0000) >> 16) / 255.0
        let g = CGFloat((rgb & 0x00FF00) >> 8) / 255.0
        let b = CGFloat(rgb & 0x0000FF) / 255.0

        self.init(red: r, green: g, blue: b, alpha: 1.0)
    }
}

Key Implementation Features

iOS UIKit Architecture:

iOS applications use a view controller-based architecture:

Completion Handler Pattern:

Error Handling:

Progress Tracking:

Callback-Driven Architecture:

The following images showcase screens from the sample application:

Initialize Progress Screen

Initialize Error Screen

Initialize Screen

Build and Run

Using Xcode:

  1. Open .xcworkspace (if using CocoaPods) or .xcodeproj
  2. Select target device or simulator
  3. Press Cmd+R to build and run

Using command line:

# Build
xcodebuild -workspace YourApp.xcworkspace -scheme YourApp -sdk iphonesimulator

# Run on simulator
xcodebuild -workspace YourApp.xcworkspace -scheme YourApp -sdk iphonesimulator -destination 'platform=iOS Simulator,name=iPhone 14'

Debugging

Xcode Debugger: Use breakpoints, LLDB, and View Debugger

Console Output: Use print() or os_log for logging

Instruments: Profile memory, CPU, and network usage

Key Test Scenarios

  1. Successful Initialization: Verify the complete flow works end-to-end
  2. Network Errors: Test with invalid host/port configurations
  3. Invalid Credentials: Test with incorrect RelId values
  4. Progress Tracking: Verify progress events are properly displayed

Verification Steps

Test your implementation with these APIs:

// Test SDK version retrieval
let version = RDNAService.shared.getSDKVersion()
print("SDK Version: \(version)")

// Test initialization
RDNAService.shared.initialize { error in
    if error.longErrorCode != 0 {
        print("Initialization failed: \(error.errorString)")
    } else {
        print("Initialization successful")
    }
}

The sample app includes comprehensive testing patterns in the tutorial screens:

Connection Profile Issues

Error: "No RelIds found in agent info" Solution: Verify your JSON structure matches the sample in Sources/Uniken/CP/agent_info.json

Error: "No matching profile found for RelId name" Solution: Ensure the Name field in RelIds matches the Name field in Profiles

Network Connectivity

Error: Network connection failures Solution: Check host/port values and network accessibility

Error: REL-ID connection issues Solution: Verify the REL-ID server setup and running

SDK Initialization

Error Code 88: SDK already initialized Solution: Terminate the SDK before re-initializing

Error Code 288: SDK detected dynamic attack performed on the app Solution: Terminate the app

Error Code 179: Initialization in progress Solution: Wait for current initialization to complete before retrying

iOS-Specific Issues

"No such module ‘RELID'"

"Resource not found in bundle"

"dyld: Library not loaded"

Code signing errors

Security Considerations

Memory Management

// Good: Weak self in closure
RDNADelegateManager.shared.onInitializeProgress = { [weak self] status in
    self?.progressMessage = ProgressHelper.getProgressMessage(from: status)
}

// Good: Cleanup in deinit
deinit {
    RDNADelegateManager.shared.onInitializeProgress = nil
    RDNADelegateManager.shared.onInitializeError = nil
}

Threading

// Good: Background thread for SDK operations
DispatchQueue.global(qos: .userInitiated).async {
    let error = rdna.initialize(...)

    // Return to main thread for UI updates
    DispatchQueue.main.async {
        completion(error)
    }
}

Performance

App Lifecycle

Congratulations! You've successfully implemented RELID SDK initialization in iOS with:

Key iOS Patterns Learned

Next Steps