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.
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 |
|
Profile Parser | Utility to parse connection data |
|
Delegate Manager | Implements SDK callbacks protocol |
|
RELID Service | Main SDK interface |
|
App Coordinator | Navigation management |
|
View Controllers | User interface screens |
|
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
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.
The RELID iOS SDK is distributed as an XCFramework that supports both physical devices and simulators.
RELID.xcframework to your project:RELID.xcframework into your Xcode projectEnsure pod installed:
pod install
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"
}
]
}
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:
Bundle.main for accessing app bundle resourcesJSONDecoder for parsing JSON contentDecodable protocol for type safetyThe 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:
RDNACallbacks protocolweak self to avoid retain cyclesKey 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)
}
}
}
}
Background Thread Execution:
DispatchQueue.global(qos: .userInitiated) provides appropriate priorityThe
initialize()
call requires specific parameters:
Parameter | Purpose | Example |
| RelId | From connection profile Ex. |
| Callback protocol |
|
| Server hostname | From connection profile Ex. |
| Server port | From connection profile Ex. |
| Logging setting |
|
iOS SDKs are distributed as frameworks that link with your application.
SDK Structure:
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")
}
}
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)
}
}
iOS UIKit Architecture:
iOS applications use a view controller-based architecture:
viewDidLoad(), viewWillAppear(), viewDidDisappear()didSet observersUINavigationController or coordinator patternCompletion Handler Pattern:
Error Handling:
error.longErrorCode to determine success/failureUIAlertControllerProgress Tracking:
Callback-Driven Architecture:
The following images showcase screens from the sample application:
|
|
|
Using Xcode:
.xcworkspace (if using CocoaPods) or .xcodeprojUsing 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'
Xcode Debugger: Use breakpoints, LLDB, and View Debugger
Console Output: Use print() or os_log for logging
Instruments: Profile memory, CPU, and network usage
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:
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
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
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
"No such module ‘RELID'"
"Resource not found in bundle"
"dyld: Library not loaded"
Code signing errors
RDNALoggingLevel.NO_LOGS in productionrelId.prefix(10) + "...")weak self in closures to avoid retain cyclesdeinit// 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
}
DispatchQueue.main.async { }// 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)
}
}
applicationDidEnterBackgroundapplicationWillEnterForegroundCongratulations! You've successfully implemented RELID SDK initialization in iOS with:
Bundle.mainviewDidLoad, deinit)