CtrlK
BlogDocsLog inGet started
Tessl Logo

dpearson2699/swift-ios-skills

Agent skills for iOS, iPadOS, Swift, SwiftUI, and modern Apple framework development.

71

Quality

89%

Does it follow best practices?

Impact

No eval scenarios have been run

SecuritybySnyk

Advisory

Suggest reviewing before use

Overview
Quality
Evals
Security
Files

permissionkit-patterns.mdskills/permissionkit/references/

PermissionKit Extended Patterns

Overflow reference for the permissionkit skill. Contains advanced patterns that exceed the main skill file's scope.

Contents

  • Full UIKit Integration
  • Response Observer Manager
  • Multi-Contact Permission Flow
  • Communication Limits Checking Pattern
  • SwiftUI Full-Screen Permission Flow
  • macOS Integration
  • Error Recovery Patterns

Full UIKit Integration

Complete UIKit view controller with permission request and response handling.

import UIKit
import PermissionKit

class ContactViewController: UIViewController {
    private var responseTask: Task<Void, Never>?

    override func viewDidLoad() {
        super.viewDidLoad()
        startObservingResponses()
    }

    deinit {
        responseTask?.cancel()
    }

    func requestPermissionToMessage(_ contact: Contact) {
        let personInfo = CommunicationTopic.PersonInformation(
            handle: CommunicationHandle(
                value: contact.phoneNumber,
                kind: .phoneNumber
            ),
            nameComponents: contact.nameComponents,
            avatarImage: contact.avatarCGImage
        )

        let topic = CommunicationTopic(
            personInformation: [personInfo],
            actions: [.message]
        )

        let question = PermissionQuestion<CommunicationTopic>(
            communicationTopic: topic
        )

        Task {
            do {
                try await AskCenter.shared.ask(question, in: self)
                showPendingState(for: contact)
            } catch AskError.communicationLimitsNotEnabled {
                enableMessaging(for: contact)
            } catch AskError.notAvailable {
                showFeatureUnavailable()
            } catch {
                showError(error)
            }
        }
    }

    private func startObservingResponses() {
        responseTask = Task { [weak self] in
            let responses = AskCenter.shared.responses(
                for: CommunicationTopic.self
            )
            for await response in responses {
                await MainActor.run {
                    self?.handleResponse(response)
                }
            }
        }
    }

    @MainActor
    private func handleResponse(_ response: PermissionResponse<CommunicationTopic>) {
        switch response.choice.answer {
        case .approval:
            let handles = response.question.topic.personInformation
                .map(\.handle)
            for handle in handles {
                enableCommunication(for: handle)
            }
        case .denial:
            let handles = response.question.topic.personInformation
                .map(\.handle)
            for handle in handles {
                showDeniedState(for: handle)
            }
        @unknown default:
            break
        }
    }

    private func showPendingState(for contact: Contact) { }
    private func enableMessaging(for contact: Contact) { }
    private func enableCommunication(for handle: CommunicationHandle) { }
    private func showDeniedState(for handle: CommunicationHandle) { }
    private func showFeatureUnavailable() { }
    private func showError(_ error: Error) { }
}

Response Observer Manager

Centralize response observation for apps with multiple permission flows.

import PermissionKit

@Observable
@MainActor
final class PermissionManager {
    static let shared = PermissionManager()

    var approvedHandles: Set<String> = []
    var deniedHandles: Set<String> = []
    var pendingQuestionIDs: Set<UUID> = []

    private var observerTask: Task<Void, Never>?

    private init() {
        startObserving()
    }

    deinit {
        observerTask?.cancel()
    }

    func askPermission(
        for handles: [CommunicationHandle],
        actions: Set<CommunicationTopic.Action>,
        in viewController: UIViewController
    ) async throws {
        let personInfo = handles.map { handle in
            CommunicationTopic.PersonInformation(
                handle: handle,
                nameComponents: nil,
                avatarImage: nil
            )
        }

        let topic = CommunicationTopic(
            personInformation: personInfo,
            actions: actions
        )

        let question = PermissionQuestion<CommunicationTopic>(
            communicationTopic: topic
        )

        try await AskCenter.shared.ask(question, in: viewController)
        pendingQuestionIDs.insert(question.id)
    }

    func isApproved(_ handleValue: String) -> Bool {
        approvedHandles.contains(handleValue)
    }

    func isDenied(_ handleValue: String) -> Bool {
        deniedHandles.contains(handleValue)
    }

    private func startObserving() {
        observerTask = Task { [weak self] in
            let responses = AskCenter.shared.responses(
                for: CommunicationTopic.self
            )
            for await response in responses {
                await MainActor.run {
                    self?.processResponse(response)
                }
            }
        }
    }

    private func processResponse(
        _ response: PermissionResponse<CommunicationTopic>
    ) {
        pendingQuestionIDs.remove(response.question.id)

        let handleValues = response.question.topic.personInformation
            .map(\.handle.value)

        switch response.choice.answer {
        case .approval:
            for value in handleValues {
                approvedHandles.insert(value)
                deniedHandles.remove(value)
            }
        case .denial:
            for value in handleValues {
                deniedHandles.insert(value)
            }
        @unknown default:
            break
        }
    }
}

Multi-Contact Permission Flow

Request permission for multiple contacts in a single question.

func requestGroupPermission(
    contacts: [Contact],
    in viewController: UIViewController
) async throws {
    let personInfoList = contacts.map { contact in
        CommunicationTopic.PersonInformation(
            handle: CommunicationHandle(
                value: contact.identifier,
                kind: .custom
            ),
            nameComponents: contact.nameComponents,
            avatarImage: contact.avatarCGImage
        )
    }

    let topic = CommunicationTopic(
        personInformation: personInfoList,
        actions: [.message, .audioCall, .videoCall]
    )

    let question = PermissionQuestion<CommunicationTopic>(
        communicationTopic: topic
    )

    // Check question properties
    print("Question ID: \(question.id)")
    print("Choices: \(question.choices.map(\.title))")
    print("Default choice: \(question.defaultChoice.title)")

    if let expiration = question.expirationDate {
        print("Expires: \(expiration)")
    }

    try await AskCenter.shared.ask(question, in: viewController)
}

Communication Limits Checking Pattern

Check handles before building the permission UI.

@Observable
@MainActor
final class ContactListViewModel {
    var contacts: [ContactItem] = []

    struct ContactItem: Identifiable {
        let id: String
        let name: String
        let handle: CommunicationHandle
        var isKnown: Bool = false
        var needsPermission: Bool = false
    }

    func refreshContactStatus() async {
        let limits = CommunicationLimits.current
        let allHandles = Set(contacts.map(\.handle))
        let knownHandles = await limits.knownHandles(in: allHandles)

        for i in contacts.indices {
            contacts[i].isKnown = knownHandles.contains(contacts[i].handle)
            contacts[i].needsPermission = !contacts[i].isKnown
        }
    }
}

SwiftUI Full-Screen Permission Flow

Build a complete permission flow in SwiftUI.

import SwiftUI
import PermissionKit

struct ContactDetailView: View {
    let contact: Contact
    @State private var permissionState: PermissionState = .unknown
    @Environment(PermissionManager.self) private var permissionManager

    enum PermissionState {
        case unknown, checking, needsPermission, approved, denied, error(String)
    }

    var body: some View {
        VStack {
            Text(contact.name)
                .font(.title)

            switch permissionState {
            case .unknown, .checking:
                ProgressView("Checking permissions...")

            case .needsPermission:
                let handle = CommunicationHandle(
                    value: contact.phoneNumber,
                    kind: .phoneNumber
                )
                let question = PermissionQuestion<CommunicationTopic>(
                    handle: handle
                )

                VStack {
                    Text("Permission needed to message this contact.")
                        .foregroundStyle(.secondary)
                    PermissionButton(question: question) {
                        Label("Ask to Message", systemImage: "message.badge.clock")
                    }
                    .buttonStyle(.borderedProminent)
                }

            case .approved:
                Label("Messaging enabled", systemImage: "checkmark.circle.fill")
                    .foregroundStyle(.green)

            case .denied:
                Label("Permission denied", systemImage: "xmark.circle.fill")
                    .foregroundStyle(.red)

            case .error(let message):
                Label(message, systemImage: "exclamationmark.triangle")
                    .foregroundStyle(.orange)
            }
        }
        .task {
            await checkPermission()
        }
    }

    private func checkPermission() async {
        permissionState = .checking
        let handle = CommunicationHandle(
            value: contact.phoneNumber,
            kind: .phoneNumber
        )
        let limits = CommunicationLimits.current
        let isKnown = await limits.isKnownHandle(handle)

        if isKnown {
            permissionState = .approved
        } else if permissionManager.isApproved(contact.phoneNumber) {
            permissionState = .approved
        } else if permissionManager.isDenied(contact.phoneNumber) {
            permissionState = .denied
        } else {
            permissionState = .needsPermission
        }
    }
}

macOS Integration

On macOS, pass an NSWindow instead of UIViewController.

#if os(macOS)
import AppKit
import PermissionKit

func requestPermission(
    for question: PermissionQuestion<CommunicationTopic>,
    in window: NSWindow
) async throws {
    try await AskCenter.shared.ask(question, in: window)
}
#endif

Error Recovery Patterns

Provide actionable recovery for each error type.

func handleAskError(_ error: AskError) -> (title: String, message: String, action: (() -> Void)?) {
    switch error {
    case .communicationLimitsNotEnabled:
        return (
            "No Restrictions",
            "Communication limits are not enabled. You can communicate freely.",
            nil
        )
    case .contactSyncNotSetup:
        return (
            "Contact Sync Required",
            "Please enable contact sync in Settings to use this feature.",
            { openContactSyncSettings() }
        )
    case .invalidQuestion:
        return (
            "Invalid Request",
            "The permission request could not be created. Please try again.",
            nil
        )
    case .notAvailable:
        return (
            "Not Available",
            "This feature is not available on this device.",
            nil
        )
    case .systemError(let underlying):
        return (
            "System Error",
            underlying.localizedDescription,
            nil
        )
    case .unknown:
        return (
            "Unknown Error",
            "An unexpected error occurred. Please try again later.",
            nil
        )
    @unknown default:
        return (
            "Error",
            "An error occurred.",
            nil
        )
    }
}

skills

permissionkit

CHANGELOG.md

README.md

tile.json