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.