A comprehensive Swift package for managing iOS permissions with a modern async/await API and SwiftUI integration.
- 🎯 Comprehensive Permission Support: Location, Notifications, Camera, Microphone, Photo Library, Contacts, Calendar, Reminders, Health, Motion, Biometrics, and App Tracking Transparency
- ⚡ Modern API: Built with async/await and Combine for reactive programming
- 🧩 SwiftUI Integration: Ready-to-use view modifiers and components
- 🔍 Protocol-Based: Easily mockable for testing
- 📱 Multi-Platform: iOS, macOS, tvOS, watchOS support
- 🧪 Fully Tested: Comprehensive unit test coverage
- 🎨 Customizable UI: Beautiful permission request views and dashboards
- 🏗️ Dependency Injection: No singletons - follows clean architecture principles
- 🧪 Testable: Easy to mock and unit test with dependency injection
Add SwiftPermissions to your project through Xcode:
- File → Add Package Dependencies...
- Enter the repository URL:
https://github.com/MoElnaggar14/SwiftPermissions - Choose your version preference
Or add it to your Package.swift:
dependencies: [
.package(url: "https://github.com/MoElnaggar14/SwiftPermissions", from: "1.0.0")
]import SwiftPermissions
class MyViewController {
private let permissionManager: PermissionManagerProtocol
init(permissionManager: PermissionManagerProtocol = PermissionManagerFactory.default()) {
self.permissionManager = permissionManager
}
func requestCameraAccess() async {
// Check permission status
let status = await permissionManager.status(for: .camera)
// Request a permission
let result = await permissionManager.request(.camera)
if result.isSuccess {
// Permission granted
} else {
// Permission denied or error occurred
}
// Request multiple permissions
let results = await permissionManager.requestMultiple([.camera, .microphone, .photoLibrary])
}
}import SwiftPermissions
// Create a permission manager instance
let permissionManager = PermissionManagerFactory.default()
// Check and request permissions
let cameraStatus = await permissionManager.status(for: .camera)
let cameraResult = await permissionManager.request(.camera)
let locationResult = await permissionManager.request(.locationWhenInUse)// Use convenience methods on any permission manager instance
let permissionManager = PermissionManagerFactory.default()
// Quick access to common permissions
let cameraResult = await permissionManager.requestCamera()
let locationResult = await permissionManager.requestLocation()
let notificationResult = await permissionManager.requestNotifications()// Create a permission manager instance
let permissionManager = PermissionManagerFactory.default()
// Request permissions by category
let mediaResults = await permissionManager.requestMultiple(.media) // Camera, microphone, photo library
let locationResults = await permissionManager.requestMultiple(.location) // Location when in use, notifications
let socialResults = await permissionManager.requestMultiple(.social) // Contacts, photo library, camera, notificationsimport SwiftUI
import SwiftPermissions
struct ContentView: View {
@State private var showPermissionAlert = false
var body: some View {
VStack {
Text("My App")
Button("Request Camera Permission") {
showPermissionAlert = true
}
}
.requestPermissionsOnAppear([.notification]) { results in
// Handle results
}
.permissionAlert(
for: .camera,
isPresented: $showPermissionAlert,
config: PermissionConfig(
title: "Camera Access Required",
message: "We need camera access to take photos."
)
)
.conditionalOnPermission(.camera) {
// Show when camera is authorized
CameraView()
} notAuthorized: {
// Show when camera is not authorized
PermissionDeniedView()
}
}
}struct PermissionView: View {
@StateObject private var permissionManager = ObservablePermissionManager()
var body: some View {
VStack {
if permissionManager.isAuthorized(.camera) {
Text("Camera is authorized")
.foregroundColor(.green)
} else {
Button("Request Camera Permission") {
Task {
await permissionManager.requestPermission(.camera)
}
}
}
}
.task {
await permissionManager.checkStatus(for: .camera)
}
}
}struct SettingsView: View {
var body: some View {
List {
PermissionStatusView(.camera)
PermissionStatusView(.microphone)
PermissionStatusView(.photoLibrary)
}
}
}struct PermissionsView: View {
var body: some View {
PermissionsDashboardView(permissions: [.camera, .microphone, .photoLibrary, .location])
}
}let permissionManager = PermissionManagerFactory.default()
let config = PermissionConfig(
title: "Location Permission Required",
message: "This app uses location to provide personalized recommendations.",
settingsTitle: "Enable Location in Settings",
settingsMessage: "Go to Settings > Privacy > Location Services to enable location for this app."
)
let result = await permissionManager.request(.location, config: config)import Combine
class ViewModel: ObservableObject {
private let permissionManager: PermissionManagerProtocol
private var cancellables = Set<AnyCancellable>()
init(permissionManager: PermissionManagerProtocol = PermissionManagerFactory.default()) {
self.permissionManager = permissionManager
permissionManager.permissionStatusChanged
.sink { (type, status) in
print("Permission \(type) changed to \(status)")
}
.store(in: &cancellables)
}
}The package supports the following permission types:
| Permission Type | Description |
|---|---|
.location |
General location access |
.locationWhenInUse |
Location when app is in use |
.locationAlways |
Always-on location access |
.notification |
Push notifications |
.camera |
Camera access |
.microphone |
Microphone access |
.photoLibrary |
Photo library access |
.contacts |
Contacts access |
.calendar |
Calendar access |
.reminders |
Reminders access |
.health |
HealthKit data access |
.motion |
Motion and fitness data |
.faceID |
Face ID authentication |
.touchID |
Touch ID authentication |
.tracking |
App tracking transparency |
Each permission can have one of the following statuses:
.notDetermined- Permission hasn't been requested yet.authorized- Permission is granted.denied- Permission is denied.restricted- Permission is restricted (parental controls, etc.).provisional- Provisional permission (notifications only)
This package follows clean architecture principles and avoids common anti-patterns:
Unlike many permission libraries, SwiftPermissions does not use singleton patterns. Instead, it uses:
- Dependency Injection: Pass permission managers as dependencies
- Protocol-based design: Easy to mock and test
- Factory pattern: Create instances when needed
- ✅ Testable: Easy to inject mocks for unit testing
- ✅ Flexible: Multiple instances for different contexts
- ✅ Maintainable: Clear dependencies, no global state
- ✅ Thread-safe: No shared mutable state
- ❌ Avoids: Tight coupling, hidden dependencies, testing difficulties
// Recommended: Use dependency injection
class MyService {
private let permissionManager: PermissionManagerProtocol
init(permissionManager: PermissionManagerProtocol = PermissionManagerFactory.default()) {
self.permissionManager = permissionManager
}
}
// Alternative: Create instances directly
let permissionManager = PermissionManagerFactory.default()
// For testing: Use mocks
let mockManager = PermissionManagerFactory.mock(shouldGrantPermissions: true)The package includes a mock implementation for testing:
import SwiftPermissions
class MyTestCase: XCTestCase {
func testPermissions() async {
let mockManager = PermissionManagerFactory.mock()
// Configure mock behavior
if let mock = mockManager as? MockPermissionManager {
mock.setShouldGrantPermissions(true)
mock.setMockStatus(.authorized, for: .camera)
}
let result = await mockManager.request(.camera)
XCTAssertTrue(result.isSuccess)
}
}Make sure to add the required usage descriptions to your app's Info.plist:
<key>NSCameraUsageDescription</key>
<string>This app needs access to camera to take photos.</string>
<key>NSMicrophoneUsageDescription</key>
<string>This app needs access to microphone to record audio.</string>
<key>NSPhotoLibraryUsageDescription</key>
<string>This app needs access to photo library to save and select photos.</string>
<key>NSLocationWhenInUseUsageDescription</key>
<string>This app needs location access to provide location-based features.</string>
<key>NSLocationAlwaysAndWhenInUseUsageDescription</key>
<string>This app needs location access to provide location-based features.</string>
<key>NSContactsUsageDescription</key>
<string>This app needs access to contacts to help you connect with friends.</string>
<key>NSCalendarsUsageDescription</key>
<string>This app needs access to calendar to schedule events.</string>
<key>NSRemindersUsageDescription</key>
<string>This app needs access to reminders to create tasks.</string>
<key>NSMotionUsageDescription</key>
<string>This app needs access to motion data to track your activity.</string>
<key>NSFaceIDUsageDescription</key>
<string>This app uses Face ID for secure authentication.</string>
<key>NSUserTrackingUsageDescription</key>
<string>This app would like to track you across apps and websites to provide personalized ads.</string>- iOS 15.0+ / macOS 12.0+ / tvOS 15.0+ / watchOS 8.0+
- Swift 5.7+
- Xcode 14.0+
We welcome contributions! Please see our Contributing Guide for more details.
- Fork the repository
- Create your feature branch (
git checkout -b feature/AmazingFeature) - Commit your changes (
git commit -m 'Add some AmazingFeature') - Push to the branch (
git push origin feature/AmazingFeature) - Open a Pull Request
- 📧 Email: moelnaggar14@gmail.com
- 💬 Discussions: GitHub Discussions
- 🐛 Issues: GitHub Issues
This package is available under the MIT license. See the LICENSE file for more info.