- Published on
> Building Child-Safe Communication Features with PermissionKit
- Authors

- Name
- Mick MacCallum
- @0x7fs
If you're building a social app, game with chat features, or any communication platform, you now have a straightforward way to integrate with Apple's parental controls. PermissionKit, introduced in iOS 26, lets children send permission requests to their parents through Messages when they want to communicate with someone new. The parent can approve or decline right from the iMessage conversation, and your app gets notified of the decision.
This approach is less intrusive than building your own parental approval system. You're leveraging the family structure already established through Family Sharing, and the permission flow happens through the familiar Messages interface rather than requiring parents to log into your app.
How PermissionKit Works
The framework connects to Communication Limits, a Screen Time feature that parents enable to control who their child can contact. When Communication Limits is active, the child's device maintains a list of known contacts. Your app can check whether conversation participants are on that list and hide content from unknown senders until permission is granted.
When a child wants to communicate with someone new, your app presents a system UI that opens a pre-addressed iMessage to the parents. The parent receives a styled message bubble showing who the child wants to contact, with approve and decline buttons right in the conversation. Once the parent responds, your app receives an update and can enable the communication.
Checking Known Contacts
Before showing message content or enabling communication, check if the participants are known to the system:
import PermissionKit
struct ConversationView: View {
let conversation: Conversation
@State private var participantsAreKnown = false
var body: some View {
Group {
if participantsAreKnown {
MessageList(messages: conversation.messages)
} else {
UnknownSenderView(conversation: conversation)
}
}
.task {
await checkParticipantStatus()
}
}
func checkParticipantStatus() async {
let knownHandles = await CommunicationLimits.current.knownHandles(
in: conversation.participantHandles
)
participantsAreKnown = knownHandles.isSuperset(of: conversation.participantHandles)
}
}
The knownHandles(in:) method performs an optimized lookup and returns the subset of handles that are approved contacts. If Communication Limits isn't enabled for the user, the API returns a sensible default that won't block normal app functionality.
Creating a Permission Request
When you need to request permission for unknown contacts, build a PermissionQuestion with information about who the child wants to communicate with:
import PermissionKit
func createPermissionRequest(for user: User) -> PermissionQuestion<CommunicationTopic> {
let handle = CommunicationHandle(value: user.username, kind: .custom)
var nameComponents = PersonNameComponents()
nameComponents.givenName = user.displayName
let personInfo = PersonInformation(
handle: handle,
nameComponents: nameComponents,
avatarImage: user.profileImage
)
var topic = CommunicationTopic(personInformation: [personInfo])
topic.actions = [.message]
return PermissionQuestion(communicationTopic: topic)
}
The CommunicationHandle represents the identifier for the contact in your system—a username, user ID, or whatever unique identifier your app uses. The kind parameter can be .custom for app-specific identifiers, or .phoneNumber and .email for standard contact types.
Including PersonInformation with a name and avatar helps parents make informed decisions. When they receive the permission request, they'll see the contact's profile picture and name rather than just an opaque username.
Presenting the Permission Request
In SwiftUI, use CommunicationLimitsButton to trigger the permission flow:
import PermissionKit
import SwiftUI
struct UserProfileView: View {
let user: User
@State private var canMessage = false
var body: some View {
VStack(spacing: 20) {
ProfileHeader(user: user)
if canMessage {
Button("Send Message") {
openConversation(with: user)
}
.buttonStyle(.borderedProminent)
} else {
let question = createPermissionRequest(for: user)
CommunicationLimitsButton(question: question) {
Label("Ask to Message", systemImage: "bubble.left.and.bubble.right")
}
.buttonStyle(.borderedProminent)
}
}
.task {
await checkMessagePermission()
}
}
func checkMessagePermission() async {
let handles = Set([CommunicationHandle(value: user.username, kind: .custom)])
let known = await CommunicationLimits.current.knownHandles(in: handles)
canMessage = known.contains(CommunicationHandle(value: user.username, kind: .custom))
}
}
When the child taps the button, the system presents a confirmation sheet, then opens Messages with a compose window addressed to the parents in the Family Sharing group. The child can add a personal note explaining why they want to connect with this person.
For UIKit, use the async ask method instead:
import PermissionKit
import UIKit
class ProfileViewController: UIViewController {
func requestPermission(for user: User) async {
let question = createPermissionRequest(for: user)
do {
try await CommunicationLimits.current.ask(question, in: self)
} catch {
// Handle error - user cancelled or system unavailable
}
}
}
Handling Permission Responses
After a parent approves or declines, your app needs to update its UI. Listen for updates using the async sequence on CommunicationLimits:
import PermissionKit
import SwiftUI
struct ConversationsListView: View {
@State private var conversations: [Conversation] = []
@State private var showingPermissionAlert = false
var body: some View {
List(conversations) { conversation in
ConversationRow(conversation: conversation)
}
.task {
await listenForPermissionUpdates()
}
.alert("Permission Updated", isPresented: $showingPermissionAlert) {
Button("OK") { }
} message: {
Text("A parent has responded to a communication request.")
}
}
func listenForPermissionUpdates() async {
for await update in CommunicationLimits.current.updates {
// Refresh conversation list to reflect new permissions
await refreshConversations()
showingPermissionAlert = true
}
}
}
When an update arrives, you'll want to re-check which contacts are now known and update your UI accordingly. The child will also receive a system notification about the parent's decision.
Requesting Permission for Multiple Contacts
If your app has group conversations or friend requests for multiple people, you can batch them into a single permission question:
func createGroupPermissionRequest(for users: [User]) -> PermissionQuestion<CommunicationTopic> {
let people = users.map { user in
var nameComponents = PersonNameComponents()
nameComponents.givenName = user.displayName
return PersonInformation(
handle: CommunicationHandle(value: user.username, kind: .custom),
nameComponents: nameComponents,
avatarImage: user.profileImage
)
}
var topic = CommunicationTopic(personInformation: people)
topic.actions = [.message]
return PermissionQuestion(communicationTopic: topic)
}
Parents will see all the contacts in a single request and can approve or decline the group.
Handling Different Action Types
The CommunicationTopic supports different action types depending on what your app offers:
var topic = CommunicationTopic(personInformation: [personInfo])
// For messaging apps
topic.actions = [.message]
// For video calling apps
topic.actions = [.video]
// For voice calling apps
topic.actions = [.call]
// For apps with multiple features
topic.actions = [.message, .call, .video]
Setting appropriate actions helps parents understand exactly what type of communication their child is requesting.
Hiding Content from Unknown Senders
Beyond just blocking communication, you should hide potentially sensitive content until permission is granted. Don't show message previews, profile pictures, or other content from unknown contacts:
struct MessagePreview: View {
let message: Message
let senderIsKnown: Bool
var body: some View {
HStack {
if senderIsKnown {
AsyncImage(url: message.sender.avatarURL) { image in
image.resizable()
} placeholder: {
Circle().fill(.gray)
}
.frame(width: 40, height: 40)
.clipShape(Circle())
VStack(alignment: .leading) {
Text(message.sender.displayName)
.font(.headline)
Text(message.preview)
.font(.subheadline)
.foregroundStyle(.secondary)
}
} else {
Circle()
.fill(.gray.opacity(0.3))
.frame(width: 40, height: 40)
VStack(alignment: .leading) {
Text("Unknown Sender")
.font(.headline)
Text("Ask permission to view this message")
.font(.subheadline)
.foregroundStyle(.secondary)
}
}
}
}
}
This protects children from seeing inappropriate content while still making them aware that a message exists.
Graceful Degradation
Not all users will have Communication Limits enabled or be part of a Family Sharing group. Design your app to work normally when PermissionKit features aren't applicable:
struct AdaptiveMessageButton: View {
let user: User
@State private var permissionState: PermissionState = .checking
enum PermissionState {
case checking
case permitted
case requiresPermission
case notApplicable
}
var body: some View {
switch permissionState {
case .checking:
ProgressView()
case .permitted, .notApplicable:
Button("Send Message") {
openConversation(with: user)
}
case .requiresPermission:
CommunicationLimitsButton(question: createPermissionRequest(for: user)) {
Label("Ask to Message", systemImage: "bubble.left.and.bubble.right")
}
}
}
}
When Communication Limits isn't configured, the knownHandles API returns results indicating unrestricted access, so your permission checks will pass and the app functions normally for adult users.
Testing Without a Family
During development, you can test PermissionKit behavior using the Xcode simulator with test accounts configured in a Family Sharing group. Apple provides test family configurations in App Store Connect's sandbox environment that you can use without setting up real family relationships.
The APIs are designed to return sensible defaults when family features aren't available, so you can also develop most of your integration on a device that isn't part of a family group—just keep in mind you won't see the full permission flow until you test with an actual family configuration.
PermissionKit makes it straightforward to respect parental controls in communication apps. By integrating with the system-level permission flow rather than building custom parental approval systems, you give families a consistent experience across all the apps their children use.
// Continue_Learning
Detecting Low Power Mode in SwiftUI and Adapting UI Performance
Learn how to detect when users enable Low Power Mode and automatically reduce animations, refresh rates, and visual effects to preserve battery life in your SwiftUI app.
Supporting Dark Mode in a SwiftUI App
Learn how to properly support dark mode in SwiftUI using semantic colors, adaptive color assets, and color scheme detection.
Using SF Symbols in SwiftUI
Learn how to use SF Symbols in your SwiftUI apps, including sizing, coloring, animations, and finding the right symbol for your needs.
// Stay Updated
Get notified when I publish new tutorials on Swift, SwiftUI, and iOS development. No spam, unsubscribe anytime.