From b35f1b432fc24fbab1fa05593c9a0bca7e4d95a6 Mon Sep 17 00:00:00 2001 From: Christian Cleberg Date: Thu, 23 Apr 2026 23:12:34 -0500 Subject: feat(v3.6.0): add iCloud sharing for workflows and tracked domains - implement CloudKit sharing (CKShare) for TrackedDomain and DomainWorkflow - support read-only and editable permissions - add Share actions and shared state indicators in UI - handle shared record ownership and participant roles - implement deterministic conflict resolution to prevent duplication - ensure compatibility with existing iCloud sync layer - maintain local-first behavior with graceful offline handling - remove debug logging for domain availability/rdap messages notes: - no custom backend or accounts introduced - sharing is Apple ecosystem only (iCloud-based) - large history data remains local-only --- DomainDig/CloudSharingSheet.swift | 84 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 84 insertions(+) create mode 100644 DomainDig/CloudSharingSheet.swift (limited to 'DomainDig/CloudSharingSheet.swift') diff --git a/DomainDig/CloudSharingSheet.swift b/DomainDig/CloudSharingSheet.swift new file mode 100644 index 0000000..3d524ef --- /dev/null +++ b/DomainDig/CloudSharingSheet.swift @@ -0,0 +1,84 @@ +import CloudKit +import Foundation +import SwiftUI +import UIKit + +struct CloudSharingSheet: UIViewControllerRepresentable { + @Environment(\.dismiss) private var dismiss + + let entity: ShareableEntity + let title: String + + func makeCoordinator() -> Coordinator { + Coordinator(dismiss: dismiss, title: title) + } + + func makeUIViewController(context: Context) -> UIViewController { + UIViewController() + } + + func updateUIViewController(_ uiViewController: UIViewController, context: Context) { + context.coordinator.presentIfNeeded(from: uiViewController, entity: entity) + } + + final class Coordinator: NSObject, UIAdaptivePresentationControllerDelegate { + private let dismissAction: DismissAction + private let title: String + private let observer = CKSystemSharingUIObserver(container: .default()) + private var hasPresented = false + + init(dismiss: DismissAction, title: String) { + self.dismissAction = dismiss + self.title = title + super.init() + + observer.systemSharingUIDidSaveShareBlock = { _, _ in + Task { @MainActor in + await CloudSyncService.shared.syncNow(trigger: .manual) + } + } + + observer.systemSharingUIDidStopSharingBlock = { _, _ in + Task { @MainActor in + await CloudSyncService.shared.syncNow(trigger: .manual) + } + } + } + + func presentIfNeeded(from presenter: UIViewController, entity: ShareableEntity) { + guard !hasPresented else { return } + hasPresented = true + + let itemProvider = NSItemProvider() + let container = CKContainer.default() + + Task { @MainActor in + do { + if let existingShare = try await CloudSyncService.shared.existingShare(for: entity) { + itemProvider.registerCKShare(existingShare, container: container) + } else { + itemProvider.registerCKShare(container: container) { + try await CloudSyncService.shared.createShare(for: entity) + } + } + } catch { + dismissAction() + return + } + + let configuration = UIActivityItemsConfiguration(itemProviders: [itemProvider]) + let activityController = UIActivityViewController(activityItemsConfiguration: configuration) + activityController.completionWithItemsHandler = { [dismissAction] _, _, _, _ in + dismissAction() + } + activityController.popoverPresentationController?.sourceView = presenter.view + activityController.presentationController?.delegate = self + presenter.present(activityController, animated: true) + } + } + + func presentationControllerDidDismiss(_ presentationController: UIPresentationController) { + dismissAction() + } + } +} -- cgit v1.2.3