Swift-ios-skills permissionkit
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.
git clone https://github.com/dpearson2699/swift-ios-skills
T=$(mktemp -d) && git clone --depth=1 https://github.com/dpearson2699/swift-ios-skills "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/permissionkit" ~/.claude/skills/dpearson2699-swift-ios-skills-permissionkit && rm -rf "$T"
skills/permissionkit/SKILL.mdPermissionKit
Note: PermissionKit is new in iOS 26. Method signatures should be verified against the latest Xcode 26 beta SDK.
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+.
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. No special entitlements are required.
import PermissionKit
Platform availability: iOS 26+, iPadOS 26+, macOS 26+.
Core Concepts
PermissionKit manages a flow where:
- A child encounters a communication limit in your app
- Your app creates a
describing the requestPermissionQuestion - 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
with the parent's decisionPermissionResponse
Key Types
| Type | Role |
|---|---|
| Singleton that manages permission requests and responses |
| Describes the permission being requested |
| The parent's decision (approval or denial) |
| The specific answer (approve/decline) |
| SwiftUI button that triggers the permission flow |
| Topic for communication-related permission requests |
| A phone number, email, or custom identifier |
| Checks whether communication limits apply |
| Topic for significant app update permission requests |
Checking Communication Limits
Before presenting a permission request, check if communication limits are enabled and whether the handle is known.
import PermissionKit func checkCommunicationStatus(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> { 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 |
|---|---|
| Text messaging |
| Voice call |
| Video call |
| Generic call |
| Chat communication |
| Follow a user |
| Allow being followed |
| Friend request |
| Connection request |
| Generic communication |
Requesting Permission with AskCenter
Use
AskCenter.shared to present the permission request to the child.
import PermissionKit func requestPermission( for question: PermissionQuestion<CommunicationTopic>, in viewController: UIViewController ) async { do { try await AskCenter.shared.ask(question, in: viewController) // Question was presented to the child } catch let error as AskError { switch error { case .communicationLimitsNotEnabled: // Communication limits not active -- no permission needed 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.
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") } } }
PermissionButton with Custom Topic
struct CustomPermissionView: View { var body: some View { let personInfo = CommunicationTopic.PersonInformation( handle: CommunicationHandle(value: "user456", kind: .custom), nameComponents: nil, avatarImage: nil ) let topic = CommunicationTopic( personInformation: [personInfo], actions: [.follow] ) let question = PermissionQuestion<CommunicationTopic>( communicationTopic: topic ) PermissionButton(question: question) { Text("Ask to Follow") } } }
Handling Responses
Listen for permission responses asynchronously.
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 print("Approved for topic: \(question.topic)") case .denial: // Parent denied -- keep restriction 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.
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) // Listen for responses for await response in AskCenter.shared.responses(for: SignificantAppUpdateTopic.self) { switch response.choice.answer { case .approval: // Proceed with update break case .denial: // Skip update break @unknown default: break } }
Common Mistakes
DON'T: Skip checking if communication limits are enabled
If communication limits are not enabled, calling
ask throws
.communicationLimitsNotEnabled. Check first or handle the error.
// WRONG: Assuming limits are always active 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 -- allow communication directly 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
Presenting a question without listening for the response means you never know if the parent approved.
// 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
-
handled to allow fallbackAskError.communicationLimitsNotEnabled -
cases handled individually with appropriate user feedbackAskError -
created with correctCommunicationHandle
(phone, email, custom)Kind -
includes at least one handle or person informationPermissionQuestion -
observed to receive parent decisionsAskCenter.shared.responses(for:) -
used instead of deprecatedPermissionButtonCommunicationLimitsButton - Person information includes name components for a clear permission prompt
- Communication actions match the app's actual communication capabilities
- 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