Skip to content

iOS SDK ​

Native iOS SDK for screen recording and feedback collection using ReplayKit.

Requirements ​

  • iOS 14.0 or higher
  • Swift 5.9+
  • Xcode 15.0 or later

Installation ​

Add the package to your Package.swift:

swift
dependencies: [
    .package(url: "https://github.com/tryhorus/horus-ios-sdk.git", from: "1.0.0")
]

Or in Xcode:

  1. Go to File → Add Packages...
  2. Enter the repository URL: https://github.com/tryhorus/horus-ios-sdk.git
  3. Select the version and add to your target

Local Development ​

To use the SDK from a local path:

swift
dependencies: [
    .package(path: "../mobile-sdk/ios/HorusSDK")
]

Permissions ​

Add these keys to your Info.plist:

xml
<!-- Required for microphone recording -->
<key>NSMicrophoneUsageDescription</key>
<string>To record audio with your screen recording for feedback</string>

For broadcast extension (system-wide recording):

xml
<!-- Required for ReplayKit broadcast -->
<key>NSCameraUsageDescription</key>
<string>Required by ReplayKit for screen recording</string>

Quick Start ​

1. Initialize the SDK ​

Initialize in your AppDelegate or App struct:

swift
import HorusSDK

@main
struct MyApp: App {
    init() {
        HorusSDK.shared.initialize(config: HorusConfig(
            embedToken: "proj_your_token_here",
            userInfo: UserInfo(
                email: "user@example.com",
                name: "John Doe",
                userId: "user_123"
            ),
            customMetadata: [
                "plan": "premium",
                "company": "Acme Inc"
            ]
        ))
    }

    var body: some Scene {
        WindowGroup {
            ContentView()
        }
    }
}

2. Start Recording ​

swift
import SwiftUI
import HorusSDK

struct FeedbackView: View {
    @State private var isRecording = false
    @State private var uploadProgress: Float = 0

    var body: some View {
        VStack {
            if isRecording {
                RecordingIndicator(duration: HorusSDK.shared.recordingDuration)

                Button("Stop Recording") {
                    stopRecording()
                }
                .buttonStyle(.borderedProminent)
                .tint(.red)
            } else {
                Button("Record Feedback") {
                    startRecording()
                }
                .buttonStyle(.borderedProminent)
            }

            if uploadProgress > 0 && uploadProgress < 1 {
                ProgressView(value: uploadProgress)
                    .padding()
            }
        }
        .onAppear {
            setupCallbacks()
        }
    }

    func setupCallbacks() {
        HorusSDK.shared.onRecordingStarted = {
            isRecording = true
        }

        HorusSDK.shared.onRecordingStopped = { result in
            isRecording = false
            print("Recording saved: \(result.duration) seconds")
        }

        HorusSDK.shared.onUploadProgress = { progress in
            uploadProgress = progress
        }

        HorusSDK.shared.onUploadComplete = { result in
            uploadProgress = 0
            print("Upload complete: \(result.recordingId)")
        }

        HorusSDK.shared.onError = { error in
            uploadProgress = 0
            print("Error: \(error.localizedDescription)")
        }
    }

    func startRecording() {
        HorusSDK.shared.startInAppRecording(includeAudio: true)
    }

    func stopRecording() {
        HorusSDK.shared.stopRecording(
            description: "Bug in checkout flow",
            metadata: [
                "screen": "checkout",
                "action": "payment_failed"
            ]
        )
    }
}

Recording Modes ​

Uses RPScreenRecorder to capture your app's content. Works well for most use cases.

swift
HorusSDK.shared.startInAppRecording(includeAudio: true)

Pros:

  • No extra setup required
  • Works in foreground
  • Captures app content including videos, animations

Cons:

  • Only captures your app (not system UI or other apps)
  • May stop when app goes to background

Broadcast Recording ​

For system-wide recording, you need a Broadcast Upload Extension.

swift
// Present the system broadcast picker
if #available(iOS 12.0, *) {
    HorusSDK.shared.presentBroadcastPicker(from: someView)
}

See Setting Up Broadcast Extension for setup instructions.

API Reference ​

HorusSDK ​

The main SDK singleton, accessed via HorusSDK.shared.

Initialization ​

swift
HorusSDK.shared.initialize(config: HorusConfig)

Recording Control ​

swift
// Start in-app recording
HorusSDK.shared.startInAppRecording(includeAudio: Bool = true)

// Start recording with system dialog (legacy)
HorusSDK.shared.startRecording(includeAudio: Bool = true)

// Stop and upload
HorusSDK.shared.stopRecording(
    description: String? = nil,
    metadata: [String: Any]? = nil
)

// Cancel without uploading
HorusSDK.shared.cancelRecording()

Properties ​

swift
HorusSDK.shared.isInitialized: Bool  // Check if initialized
HorusSDK.shared.isRecording: Bool    // Check if recording
HorusSDK.shared.recordingDuration: TimeInterval  // Current duration
HorusSDK.shared.isBroadcastAvailable: Bool  // Can use broadcast

Callbacks ​

swift
HorusSDK.shared.onRecordingStarted: (() -> Void)?
HorusSDK.shared.onRecordingStopped: ((RecordingResult) -> Void)?
HorusSDK.shared.onUploadProgress: ((Float) -> Void)?
HorusSDK.shared.onUploadComplete: ((UploadResult) -> Void)?
HorusSDK.shared.onError: ((HorusError) -> Void)?

User Management ​

swift
// Update user info
HorusSDK.shared.setUserInfo(UserInfo(email: "new@example.com"))

// Reset SDK
HorusSDK.shared.reset()

HorusConfig ​

swift
struct HorusConfig {
    var embedToken: String?           // Project embed token (proj_...)
    var recordingLinkId: String?      // Legacy: Direct link token
    var apiUrl: String                // Default: "https://tryhorus.io"
    var userInfo: UserInfo?           // User identification
    var customMetadata: [String: Any]? // Custom key-value pairs
    var videoQuality: VideoQuality    // .low, .medium, .high
    var audioSampleRate: Int          // Default: 44100
}

VideoQuality ​

swift
enum VideoQuality {
    case low     // 480p, 1 Mbps
    case medium  // 720p, 2 Mbps (default)
    case high    // 1080p, 4 Mbps
}

UserInfo ​

swift
struct UserInfo: Codable {
    var email: String?
    var name: String?
    var userId: String?
}

RecordingResult ​

swift
struct RecordingResult {
    let videoURL: URL
    let audioURL: URL?
    let duration: TimeInterval
    let description: String?
}

UploadResult ​

swift
struct UploadResult: Codable {
    let recordingId: String
    let streamVideoId: String?
    let success: Bool
    let message: String?
}

HorusError ​

swift
enum HorusError: Error {
    case notInitialized(String)
    case permissionDenied(String)
    case recordingFailed(String)
    case uploadFailed(String)
    case invalidConfig(String)
    case networkError(String)
}

Setting Up Broadcast Extension ​

For system-wide recording (iOS 12+):

1. Create Extension Target ​

  1. In Xcode, go to File → New → Target
  2. Select Broadcast Upload Extension
  3. Name it (e.g., "HorusBroadcast")
  4. Add to your app target

2. Configure App Groups ​

  1. Enable App Groups capability for both your app and extension
  2. Create a shared app group (e.g., group.com.yourapp.horus)

3. Implement Extension ​

swift
// SampleHandler.swift in your extension
import ReplayKit
import HorusSDK

class SampleHandler: RPBroadcastSampleHandler {

    override func broadcastStarted(withSetupInfo setupInfo: [String : NSObject]?) {
        // Initialize SDK with shared config
        HorusSDK.shared.initializeForBroadcast(
            appGroup: "group.com.yourapp.horus"
        )
    }

    override func processSampleBuffer(_ sampleBuffer: CMSampleBuffer, with sampleBufferType: RPSampleBufferType) {
        // Forward samples to SDK
        HorusSDK.shared.processBroadcastSample(sampleBuffer, type: sampleBufferType)
    }

    override func broadcastFinished() {
        HorusSDK.shared.finishBroadcast()
    }
}

4. Present Broadcast Picker ​

swift
let picker = BroadcastRecorder.createBroadcastPicker(
    preferredExtension: "com.yourapp.HorusBroadcast",
    showMicrophoneButton: true
)
view.addSubview(picker)

SwiftUI Recording Indicator ​

swift
struct RecordingIndicator: View {
    let duration: TimeInterval
    @State private var isBlinking = false

    var body: some View {
        HStack(spacing: 8) {
            Circle()
                .fill(Color.red)
                .frame(width: 12, height: 12)
                .opacity(isBlinking ? 0.3 : 1)

            Text(formatDuration(duration))
                .font(.system(.body, design: .monospaced))
                .foregroundColor(.primary)
        }
        .padding(.horizontal, 12)
        .padding(.vertical, 8)
        .background(Color(.systemBackground))
        .cornerRadius(20)
        .shadow(radius: 4)
        .onAppear {
            withAnimation(.easeInOut(duration: 0.5).repeatForever()) {
                isBlinking = true
            }
        }
    }

    func formatDuration(_ duration: TimeInterval) -> String {
        let minutes = Int(duration) / 60
        let seconds = Int(duration) % 60
        return String.format("%02d:%02d", minutes, seconds)
    }
}

Troubleshooting ​

Recording fails to start ​

  1. Check that RPScreenRecorder.shared().isAvailable returns true
  2. Ensure the app is in foreground
  3. Verify Info.plist has required permission strings

No audio in recording ​

  1. Request microphone permission first
  2. Check AVAudioSession is properly configured
  3. Ensure includeAudio: true is passed

Recording stops when app backgrounds ​

  • This is expected behavior for in-app recording
  • Use Broadcast Extension for background recording

Upload fails ​

  1. Verify network connectivity
  2. Check that embedToken is valid
  3. Ensure sufficient storage space

ReplayKit not available ​

  • Some devices/configurations don't support ReplayKit
  • Screen recording may be disabled via MDM
  • Check isBroadcastAvailable before showing UI

Released under the MIT License.