Request Permission to access the Camera, Microphone, Location, etc.
SwiftyLaunch's SharedKit comes with a handy integrated way to ask users for permissions to access their Camera, Microphone, Location, and more. This is a common task in many apps and SharedKit makes it easy to do so.
RequestType
enum
The RequestType
enum defines all the different Capabalities the user can be asked to grant access to.
The following types are available out of the box:
public enum RequestType: Identifiable {
case photosAccess
case cameraAccess
case microphoneAccess
case locationAccess
case contactsAccess
case calendarAccess
case remindersAccess
}
These types describe access to specific system resources and require the user's permission to access them.
Problem with Permissions
When asking for permissions, it is important to remember that the user can deny access to the requested resource, and that the app should handle this case gracefully. Additionally, in most cases we can only ask for permissions once, so it is important to be able to clearly tell the user that they should grant access to the requested resource in app's settings.
So, we have to keep following things in mind:
- We have to ask the user for permissions to access specific resources.
- We can only do it once.
- If the user denies access, we have to handle this case gracefully.
- The user can always revoke access in the app's settings.
To address this, we have designed a simple way to ask for permissions in SharedKit, using closures, notification center and SwiftUI's view modifiers.
Basic Usage Example
Here's a common example on how to ask the user for microphone access:
In this example we have the haveMicrophonePermission
boolean variable, which holds the information, whether
the user has granted us access to the microphone or not. By default it is set to false.
As soon as the view appears, we fetch the permission status and set the haveMicrophonePermission
variable accordingly.
Note, that we can't set it in init, as we can't use async functions in init.
Then, there is a button, which asks the user for microphone access with two closures. The first one is executed if the user grants us access,
the second one is executed if the user denies access. We use these closures to set the haveMicrophonePermission
variable accordingly.
By pressing on the button, a system dialog appears asking the user for microphone access. Depending on the user's choice, the first or the second closure is executed.
If we already have access to the microphone, pressing on the button will straight up execute the executeIfGotAccess
closure.
import SwiftUI
import SharedKit
struct TestPermissions: View {
@State private var haveMicrophonePermission: Bool = false
var body: some View {
VStack {
Text("Got Permission: \(haveMicrophonePermission ? "Yes" : "No")")
Button("Ask For Microphone Access") {
askUserFor(.microphoneAccess, executeIfGotAccess: {
haveMicrophonePermission = true
}, onDismiss: {
haveMicrophonePermission = false
})
}
}
.onAppear {
Task {
let permissionStatus = await RequestType.microphoneAccess.permissionStatus()
haveMicrophonePermission = (permissionStatus == .gotPermission)
}
}
}
}
What happens if the user presses on allow in the sheet, but denies the system prompt?
If the user presses on allow, but denies the system prompt, the access sheet will automatically be dismissed.
As we can only show the user the system prompt once, we will have to refer the user to grant us access in app's settings.
The provided sheet knows that out of the box and replaces the button that shows the system prompt with a button that redirets the user to the settings panel.
Requiring Permissions to display a View
Another common use case would be to require the user to grand access to a specific resource to display a view.
An example could be the requirement to user location to show a map view, or to require camera access to show camera preview view.
For this, we would use the .requireCapabilityPermission()
view modifier, which takes a RequestType
as an argument.
This is exactly what we use to power our AI Vision Example in AIKit. We display a camera feed, but only if the user has granted us access to the camera.
import SwiftUI
import SharedKit
// Other imports
struct AIVisionExampleView: View {
// ...
/// View Model to control this View
@StateObject private var vm = AIVisionExampleViewModel()
/// Interact with the camera
@StateObject private var cameraViewModel = CameraViewModel()
// ...
VStack(spacing: 25) {
CameraView()
.requireCapabilityPermission(
of: .cameraAccess,
onSuccess: {
// got access to camera -> start feed
vm.gotCameraPermissions(cameraVM: cameraViewModel)
},
)
HStack {
// ...
Button {
// Once the user presses the button and releases it, we tell that to our ViewModel
vm.releasedShutterButton(cameraVM: cameraViewModel, voiceRecordingVM: voiceRecordingViewModel)
} label: {
Circle()
.fill(.white)
.frame(width: 75, height: 75)
}
.buttonStyle(ShutterButton())
// ...
}
}
}
This results in an inline permission request view, which will be shown instead of the feed if the user hasn't granted us access to the camera yet.
Relevant Functions, Variables and Modifiers
Attached to RequestType
enum
As discussed before, it defines all the different Capabalities the user can be asked to grant access to.
Current Permission Status
Async function will return the current permission status of the requested capability. An example of the photosAccess
case is shown below:
public enum RequestType: Identifiable {
@MainActor public var permissionStatus: () async -> RequestPermissionStatus {
switch self {
// ... other cases
case .photosAccess:
return {
let status = PHPhotoLibrary.authorizationStatus(for: .readWrite)
switch status {
case .notDetermined:
return .notYetAsked
case .restricted:
return .denied
case .denied:
return .denied
case .authorized:
return .gotPermission
case .limited: // means that we do have access to the library, but just not all of it.
return .gotPermission
@unknown default: // This is for future proofing, if Apple adds more cases.
return .denied
}
}
// ... other cases
}
}
}
The RequestPermissionStatus
enum is defined as follows:
public enum RequestPermissionStatus {
/// Means the user has not yet given permission
case notYetAsked
/// Means the user has already given permission
case gotPermission
/// Means the user has denied and we should send the user to the settings
case denied
}
Usage example:
import SharedKit
let permissionStatus = await RequestType.photosAccess.permissionStatus()
Globally defined
Ask User for Permission
This function will show user a sheet if the user hasn't granted us access to the requested capability yet,
and will execute the executeIfGotAccess
closure if the user grants us access, or the onDismiss
closure if the user denies access.
public func askUserFor(_ type: RequestType, executeIfGotAccess: (() -> Void)? = nil, onDismiss: (() -> Void)? = nil) { }
Usage example:
import SharedKit
askUserFor(.locationAccess, executeIfGotAccess: {
// do something if we got access
}, onDismiss: {
// do something if we didn't get access
})
This function works by calling sending a notification to the notification center, which is then picked up by the ShowRequestSheetWhenNeededModifier()
view modifier,
which is attached to the root view of the app. It listens for the notification and shows the sheet if needed (if the user has not granted permission yet).
Require Capability Permission View Modifier
If you want to lock a view behind a permission request, you can use the .requireCapabilityPermission()
view modifier.
extension View {
/// This modifier makes sure that the view that it is applied to will only be shown if the user has granted us with the required permissions
public func requireCapabilityPermission(
of permissionRequirement: RequestType,
onSuccess: @escaping () -> Void = {},
onCancel: @escaping () -> Void = {}
) -> some View {
modifier(RequireCapabilityPermissionViewModifier(permissionRequirement: permissionRequirement, onSuccess: onSuccess, onCancel: onCancel))
}
}
You can attach it to any view that might need special capabilities to be of use. It will replace the view with a permission request view if the user hasn't granted us access to the requested capability yet.
You pass to the view modifier the capability you want to ask for, and optionally two additional closures that will be executed if the user grants us access or denies access.
See the example above for usage.
Customizing the Permission Request Sheet Text
It is recommended to customize the text of the permission request sheet to tell the user why exactly you need access to the requested capability.
To do so, you have to set the permission message in two places:
Project's Info.plist
In your project's Info.plist file, you have to set the NSCameraUsageDescription
, NSMicrophoneUsageDescription
, NSLocationWhenInUseUsageDescription
, etc. keys.
This is the text that will be shown in the system prompt.
In RequestType.swift
Edit the data
variable in the RequestType
enum to the information that you want to be shown in the permission request sheet.
public enum RequestType: Identifiable {
// ...
/// Ask the user for photo library access
case photosAccess
// ...
public var data: RequestTypeData {
switch self {
// ...
case .photosAccess:
return RequestTypeData(
sfSymbolName: "photo.on.rectangle.angled",
title: "Allow Access to Photos?",
subtitle: "With Access to your Photo\nLibrary, we can do cool stuff.",
footerNote: "We won't spy on you, we promise.",
ctaText: "Allow Access to Photos"
)
// ...
}
}
}