
Facilitates permission requests across Android and iOS environments, supporting a variety of permissions like location and camera, with easy setup and integration in multiplatform projects.
kPermissions is a Kotlin Multiplatform library that helps you manage runtime permissions on Android and iOS, with full modularity and optional support for Jetpack Compose Multiplatform (CMP).
expect/actual API structurecommonMain
| Module | Purpose |
|---|---|
kPermissionsApi |
Shared interface definitions like Permission, PermissionStatus, etc. |
kPermissionsCore |
Base logic and iOS Manager |
kPermissionsGallery, kPermissionsCamera, kPermissionsNotification, etc. |
Ready-to-use permission modules |
| Approach | Shared module | iOS app | Android app |
|---|---|---|---|
| KMP (native SwiftUI) |
sharedCode/build.gradle.kts → SharedCode |
iosApp/iosApp/iOSApp.swift |
— |
| CMP (Compose Multiplatform) |
sharedUI/build.gradle.kts → ComposeApp |
iosAppCMP |
androidSimple |
sharedCode + iosApp for native SwiftUI iOS. Permissions via
IOSPermissionManager + PermissionsView.sharedUI for iOS (iosAppCMP) and Android (androidSimple). Permissions via
rememberPermissionState in Compose.| App / Module | Framework | UI Stack | Purpose |
|---|---|---|---|
iosApp |
SharedCode | Native SwiftUI | Sample iOS app using IOSPermissionManager + PermissionsView
|
iosAppCMP |
ComposeApp | Compose Multiplatform | Sample iOS app using Compose UI (ContentView → ComposeView) |
androidSimple |
— | Compose Multiplatform | Sample Android app using sharedUI |
sharedCode |
SharedCode.xcframework | — | KMP shared module; exports kPermissions APIs for native iOS (used by iosApp) |
sharedUI |
ComposeApp.xcframework | Compose | KMP Compose module with kPermissions CMP; used by iosAppCMP & androidSimple
|
dependencies {
implementation("io.github.kpermissions:kpermissions-gallery:<version>")
// Add more as needed
}kPermissionsCore manually. It's included automatically inside each
permission module via api(...).
sharedCode/build.gradle.kts — export kPermissions so Swift can use them:iosTarget.binaries.framework {
baseName = "SharedCode"
isStatic = true
export(projects.kPermissionsApi)
export(projects.kPermissionsCore)
export(projects.kPermissionsCamera)
export(projects.kPermissionsGallery)
export(projects.kPermissionsNotification)
}iosApp/iosApp/iOSApp.swift — entry point for native SwiftUI:@main
struct iOSApp: App {
var body: some Scene {
WindowGroup {
PermissionsView()
}
}
}sharedUI/build.gradle.kts — add kPermissions CMP modules in commonMain:commonMain.dependencies {
implementation(projects.kPermissionsCore)
implementation(projects.kPermissionsCamera)
implementation(projects.kPermissionsGallery)
implementation(projects.kPermissionsNotification)
implementation(projects.kPermissionsCMP)
}iosAppCMP/iosApp/iOSApp.swift — CMP iOS entry point:@main
struct iOSApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
}Android: androidSimple depends on sharedUI; no extra Swift setup.
In CMP apps (e.g. iosAppCMP, androidSimple via sharedUI), use rememberPermissionState:
val permission = rememberPermissionState(GalleryPermission)
LaunchedEffect(Unit) {
permission.launchPermissionRequest { granted ->
if (granted) {
// ✅ Permission granted
} else {
// ❌ Permission denied
}
}
}On iOS, the CMP UI is hosted via ComposeView (see iosAppCMP/iosApp/ContentView.swift).
| App | Entry Point | UI |
|---|---|---|
| iosApp (NativeUI) |
iOSApp.swift → WindowGroup { PermissionsView() }
|
Native SwiftUI + SharedCode |
| iosAppCMP (KPermissionsApp) |
iOSApp.swift → WindowGroup { ContentView() }
|
Compose Multiplatform via ComposeView
|
If you are using the library in a native iOS project with SwiftUI (like iosApp), you can use
IOSPermissionManager to handle permissions:
import SwiftUI
import SharedCode
struct PermissionsView: View {
@State private var cameraStatus: String = "غير معروف"
@State private var notificationStatus: String = "لم يتم الطلب"
@State private var galleryStatus: String = "غير معروف"
var body: some View {
ScrollView {
VStack(spacing: 25) {
Text("إدارة التصاريح")
.font(.largeTitle)
.fontWeight(.bold)
.padding(.top)
// قسم الإشعارات
PermissionCard(
title: "الإشعارات",
status: notificationStatus,
icon: "bell.badge.fill",
color: .green,
action: { requestNotifications() }
)
// قسم الكاميرا
PermissionCard(
title: "الكاميرا",
status: cameraStatus,
icon: "camera.fill",
color: .blue,
action: { requestCamera() }
)
// قسم المعرض
PermissionCard(
title: "معرض الصور",
status: galleryStatus,
icon: "photo.on.rectangle.angled",
color: .purple,
action: { requestGallery() }
)
Spacer()
}
.padding()
}
.onAppear {
requestNotifications()
}
}
// --- الدوال الخاصة بطلب التصاريح ---
private func requestCamera() {
Task {
do {
let status = try await IOSPermissionManager.shared.requestPermission(permission: CameraPermission())
updateStatusText(status: status, for: "camera")
handlePermissionResult(status: status)
} catch {
print(error)
}
}
}
private func requestNotifications() {
Task {
do {
let status = try await IOSPermissionManager.shared.requestPermission(permission: NotificationPermission())
updateStatusText(status: status, for: "notification")
handlePermissionResult(status: status)
} catch {
print(error)
}
}
}
private func requestGallery() {
Task {
do {
let status = try await IOSPermissionManager.shared.requestPermission(permission: GalleryPermission())
updateStatusText(status: status, for: "gallery")
handlePermissionResult(status: status)
} catch {
print(error)
}
}
}
// دالة لتحديث النصوص المعروضة في الواجهة بالعربي
private func updateStatusText(status: PermissionStatus, for type: String) {
let text: String
switch status {
case is PermissionStatusGranted:
text = "مسموح"
case is PermissionStatusDenied:
text = "مرفوض"
case is PermissionStatusDeniedPermanently:
text = "مرفوض نهائياً"
case is PermissionStatusUnavailable:
text = "غير متوفر"
default:
text = "غير معروف"
}
// تحديث المتغير الصحيح بناءً على النوع
DispatchQueue.main.async {
if type == "camera" {
self.cameraStatus = text
} else if type == "notification" {
self.notificationStatus = text
} else if type == "gallery" {
self.galleryStatus = text
}
}
}
private func handlePermissionResult(status: PermissionStatus) {
if status is PermissionStatusDeniedPermanently {
IOSPermissionManager.shared.openAppSettings()
}
}
}
// مكون فرعي للبطاقة
struct PermissionCard: View {
let title: String
let status: String
let icon: String
let color: Color
let action: () -> Void
var body: some View {
VStack(alignment: .leading, spacing: 12) {
HStack {
Image(systemName: icon)
.foregroundColor(color)
.font(.headline)
Text(title)
.font(.headline)
Spacer()
Text(status)
.font(.subheadline)
.foregroundColor(status == "مسموح" ? .green : (status == "مرفوض" ? .red : .gray))
}
Button(action: action) {
Label("طلب إذن \(title)", systemImage: icon)
.font(.subheadline)
.fontWeight(.semibold)
.frame(maxWidth: .infinity)
.padding(.vertical, 12)
.background(color)
.foregroundColor(.white)
.cornerRadius(10)
}
}
.padding()
.background(Color(.secondarySystemBackground))
.cornerRadius(12)
}
}import io.github.kPermissions_api.Permission
expect object RecordAudioPermission : Permissionimport android.Manifest
import io.github.kPermissions_api.Permission
actual object RecordAudioPermission : Permission {
override val name: String = "record_audio"
override val androidPermissionName: String = Manifest.permission.RECORD_AUDIO
}import io.github.kPermissions_api.Permission
import io.github.kPermissions_api.PermissionStatus
import platform.AVFoundation.*
actual object RecordAudioPermission : Permission {
override val name: String = "record_audio"
override val permissionRequest: ((Boolean) -> Unit) -> Unit
get() = { callback ->
AVAudioSession.sharedInstance().requestRecordPermission { granted ->
dispatch_async(dispatch_get_main_queue()) { callback(granted) }
}
}
override fun getPermissionStatus(): PermissionStatus {
return when (AVAudioSession.sharedInstance().recordPermission) {
AVAudioSessionRecordPermissionGranted -> PermissionStatus.Granted
AVAudioSessionRecordPermissionDenied -> PermissionStatus.DeniedPermanently
AVAudioSessionRecordPermissionUndetermined -> PermissionStatus.Denied
else -> PermissionStatus.Denied
}
}
}MIT License
kPermissions is a Kotlin Multiplatform library that helps you manage runtime permissions on Android and iOS, with full modularity and optional support for Jetpack Compose Multiplatform (CMP).
expect/actual API structurecommonMain
| Module | Purpose |
|---|---|
kPermissionsApi |
Shared interface definitions like Permission, PermissionStatus, etc. |
kPermissionsCore |
Base logic and iOS Manager |
kPermissionsGallery, kPermissionsCamera, kPermissionsNotification, etc. |
Ready-to-use permission modules |
| Approach | Shared module | iOS app | Android app |
|---|---|---|---|
| KMP (native SwiftUI) |
sharedCode/build.gradle.kts → SharedCode |
iosApp/iosApp/iOSApp.swift |
— |
| CMP (Compose Multiplatform) |
sharedUI/build.gradle.kts → ComposeApp |
iosAppCMP |
androidSimple |
sharedCode + iosApp for native SwiftUI iOS. Permissions via
IOSPermissionManager + PermissionsView.sharedUI for iOS (iosAppCMP) and Android (androidSimple). Permissions via
rememberPermissionState in Compose.| App / Module | Framework | UI Stack | Purpose |
|---|---|---|---|
iosApp |
SharedCode | Native SwiftUI | Sample iOS app using IOSPermissionManager + PermissionsView
|
iosAppCMP |
ComposeApp | Compose Multiplatform | Sample iOS app using Compose UI (ContentView → ComposeView) |
androidSimple |
— | Compose Multiplatform | Sample Android app using sharedUI |
sharedCode |
SharedCode.xcframework | — | KMP shared module; exports kPermissions APIs for native iOS (used by iosApp) |
sharedUI |
ComposeApp.xcframework | Compose | KMP Compose module with kPermissions CMP; used by iosAppCMP & androidSimple
|
dependencies {
implementation("io.github.kpermissions:kpermissions-gallery:<version>")
// Add more as needed
}kPermissionsCore manually. It's included automatically inside each
permission module via api(...).
sharedCode/build.gradle.kts — export kPermissions so Swift can use them:iosTarget.binaries.framework {
baseName = "SharedCode"
isStatic = true
export(projects.kPermissionsApi)
export(projects.kPermissionsCore)
export(projects.kPermissionsCamera)
export(projects.kPermissionsGallery)
export(projects.kPermissionsNotification)
}iosApp/iosApp/iOSApp.swift — entry point for native SwiftUI:@main
struct iOSApp: App {
var body: some Scene {
WindowGroup {
PermissionsView()
}
}
}sharedUI/build.gradle.kts — add kPermissions CMP modules in commonMain:commonMain.dependencies {
implementation(projects.kPermissionsCore)
implementation(projects.kPermissionsCamera)
implementation(projects.kPermissionsGallery)
implementation(projects.kPermissionsNotification)
implementation(projects.kPermissionsCMP)
}iosAppCMP/iosApp/iOSApp.swift — CMP iOS entry point:@main
struct iOSApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
}Android: androidSimple depends on sharedUI; no extra Swift setup.
In CMP apps (e.g. iosAppCMP, androidSimple via sharedUI), use rememberPermissionState:
val permission = rememberPermissionState(GalleryPermission)
LaunchedEffect(Unit) {
permission.launchPermissionRequest { granted ->
if (granted) {
// ✅ Permission granted
} else {
// ❌ Permission denied
}
}
}On iOS, the CMP UI is hosted via ComposeView (see iosAppCMP/iosApp/ContentView.swift).
| App | Entry Point | UI |
|---|---|---|
| iosApp (NativeUI) |
iOSApp.swift → WindowGroup { PermissionsView() }
|
Native SwiftUI + SharedCode |
| iosAppCMP (KPermissionsApp) |
iOSApp.swift → WindowGroup { ContentView() }
|
Compose Multiplatform via ComposeView
|
If you are using the library in a native iOS project with SwiftUI (like iosApp), you can use
IOSPermissionManager to handle permissions:
import SwiftUI
import SharedCode
struct PermissionsView: View {
@State private var cameraStatus: String = "غير معروف"
@State private var notificationStatus: String = "لم يتم الطلب"
@State private var galleryStatus: String = "غير معروف"
var body: some View {
ScrollView {
VStack(spacing: 25) {
Text("إدارة التصاريح")
.font(.largeTitle)
.fontWeight(.bold)
.padding(.top)
// قسم الإشعارات
PermissionCard(
title: "الإشعارات",
status: notificationStatus,
icon: "bell.badge.fill",
color: .green,
action: { requestNotifications() }
)
// قسم الكاميرا
PermissionCard(
title: "الكاميرا",
status: cameraStatus,
icon: "camera.fill",
color: .blue,
action: { requestCamera() }
)
// قسم المعرض
PermissionCard(
title: "معرض الصور",
status: galleryStatus,
icon: "photo.on.rectangle.angled",
color: .purple,
action: { requestGallery() }
)
Spacer()
}
.padding()
}
.onAppear {
requestNotifications()
}
}
// --- الدوال الخاصة بطلب التصاريح ---
private func requestCamera() {
Task {
do {
let status = try await IOSPermissionManager.shared.requestPermission(permission: CameraPermission())
updateStatusText(status: status, for: "camera")
handlePermissionResult(status: status)
} catch {
print(error)
}
}
}
private func requestNotifications() {
Task {
do {
let status = try await IOSPermissionManager.shared.requestPermission(permission: NotificationPermission())
updateStatusText(status: status, for: "notification")
handlePermissionResult(status: status)
} catch {
print(error)
}
}
}
private func requestGallery() {
Task {
do {
let status = try await IOSPermissionManager.shared.requestPermission(permission: GalleryPermission())
updateStatusText(status: status, for: "gallery")
handlePermissionResult(status: status)
} catch {
print(error)
}
}
}
// دالة لتحديث النصوص المعروضة في الواجهة بالعربي
private func updateStatusText(status: PermissionStatus, for type: String) {
let text: String
switch status {
case is PermissionStatusGranted:
text = "مسموح"
case is PermissionStatusDenied:
text = "مرفوض"
case is PermissionStatusDeniedPermanently:
text = "مرفوض نهائياً"
case is PermissionStatusUnavailable:
text = "غير متوفر"
default:
text = "غير معروف"
}
// تحديث المتغير الصحيح بناءً على النوع
DispatchQueue.main.async {
if type == "camera" {
self.cameraStatus = text
} else if type == "notification" {
self.notificationStatus = text
} else if type == "gallery" {
self.galleryStatus = text
}
}
}
private func handlePermissionResult(status: PermissionStatus) {
if status is PermissionStatusDeniedPermanently {
IOSPermissionManager.shared.openAppSettings()
}
}
}
// مكون فرعي للبطاقة
struct PermissionCard: View {
let title: String
let status: String
let icon: String
let color: Color
let action: () -> Void
var body: some View {
VStack(alignment: .leading, spacing: 12) {
HStack {
Image(systemName: icon)
.foregroundColor(color)
.font(.headline)
Text(title)
.font(.headline)
Spacer()
Text(status)
.font(.subheadline)
.foregroundColor(status == "مسموح" ? .green : (status == "مرفوض" ? .red : .gray))
}
Button(action: action) {
Label("طلب إذن \(title)", systemImage: icon)
.font(.subheadline)
.fontWeight(.semibold)
.frame(maxWidth: .infinity)
.padding(.vertical, 12)
.background(color)
.foregroundColor(.white)
.cornerRadius(10)
}
}
.padding()
.background(Color(.secondarySystemBackground))
.cornerRadius(12)
}
}import io.github.kPermissions_api.Permission
expect object RecordAudioPermission : Permissionimport android.Manifest
import io.github.kPermissions_api.Permission
actual object RecordAudioPermission : Permission {
override val name: String = "record_audio"
override val androidPermissionName: String = Manifest.permission.RECORD_AUDIO
}import io.github.kPermissions_api.Permission
import io.github.kPermissions_api.PermissionStatus
import platform.AVFoundation.*
actual object RecordAudioPermission : Permission {
override val name: String = "record_audio"
override val permissionRequest: ((Boolean) -> Unit) -> Unit
get() = { callback ->
AVAudioSession.sharedInstance().requestRecordPermission { granted ->
dispatch_async(dispatch_get_main_queue()) { callback(granted) }
}
}
override fun getPermissionStatus(): PermissionStatus {
return when (AVAudioSession.sharedInstance().recordPermission) {
AVAudioSessionRecordPermissionGranted -> PermissionStatus.Granted
AVAudioSessionRecordPermissionDenied -> PermissionStatus.DeniedPermanently
AVAudioSessionRecordPermissionUndetermined -> PermissionStatus.Denied
else -> PermissionStatus.Denied
}
}
}MIT License