Agent skills for iOS, iPadOS, Swift, SwiftUI, and modern Apple framework development.
90
90%
Does it follow best practices?
Impact
—
Average score across 248 eval scenarios
Advisory
Suggest reviewing before use
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.
Import PermissionKit. Do not invent PermissionKit entitlement keys; verify
current Apple documentation and Xcode capabilities before adding signing
requirements.
import PermissionKitPlatform availability:
When reviewing or correcting code, state these exact tiers instead of collapsing PermissionKit to "iOS 26+":
CommunicationLimits
APIs: 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, and SignificantAppUpdateTopic: iOS/iPadOS/
Mac Catalyst/macOS/visionOS 26.2+.PermissionKit manages a flow where:
PermissionQuestion describing the requestPermissionResponse with the parent's decision| 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 |
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)
}let phoneHandle = CommunicationHandle(
value: "+1234567890",
kind: .phoneNumber
)
let emailHandle = CommunicationHandle(
value: "friend@example.com",
kind: .emailAddress
)
let customHandle = CommunicationHandle(
value: "user123",
kind: .custom
)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)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)| 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 |
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
}
}
}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.
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
}
}
}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.declineRequest 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.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)
}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
}
}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)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)Use PermissionButton instead of the deprecated CommunicationLimitsButton.
// WRONG: Deprecated
CommunicationLimitsButton(question: question) {
Text("Ask Permission")
}
// CORRECT: Use PermissionButton
PermissionButton(question: question) {
Text("Ask Permission")
}AskError 26.1+, and AskCenter/PermissionButton/
responses/significant-update topics 26.2+AskError.communicationLimitsNotEnabled handled to allow fallbackAskError cases handled individually with appropriate user feedbackCommunicationHandle created with correct Kind (phone, email, custom)knownHandles(in:)PermissionQuestion includes at least one handle or person informationAskCenter.shared.responses(for:) observed to receive parent decisionsPermissionButton used instead of deprecated CommunicationLimitsButton.tessl-plugin
skills
accessorysetupkit
references
activitykit
references
adattributionkit
references
alarmkit
references
app-clips
app-intents
references
app-store-optimization
app-store-review
apple-on-device-ai
appmigrationkit
references
audioaccessorykit
references
authentication
references
avkit
references
background-processing
references
browserenginekit
references
callkit
references
carplay
references
cloudkit
references
contacts-framework
references
core-bluetooth
references
core-data
core-motion
references
core-nfc
references
coreml
references
cryptokit
references
cryptotokenkit
references
debugging-instruments
device-integrity
references
dockkit
references
energykit
references
eventkit
references
financekit
references
focus-engine
gamekit
references
healthkit
references
homekit
references
ios-accessibility
ios-localization
ios-networking
ios-simulator
references
mapkit
metrickit
references
musickit
references
natural-language
references
paperkit
references
passkit
references
pdfkit
references
pencilkit
references
permissionkit
references
photokit
push-notifications
realitykit
references
relevancekit
references
scenekit
references
sensorkit
references
speech-recognition
references
spritekit
references
storekit
swift-api-design-guidelines
swift-architecture
swift-charts
references
swift-codable
swift-concurrency
swift-formatstyle
swift-language
swift-security
references
swift-testing
swiftdata
swiftlint
swiftui-animation
swiftui-gestures
references
swiftui-layout-components
swiftui-liquid-glass
references
swiftui-patterns
swiftui-performance
swiftui-uikit-interop
swiftui-webkit
tabletopkit
references
tipkit
references
vision-framework
weatherkit
references
widgetkit
references