permissionkit
dpearson2699/swift-ios-skills
Request parental permission for child communication in iOS apps using PermissionKit.
What is permissionkit?
PermissionKit enables child-safe communication by letting children request parental approval for contact interactions. Use it when building apps that involve child-to-contact communication, need to enforce communication limits, or require parent/guardian approval for minors' messaging and calling actions.
- Check which communication handles (phone numbers, emails, custom IDs) are known to the system via CommunicationLimits
- Create PermissionQuestion objects describing the communication action (message, call, chat, follow, etc.) and contacts involved
- Request permission through AskCenter to send questions to parents/guardians via iMessage
- Receive PermissionResponse objects with parent approval or denial decisions
- Integrate permission flows into SwiftUI with PermissionButton component
- Handle communication limit enforcement and error states (limits disabled, contact sync not setup, etc.)
How to install permissionkit
npx skills add https://github.com/dpearson2699/swift-ios-skills --skill permissionkit- Swift 6.3 or later
- iOS 26.0+ (core APIs), iOS 26.1+ (AskError), iOS 26.2+ (AskCenter, PermissionButton)
- Valid app bundle identifier
- Communication limits enabled on the device
- Contact sync configured (for full functionality)
How to use permissionkit
- 1.Import PermissionKit at the top of your file
- 2.Check if a communication handle is known using CommunicationLimits.current.isKnownHandle() or knownHandles(in:)
- 3.Create a CommunicationHandle with the contact's phone number, email, or custom identifier
- 4.Build a PermissionQuestion with the handle(s) and optionally add PersonInformation with name and avatar
- 5.Call AskCenter.shared.ask(question, in:) to initiate the permission request flow in a UIViewController or use PermissionButton in SwiftUI
- 6.Observe responses using AskCenter.shared.responses(for:) to receive the parent's approval or denial decision
- 7.Handle AskError cases (communicationLimitsNotEnabled, contactSyncNotSetup, invalidQuestion, etc.) to gracefully degrade when PermissionKit is unavailable
Use cases
- Child wants to message or call a contact not yet approved by parent; app requests parental permission before allowing the action
- App enforces communication limits and needs to check if a handle is known before prompting for permission
- Child messaging app needs to display rich permission prompts with contact names and avatars for parent review
- App must handle parent denial and restrict communication to approved contacts only
- Significant app update requires re-requesting permissions for new communication actions or contacts
- iOS app developers building child-safe communication features
- Parental control and family safety app developers
- Messaging and social apps targeting families with minors
- Developers implementing communication limit enforcement for regulated environments
permissionkit FAQ
PermissionKit is available on iOS 26.0+, iPadOS 26.0+, Mac Catalyst 26.0+, macOS 26.0+, and visionOS 26.0+. Some APIs like AskCenter and PermissionButton require iOS 26.2+.
No. PermissionKit is specifically for parent/guardian approval flows in child communication scenarios. It is not a general contact permission, moderation, or chat-safety framework.
Permission questions are sent to parents/guardians via iMessage. Parents review and approve or deny the request through the iMessage interface, and responses are delivered to your app through AskCenter.responses(for:).
If AskCenter.shared.ask() throws AskError.communicationLimitsNotEnabled, communication limits are not active on the device. Handle this by continuing with your normal app flow without requiring permission.
PermissionQuestion supports rich display via CommunicationTopic.PersonInformation, which includes contact name components and avatar images. The system controls the final prompt presentation.
Full instructions (SKILL.md)
Source of truth, from dpearson2699/swift-ios-skills.
name: permissionkit description: "Create child communication safety experiences using PermissionKit to request parental permission for children. Use when building apps that involve child-to-contact communication, need to check communication limits, request parent/guardian approval, or handle permission responses for minors."
PermissionKit
Note: PermissionKit APIs span multiple 26.x releases. Verify signatures and availability against the current Xcode 26 SDK before shipping.
Request permission from a parent or guardian to modify a child's communication rules. PermissionKit creates communication safety experiences that let children ask for exceptions to communication limits set by their parents. Targets Swift 6.3 / iOS 26+.
PermissionKit communication experiences are available only through iMessage. Use it for parent/guardian approval flows, not as a general in-app contact permission, moderation, or chat-safety framework.
Contents
- Setup
- Core Concepts
- Checking Communication Limits
- Creating Permission Questions
- Requesting Permission with AskCenter
- SwiftUI Integration with PermissionButton
- Handling Responses
- Significant App Update Topic
- Common Mistakes
- Review Checklist
- References
Setup
Import PermissionKit. Do not invent PermissionKit entitlement keys; verify
current Apple documentation and Xcode capabilities before adding signing
requirements.
import PermissionKit
Platform availability:
When reviewing or correcting code, state these exact tiers instead of collapsing PermissionKit to "iOS 26+":
- Core topic, handle, question, response, choice, and
CommunicationLimitsAPIs: iOS 26.0+, iPadOS 26.0+, Mac Catalyst 26.0+, macOS 26.0+, visionOS 26.0+. AskError: iOS 26.1+, iPadOS 26.1+, Mac Catalyst 26.1+, macOS 26.1+, visionOS 26.1+.AskCenter,AskCenter.ask(_:in:),AskCenter.responses(for:),PermissionButton, andSignificantAppUpdateTopic: iOS/iPadOS/ Mac Catalyst/macOS/visionOS 26.2+.
Core Concepts
PermissionKit manages a flow where:
- A child encounters a communication limit in your app
- Your app creates a
PermissionQuestiondescribing the request - The system presents the question to the child for them to send to their parent
- The parent reviews and approves or denies the request
- Your app receives a
PermissionResponsewith the parent's decision
Key Types
| Type | Role |
|---|---|
AskCenter | Singleton that manages permission requests and responses |
PermissionQuestion | Describes the permission being requested |
PermissionResponse | The parent's decision (approval or denial) |
PermissionChoice | The specific answer (approve/decline) |
PermissionButton | SwiftUI button that triggers the permission flow |
CommunicationTopic | Topic for communication-related permission requests |
CommunicationHandle | A phone number, email, or custom identifier |
CommunicationLimits | Checks which communication handles are known to the system |
SignificantAppUpdateTopic | Topic for significant app update permission requests |
Checking Communication Limits
Use CommunicationLimits.current to check whether the system already knows a
communication handle for your app. This is not an "are communication limits
enabled?" probe. If limits are not enabled, AskCenter.shared.ask(_:in:)
throws AskError.communicationLimitsNotEnabled; handle that path when asking.
knownHandles(in:) also requires the calling app to have a non-nil, nonempty
bundle identifier. Corrected code should guard Bundle.main.bundleIdentifier
before calling it.
import PermissionKit
func needsPermissionPrompt(for handle: CommunicationHandle) async -> Bool {
let limits = CommunicationLimits.current
let isKnown = await limits.isKnownHandle(handle)
return !isKnown
}
// Check multiple handles at once.
func filterKnownHandles(_ handles: Set<CommunicationHandle>) async -> Set<CommunicationHandle> {
guard Bundle.main.bundleIdentifier?.isEmpty == false else { return [] }
let limits = CommunicationLimits.current
return await limits.knownHandles(in: handles)
}
Creating Communication Handles
let phoneHandle = CommunicationHandle(
value: "+1234567890",
kind: .phoneNumber
)
let emailHandle = CommunicationHandle(
value: "friend@example.com",
kind: .emailAddress
)
let customHandle = CommunicationHandle(
value: "user123",
kind: .custom
)
Creating Permission Questions
Build a PermissionQuestion with the contact information and communication
action type.
// Question for a single contact
let handle = CommunicationHandle(value: "+1234567890", kind: .phoneNumber)
let question = PermissionQuestion<CommunicationTopic>(handle: handle)
// Question for multiple contacts
let handles = [
CommunicationHandle(value: "+1234567890", kind: .phoneNumber),
CommunicationHandle(value: "friend@example.com", kind: .emailAddress)
]
let multiQuestion = PermissionQuestion<CommunicationTopic>(handles: handles)
Using CommunicationTopic with Person Information
Provide display names and avatars for a richer permission prompt.
let personInfo = CommunicationTopic.PersonInformation(
handle: CommunicationHandle(value: "+1234567890", kind: .phoneNumber),
nameComponents: {
var name = PersonNameComponents()
name.givenName = "Alex"
name.familyName = "Smith"
return name
}(),
avatarImage: nil
)
let topic = CommunicationTopic(
personInformation: [personInfo],
actions: [.message, .audioCall]
)
let question = PermissionQuestion<CommunicationTopic>(communicationTopic: topic)
Communication Actions
| Action | Description |
|---|---|
.message | Text messaging |
.audioCall | Voice call |
.videoCall | Video call |
.call | Generic call |
.chat | Chat communication |
.follow | Follow a user |
.beFollowed | Allow being followed |
.friend | Friend request |
.connect | Connection request |
.communicate | Generic communication |
Requesting Permission with AskCenter
Use AskCenter.shared to request that the child send the permission question
to their parent or guardian. The async ask call starts the send flow; parent
decisions arrive later through responses(for:). If the child cancels the send
flow, the system does not deliver a PermissionResponse for that question.
import PermissionKit
func requestPermission(
for question: PermissionQuestion<CommunicationTopic>,
in viewController: UIViewController
) async {
do {
try await AskCenter.shared.ask(question, in: viewController)
// Question send flow was started; wait for responses(for:) separately.
} catch let error as AskError {
switch error {
case .communicationLimitsNotEnabled:
// Communication limits not active -- continue with normal app flow.
break
case .contactSyncNotSetup:
// Contact sync not configured
break
case .invalidQuestion:
// Question is malformed
break
case .notAvailable:
// PermissionKit not available on this device
break
case .systemError(let underlying):
print("System error: \(underlying)")
case .unknown:
break
@unknown default:
break
}
}
}
SwiftUI Integration with PermissionButton
PermissionButton is a SwiftUI view that triggers the permission flow when
tapped. It uses the same response model as AskCenter: observe responses and
model a pending/canceled state instead of assuming every tap produces a parent
decision.
import SwiftUI
import PermissionKit
struct ContactPermissionView: View {
let handle = CommunicationHandle(value: "+1234567890", kind: .phoneNumber)
var body: some View {
let question = PermissionQuestion<CommunicationTopic>(handle: handle)
PermissionButton(question: question) {
Label("Ask to Message", systemImage: "message")
}
}
}
For richer SwiftUI flows, custom topics, and long-lived managers, read references/permissionkit-patterns.md.
Handling Responses
Listen for permission responses asynchronously. Track pending questions by
question.id, and give the UI a retry or expiration path because a child can
cancel the iMessage send flow without producing a response.
When combining known-handle checks with response handling, carry forward the
bundle-identifier guard from knownHandles(in:).
enum PermissionRequestState {
case pending, approved, denied, expired
}
var requestStates: [UUID: PermissionRequestState] = [:]
func expireIfStillPending(_ id: UUID) {
guard requestStates[id] == .pending else { return }
requestStates[id] = .expired
// Re-enable asking or show retry/canceled UI.
}
func observeResponses() async {
let responses = AskCenter.shared.responses(for: CommunicationTopic.self)
for await response in responses {
let choice = response.choice
let question = response.question
switch choice.answer {
case .approval:
// Parent approved -- enable communication
requestStates[question.id] = .approved
print("Approved for topic: \(question.topic)")
case .denial:
// Parent denied -- keep restriction
requestStates[question.id] = .denied
print("Denied")
@unknown default:
break
}
}
}
PermissionChoice Properties
let choice: PermissionChoice = response.choice
print("Answer: \(choice.answer)") // .approval or .denial
print("Choice ID: \(choice.id)")
print("Title: \(choice.title)")
// Convenience statics
let approved = PermissionChoice.approve
let declined = PermissionChoice.decline
Significant App Update Topic
Request permission for significant app updates that require parental approval. Your app determines what counts as significant based on applicable regulations and should consult qualified legal counsel for compliance interpretation. Use concise, understandable descriptions that state the concrete change parents are approving.
let updateTopic = SignificantAppUpdateTopic(
description: "This update adds multiplayer chat features"
)
let question = PermissionQuestion<SignificantAppUpdateTopic>(
significantAppUpdateTopic: updateTopic
)
// Present the question
try await AskCenter.shared.ask(question, in: viewController)
requestStates[question.id] = .pending
scheduleExpiration(for: question.id)
// Listen for responses
for await response in AskCenter.shared.responses(for: SignificantAppUpdateTopic.self) {
switch response.choice.answer {
case .approval:
// Proceed with update
requestStates[response.question.id] = .approved
case .denial:
// Skip update
requestStates[response.question.id] = .denied
@unknown default:
break
}
}
// If no response arrives before your pending window expires, keep the update
// blocked or offer a retry. Child cancellation produces no denial response.
Common Mistakes
DON'T: Treat known-handle checks as enabled-limits checks
isKnownHandle(_:) and knownHandles(in:) only classify handles. They do not
replace handling .communicationLimitsNotEnabled from ask(_:in:).
// WRONG: Assuming a handle lookup proves active limits
let isKnown = await CommunicationLimits.current.isKnownHandle(handle)
if !isKnown {
try await AskCenter.shared.ask(question, in: viewController)
}
// CORRECT: Handle the case where limits are not enabled
do {
try await AskCenter.shared.ask(question, in: viewController)
} catch AskError.communicationLimitsNotEnabled {
// Communication limits not active -- continue with normal app flow.
allowCommunication()
} catch {
handleError(error)
}
DON'T: Ignore AskError cases
Each error case requires different handling.
// WRONG: Catch-all with no user feedback
do {
try await AskCenter.shared.ask(question, in: viewController)
} catch {
print(error)
}
// CORRECT: Handle each case
do {
try await AskCenter.shared.ask(question, in: viewController)
} catch let error as AskError {
switch error {
case .communicationLimitsNotEnabled:
allowCommunication()
case .contactSyncNotSetup:
showContactSyncPrompt()
case .invalidQuestion:
showInvalidQuestionAlert()
case .notAvailable:
showUnavailableMessage()
case .systemError(let underlying):
showSystemError(underlying)
case .unknown:
showGenericError()
@unknown default:
break
}
}
DON'T: Create questions with empty handles
A question with no handles or person information is invalid.
// WRONG: Empty handles array
let question = PermissionQuestion<CommunicationTopic>(handles: []) // Invalid
// CORRECT: Provide at least one handle
let handle = CommunicationHandle(value: "+1234567890", kind: .phoneNumber)
let question = PermissionQuestion<CommunicationTopic>(handle: handle)
DON'T: Forget to observe responses and pending states
Presenting a question without listening for the response means you never know if the parent approved. A child can also cancel the send flow, so do not wait forever for a response to every question.
// WRONG: Fire and forget
try await AskCenter.shared.ask(question, in: viewController)
// CORRECT: Observe responses
Task {
for await response in AskCenter.shared.responses(for: CommunicationTopic.self) {
handleResponse(response)
}
}
try await AskCenter.shared.ask(question, in: viewController)
DON'T: Use deprecated CommunicationLimitsButton
Use PermissionButton instead of the deprecated CommunicationLimitsButton.
// WRONG: Deprecated
CommunicationLimitsButton(question: question) {
Text("Ask Permission")
}
// CORRECT: Use PermissionButton
PermissionButton(question: question) {
Text("Ask Permission")
}
Review Checklist
- iMessage-only routing understood before choosing PermissionKit
- Corrected guidance states exact availability tiers: core communication
types/limits 26.0+,
AskError26.1+, andAskCenter/PermissionButton/ responses/significant-update topics 26.2+ -
AskError.communicationLimitsNotEnabledhandled to allow fallback -
AskErrorcases handled individually with appropriate user feedback -
CommunicationHandlecreated with correctKind(phone, email, custom) - Known-handle examples guard a non-nil, nonempty bundle identifier before
knownHandles(in:) - Known-handle checks are not treated as active-limits checks
-
PermissionQuestionincludes at least one handle or person information -
AskCenter.shared.responses(for:)observed to receive parent decisions -
PermissionButtonused instead of deprecatedCommunicationLimitsButton - Person information includes name components for a clear permission prompt
- Communication actions match the app's actual communication capabilities
- Pending/canceled/expired question states handled when no response arrives
- Response handling updates UI on the main actor
- Error states provide clear guidance to the user
References
- Extended patterns (response handling, multi-topic, UIKit): references/permissionkit-patterns.md
- PermissionKit framework
- AskCenter
- PermissionQuestion
- PermissionButton
- PermissionResponse
- CommunicationTopic
- CommunicationHandle
- CommunicationLimits
- SignificantAppUpdateTopic
- AskError
- Creating a communication experience
Related skills
More from dpearson2699/swift-ios-skills and the wider catalog.

photokit
Photo picking, camera capture, and media handling for iOS apps using PhotoKit and AVFoundation.

push-notifications
Implement and debug local and remote push notifications in iOS/macOS apps with APNs, rich content, and actions.

realitykit
Build iOS AR and 3D experiences with RealityKit and ARKit integration.

relevancekit
Increase Apple Watch widget visibility using contextual relevance signals for time, location, fitness, and hardware.

scenekit
Maintain and extend existing SceneKit 3D scenes; soft-deprecated, use for legacy apps only.

sensorkit
Access research-grade sensor data from iOS/watchOS for Apple-approved studies.