Permission Delegate Guide
- Permission Delegate Guide
- Background
- Overview
- Integration Steps
- Feature-by-Feature Implementation
- Minimal Integrations (Advertising-Only)
- Troubleshooting
- Further Reading
Background
Starting from version 8.22.184, StreamLayer SDK no longer calls privacy-sensitive iOS frameworks (Contacts, Photos, AVCaptureDevice, AVAudioSession.requestRecordPermission) directly from its binary. Instead, the SDK asks the host app to handle all permission prompts and sensitive data access through the SLRPermissionDelegate protocol.
Why a delegate?
Apple's App Store static analyzer flags any reference to privacy-sensitive APIs in a binary — even if the code path is never executed — and requires the corresponding NSCameraUsageDescription, NSMicrophoneUsageDescription, NSContactsUsageDescription, and NSPhotoLibraryUsageDescription keys in the host app's Info.plist.
Moving these calls out of the SDK binary gives host apps full control over:
- Which permissions appear in the App Store review (via
Info.plistkeys). - When and how permission prompts are displayed to the user.
- Whether privacy-gated features (Watch Party camera, chat photo input, contacts sync) are available at all.
If your integration only uses the advertising overlay and does not need camera, microphone, contacts, or photos — you don't implement the delegate at all, and your Info.plist stays clean.
When do I need to implement it?
| SDK feature used | Permissions required |
|---|---|
| Advertising overlay only | None — skip this guide entirely |
| Voice chat / Watch Party (audio) | Microphone |
| Watch Party (video) | Microphone + Camera |
| Chat photo input | Camera + Photo Library |
| Contacts sync / invite friends | Contacts |
Implement only the delegate methods your features require. All methods have default no-op implementations, so unused permissions are simply "denied" and the relevant features degrade gracefully.
Overview
Protocol: SLRPermissionDelegate
SLRPermissionDelegatepublic protocol SLRPermissionDelegate: AnyObject {
// Permission status & requests
func permissionStatus(for type: SLRPermissionType) -> SLRPermissionStatus
func requestPermission(for type: SLRPermissionType, completion: @escaping (Bool) -> Void)
// Contacts data access
func fetchContacts(completion: @escaping ([SLRContactInfo]) -> Void)
func fetchContact(identifier: String, completion: @escaping (SLRContactInfo?) -> Void)
// Photo library data source
func createPhotoLibraryDataSource() -> SLRPhotoLibraryDataSource?
// Camera capture session
func createCameraCaptureSession() -> SLRCameraCaptureSession?
}All methods are optional thanks to protocol extensions with default implementations that return denied/empty/nil.
Supporting Types
public enum SLRPermissionType: Sendable {
case camera
case microphone
case contacts
case photoLibrary
}
public enum SLRPermissionStatus: Sendable {
case authorized
case denied
case notDetermined
case restricted
}
public struct SLRContactInfo: Sendable {
public let identifier: String
public let givenName: String
public let familyName: String
public let phoneNumbers: [String]
public let thumbnailImageData: Data?
}Integration Steps
Step 1 — Add Required Info.plist Keys
Add only the keys that correspond to features you use. Each key must include a user-facing description explaining why your app needs the data.
<!-- Only if you use Watch Party or voice chat -->
<key>NSMicrophoneUsageDescription</key>
<string>$(PRODUCT_NAME) uses the microphone for voice chat in Watch Party.</string>
<!-- Only if you use Watch Party video or chat photo input -->
<key>NSCameraUsageDescription</key>
<string>$(PRODUCT_NAME) uses the camera for video chat and sharing photos.</string>
<!-- Only if you use contact sync / invite friends -->
<key>NSContactsUsageDescription</key>
<string>$(PRODUCT_NAME) accesses your contacts to help you find friends.</string>
<!-- Only if you use chat photo input -->
<key>NSPhotoLibraryUsageDescription</key>
<string>$(PRODUCT_NAME) accesses your photo library to share photos in chat.</string>Step 2 — Implement SLRPermissionDelegate
SLRPermissionDelegateCreate a dedicated class that implements the protocol. Import the system frameworks corresponding to the features you support:
import UIKit
import AVFoundation // for camera & microphone
import Contacts // for contacts sync
import Photos // for photo library
import StreamLayerSDK
final class PermissionDelegateImpl: NSObject, SLRPermissionDelegate {
// ...implementations below
}Tip: You can implement the delegate directly on your
AppDelegate, but a dedicated class keeps yourAppDelegateclean.
Step 3 — Wire Up the Delegate
Assign the delegate after StreamLayer.initSDK(...):
func application(_ application: UIApplication,
didFinishLaunchingWithOptions: [...]) -> Bool {
StreamLayer.initSDK(with: apiKey,
isDebug: false,
delegate: self,
loggerDelegate: self)
// Assign permission delegate
StreamLayer.shared.permissionDelegate = permissionDelegate
return true
}
private let permissionDelegate = PermissionDelegateImpl()Important:
permissionDelegateis declaredweakonStreamLayer.shared. Retain your delegate instance as a property on yourAppDelegateor Scene coordinator so it isn't deallocated.
Feature-by-Feature Implementation
Permission Status Checks
Map iOS system status enums to SLRPermissionStatus:
func permissionStatus(for type: SLRPermissionType) -> SLRPermissionStatus {
switch type {
case .camera:
switch AVCaptureDevice.authorizationStatus(for: .video) {
case .authorized: return .authorized
case .denied: return .denied
case .notDetermined: return .notDetermined
case .restricted: return .restricted
@unknown default: return .denied
}
case .microphone:
switch AVAudioSession.sharedInstance().recordPermission {
case .granted: return .authorized
case .denied: return .denied
case .undetermined: return .notDetermined
@unknown default: return .denied
}
case .contacts:
switch CNContactStore.authorizationStatus(for: .contacts) {
case .authorized: return .authorized
case .denied: return .denied
case .notDetermined: return .notDetermined
case .restricted: return .restricted
@unknown default: return .denied
}
case .photoLibrary:
switch PHPhotoLibrary.authorizationStatus() {
case .authorized, .limited: return .authorized
case .denied: return .denied
case .notDetermined: return .notDetermined
case .restricted: return .restricted
@unknown default: return .denied
}
}
}Permission Requests
Forward the request to the system API and call completion on the main queue:
func requestPermission(for type: SLRPermissionType,
completion: @escaping (Bool) -> Void) {
switch type {
case .camera:
AVCaptureDevice.requestAccess(for: .video) { granted in
DispatchQueue.main.async { completion(granted) }
}
case .microphone:
AVAudioSession.sharedInstance().requestRecordPermission { granted in
DispatchQueue.main.async { completion(granted) }
}
case .contacts:
CNContactStore().requestAccess(for: .contacts) { granted, _ in
DispatchQueue.main.async { completion(granted) }
}
case .photoLibrary:
PHPhotoLibrary.requestAuthorization { status in
DispatchQueue.main.async {
completion(status == .authorized || status == .limited)
}
}
}
}Contacts Data Access
The SDK needs contact info for address book sync and invite flows. Provide contacts as SLRContactInfo structs — not CNContact objects.
func fetchContacts(completion: @escaping ([SLRContactInfo]) -> Void) {
DispatchQueue.global(qos: .userInitiated).async {
let store = CNContactStore()
let keys: [CNKeyDescriptor] = [
CNContactIdentifierKey as CNKeyDescriptor,
CNContactGivenNameKey as CNKeyDescriptor,
CNContactFamilyNameKey as CNKeyDescriptor,
CNContactPhoneNumbersKey as CNKeyDescriptor,
CNContactThumbnailImageDataKey as CNKeyDescriptor
]
let request = CNContactFetchRequest(keysToFetch: keys)
var result: [SLRContactInfo] = []
do {
try store.enumerateContacts(with: request) { cnContact, _ in
result.append(SLRContactInfo(
identifier: cnContact.identifier,
givenName: cnContact.givenName,
familyName: cnContact.familyName,
phoneNumbers: cnContact.phoneNumbers.map { $0.value.stringValue },
thumbnailImageData: cnContact.thumbnailImageData
))
}
} catch {
print("Failed to fetch contacts: \(error)")
}
DispatchQueue.main.async { completion(result) }
}
}
func fetchContact(identifier: String,
completion: @escaping (SLRContactInfo?) -> Void) {
DispatchQueue.global(qos: .userInitiated).async {
let store = CNContactStore()
let keys: [CNKeyDescriptor] = [
CNContactIdentifierKey as CNKeyDescriptor,
CNContactGivenNameKey as CNKeyDescriptor,
CNContactFamilyNameKey as CNKeyDescriptor,
CNContactPhoneNumbersKey as CNKeyDescriptor,
CNContactThumbnailImageDataKey as CNKeyDescriptor
]
do {
let cn = try store.unifiedContact(withIdentifier: identifier,
keysToFetch: keys)
let info = SLRContactInfo(
identifier: cn.identifier,
givenName: cn.givenName,
familyName: cn.familyName,
phoneNumbers: cn.phoneNumbers.map { $0.value.stringValue },
thumbnailImageData: cn.thumbnailImageData
)
DispatchQueue.main.async { completion(info) }
} catch {
DispatchQueue.main.async { completion(nil) }
}
}
}Photo Library Data Source
The SDK uses a data source for browsing photos in the chat photo picker. Implement SLRPhotoLibraryDataSource backed by PHAsset and PHCachingImageManager:
public protocol SLRPhotoLibraryDataSource: AnyObject {
var delegate: SLRPhotoLibraryDataSourceDelegate? { get set }
var count: Int { get }
@discardableResult
func requestPreviewImage(at index: Int, targetSize: CGSize,
completion: @escaping (Result<UIImage, Error>) -> Void)
-> SLRPhotoRequestHandle
@discardableResult
func requestFullImage(at index: Int, progressHandler: ((Double) -> Void)?,
completion: @escaping (Result<UIImage, Error>) -> Void)
-> SLRPhotoRequestHandle
func fullImageRequest(at index: Int) -> SLRPhotoRequestHandle?
}Return an instance from createPhotoLibraryDataSource():
func createPhotoLibraryDataSource() -> SLRPhotoLibraryDataSource? {
return PhotoLibraryDataSourceImpl()
}A complete reference implementation backed by PHAsset/PHCachingImageManager is provided in the example app at:
Example iOS/Example iOS/SLRPermission/SLRPhotoLibraryDataSourceImpl.swift
Copy it as-is if your integration only needs standard photo library browsing.
Camera Capture Session
For live camera preview in the chat input, implement SLRCameraCaptureSession backed by AVCaptureSession:
public protocol SLRCameraCaptureSession: AnyObject {
var previewLayer: CALayer? { get }
var isInitialized: Bool { get }
var isCapturing: Bool { get }
func startCapturing(_ completion: @escaping () -> Void)
func stopCapturing(_ completion: @escaping () -> Void)
}Return an instance from createCameraCaptureSession():
func createCameraCaptureSession() -> SLRCameraCaptureSession? {
return CameraCaptureSessionImpl()
}Minimal Integrations (Advertising-Only)
If your integration only uses the advertising overlay:
- Do not add any of the four privacy
Info.plistkeys. - Do not assign
StreamLayer.shared.permissionDelegate. - Do not import
Contacts,Photos,AVFoundation(except for video playback in your own player).
The SDK will:
- Treat every permission as
.denied. - Skip privacy-gated features (Watch Party camera/mic, chat photo input, contacts sync) without crashing.
- Ship a binary with no references to
CNContactStore,PHPhotoLibrary, orAVCaptureDevice— App Store Connect will not require the privacy keys.
Troubleshooting
Q: App Store Connect still complains about missing privacy descriptions.
Your host app code, not the SDK, is likely referencing these frameworks. Search your project for import Contacts, import Photos, AVCaptureDevice, or AVAudioSession.requestRecordPermission. If you implement SLRPermissionDelegate, these references belong only in your delegate implementation file(s) and you must add the matching Info.plist keys.
Q: My delegate method isn't being called.
Ensure you've assigned the delegate after StreamLayer.initSDK(...) and that your delegate instance is retained (held as a strong property somewhere).
Q: The feature (camera preview / photo picker) is blank.
Confirm createPhotoLibraryDataSource() / createCameraCaptureSession() returns a non-nil instance, and that permissions were granted via requestPermission(for:).
Q: How do I test the "no delegate" path?
Temporarily comment out the line StreamLayer.shared.permissionDelegate = .... Verify the advertising overlay still works and that privacy-gated features simply do not appear (no crashes, no prompts).
Updated about 4 hours ago
