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
Swift Package Manager (Recommended)
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:
- Go to File → Add Packages...
- Enter the repository URL:
https://github.com/tryhorus/horus-ios-sdk.git - 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
In-App Recording (Recommended)
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 broadcastCallbacks
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
- In Xcode, go to File → New → Target
- Select Broadcast Upload Extension
- Name it (e.g., "HorusBroadcast")
- Add to your app target
2. Configure App Groups
- Enable App Groups capability for both your app and extension
- 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
- Check that
RPScreenRecorder.shared().isAvailablereturnstrue - Ensure the app is in foreground
- Verify Info.plist has required permission strings
No audio in recording
- Request microphone permission first
- Check
AVAudioSessionis properly configured - Ensure
includeAudio: trueis passed
Recording stops when app backgrounds
- This is expected behavior for in-app recording
- Use Broadcast Extension for background recording
Upload fails
- Verify network connectivity
- Check that
embedTokenis valid - Ensure sufficient storage space
ReplayKit not available
- Some devices/configurations don't support ReplayKit
- Screen recording may be disabled via MDM
- Check
isBroadcastAvailablebefore showing UI