BS
BleepingSwift
Published on

> Building Child-Safe Communication Features with PermissionKit

Authors
  • avatar
    Name
    Mick MacCallum
    Twitter
    @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.

subscribe.sh

// Stay Updated

Get notified when I publish new tutorials on Swift, SwiftUI, and iOS development. No spam, unsubscribe anytime.

>

By subscribing, you agree to our Privacy Policy.