How to implement CloudKit sharing in SwiftUI
Create or fetch a CKShare for your CloudKit record, then present SwiftUI's built-in
CloudSharingView in a sheet — iOS handles invitations, permissions, and participant management automatically.
import SwiftUI
import CloudKit
struct ShareButton: View {
@State private var share: CKShare?
@State private var showShare = false
let container = CKContainer.default()
var body: some View {
Button("Share") {
Task { await prepareShare() }
}
.sheet(isPresented: $showShare) {
if let share {
CloudSharingView(share: share, container: container)
}
}
}
func prepareShare() async {
// Fetch or create CKShare, then toggle sheet
showShare = true
}
}
Full implementation
The pattern below stores a simple note record in the CloudKit private database, creates a
CKShare for it when the user taps Share, and presents CloudSharingView
so participants can be invited by email or link. A separate onContinueUserActivity
modifier on the root scene accepts incoming shares from other users' devices.
import SwiftUI
import CloudKit
// MARK: - Model
struct SharedNote: Identifiable {
let id: CKRecord.ID
var title: String
var body: String
var record: CKRecord
}
// MARK: - ViewModel
@Observable
final class SharedNoteViewModel {
var note: SharedNote?
var share: CKShare?
var errorMessage: String?
private let container = CKContainer(identifier: "iCloud.com.example.MyApp")
private var privateDB: CKDatabase { container.privateCloudDatabase }
// Create a new note record and its CKShare in one operation
func createNoteAndShare(title: String, body: String) async {
let recordID = CKRecord.ID(recordName: UUID().uuidString)
let record = CKRecord(recordType: "Note", recordID: recordID)
record["title"] = title as CKRecordValue
record["body"] = body as CKRecordValue
let share = CKShare(rootRecord: record)
share[CKShare.SystemFieldKey.title] = title as CKRecordValue
share.publicPermission = .none // invite-only
let op = CKModifyRecordsOperation(
recordsToSave: [record, share],
recordIDsToDelete: nil
)
op.savePolicy = .ifServerRecordUnchanged
do {
let (savedRecords, _) = try await withCheckedThrowingContinuation {
(cont: CheckedContinuation<([CKRecord], [CKRecord.ID]), Error>) in
op.modifyRecordsResultBlock = { result in
switch result {
case .success:
break
case .failure(let error):
cont.resume(throwing: error)
}
}
op.perRecordSaveBlock = { _, result in
if case .failure(let e) = result {
cont.resume(throwing: e)
}
}
op.fetchRecordsResultBlock = { _ in }
cont.resume(returning: ([], []))
privateDB.add(op)
}
_ = savedRecords
self.share = share
self.note = SharedNote(id: recordID, title: title, body: body, record: record)
} catch {
errorMessage = error.localizedDescription
}
}
// Accept an incoming share when another user taps the invite link
func acceptShare(metadata: CKShare.Metadata) async {
let op = CKAcceptSharesOperation(shareMetadatas: [metadata])
do {
try await container.accept(shareMetadatas: [metadata])
} catch {
errorMessage = error.localizedDescription
}
_ = op
}
}
// MARK: - Root View
struct CloudKitSharingDemoView: View {
@State private var vm = SharedNoteViewModel()
@State private var showShareSheet = false
@State private var showCreateSheet = false
@State private var noteTitle = "Team Sprint Notes"
@State private var noteBody = "Add your updates here…"
var body: some View {
NavigationStack {
VStack(spacing: 24) {
if let note = vm.note {
VStack(alignment: .leading, spacing: 8) {
Text(note.title).font(.title2.bold())
Text(note.body).foregroundStyle(.secondary)
}
.frame(maxWidth: .infinity, alignment: .leading)
.padding()
.background(.regularMaterial, in: RoundedRectangle(cornerRadius: 16))
Button {
showShareSheet = true
} label: {
Label("Share Note", systemImage: "person.crop.circle.badge.plus")
.frame(maxWidth: .infinity)
}
.buttonStyle(.borderedProminent)
.controlSize(.large)
.accessibilityLabel("Share this note with others via CloudKit")
} else {
ContentUnavailableView(
"No Note Yet",
systemImage: "doc.text",
description: Text("Tap Create to make a shareable note.")
)
Button("Create Note") { showCreateSheet = true }
.buttonStyle(.borderedProminent)
}
if let err = vm.errorMessage {
Text(err)
.font(.caption)
.foregroundStyle(.red)
.padding(.horizontal)
}
}
.padding()
.navigationTitle("CloudKit Sharing")
.sheet(isPresented: $showShareSheet) {
if let share = vm.share {
CloudSharingView(
share: share,
container: CKContainer(identifier: "iCloud.com.example.MyApp")
)
.ignoresSafeArea()
}
}
.sheet(isPresented: $showCreateSheet) {
CreateNoteSheet(title: $noteTitle, body: $noteBody) {
Task { await vm.createNoteAndShare(title: noteTitle, body: noteBody) }
showCreateSheet = false
}
}
}
.onContinueUserActivity(NSUserActivityTypes.cloudKitShareMetadata) { activity in
guard let metadata = activity.userInfo?[NSItemProvider.cloudKitShareMetadataKey]
as? CKShare.Metadata else { return }
Task { await vm.acceptShare(metadata: metadata) }
}
}
}
// MARK: - Create Note Sheet
struct CreateNoteSheet: View {
@Binding var title: String
@Binding var body: String
var onCreate: () -> Void
var body: some View {
NavigationStack {
Form {
Section("Title") { TextField("Sprint notes…", text: $title) }
Section("Body") { TextEditor(text: $body).frame(minHeight: 80) }
}
.navigationTitle("New Note")
.toolbar {
ToolbarItem(placement: .confirmationAction) {
Button("Create", action: onCreate).bold()
}
}
}
}
}
// MARK: - Preview
#Preview {
CloudKitSharingDemoView()
}
How it works
-
CKShare + CKModifyRecordsOperation — Both the root
CKRecord("Note") and theCKSharemust be saved together in a singleCKModifyRecordsOperation. Saving them separately causes a "share already exists" server error or leaves the record unlinked to the share. -
CloudSharingView sheet — SwiftUI's native
CloudSharingView(share:container:)renders Apple's standard sharing UI (invite by email, copy link, manage participants) without any extra code. Pass theCKShareand theCKContainerthat owns it. -
share.publicPermission = .none — Setting this to
.nonebefore saving means only explicitly invited participants can access the record. Use.readWriteto create a public link that anyone can join. -
CKShare.SystemFieldKey.title — Setting the share's title (line
share[CKShare.SystemFieldKey.title]) populates the preview card that iOS shows when a recipient taps the invitation link — always set it to something descriptive. -
onContinueUserActivity for acceptance — When a recipient taps an invitation link, iOS delivers
a
NSUserActivitywith typeNSUserActivityTypes.cloudKitShareMetadata. TheonContinueUserActivitymodifier extracts theCKShare.Metadataand callsCKContainer.accept(shareMetadatas:)to join the share zone.
Variants
Public "anyone with the link" share
// After creating the share, grant read-only access to anyone
let share = CKShare(rootRecord: record)
share.publicPermission = .readOnly
share[CKShare.SystemFieldKey.title] = "My Public Board" as CKRecordValue
// Save with CKModifyRecordsOperation as usual, then retrieve the URL
if let url = share.url {
// Present ShareLink(item: url) to let the user copy or send the link
ShareLink(item: url, subject: Text("Join my board")) {
Label("Copy Link", systemImage: "link")
}
}
Checking participant permissions at runtime
After a share is active you can inspect share.participants to discover each user's
CKShare.Participant.Role (.owner, .privateUser) and
CKShare.Participant.Permission (.readOnly, .readWrite).
Use this to hide or show editing UI:
let canEdit = share.currentUserParticipant?.permission == .readWrite.
Only the share owner can promote other participants — enforce this in your view model before calling
CKModifyRecordsOperation on the share record.
Common pitfalls
-
⚠️ iOS version:
CloudSharingViewwas introduced in iOS 16, but theCKContainer.accept(shareMetadatas:)async overload requires iOS 17+. Guard with#available(iOS 17, *)if you still support iOS 16 targets, or adopt a minimum deployment of 17. - ⚠️ Missing entitlements: CloudKit sharing silently fails if iCloud capability is not added to your target in Xcode and the CloudKit container identifier does not match exactly (case-sensitive). Always verify under Signing & Capabilities → iCloud → CloudKit → Containers.
- ⚠️ Schema migration: CloudKit record types used in a shared zone must be deployed to the Production environment before submission. Test in Development first; use CloudKit Console → Deploy Schema before App Store review — reviewers' devices hit Production, not Development.
-
⚠️ VoiceOver on CloudSharingView: Apple's
CloudSharingViewsheet is a UIKit component wrapped for SwiftUI — it already carries full accessibility support, but any custom wrap-around buttons you add (like "Share Note") must have explicit.accessibilityLabelstrings so VoiceOver announces context correctly.
Prompt this with Claude Code
When using Soarias or Claude Code directly to implement this:
Implement cloudkit sharing in SwiftUI for iOS 17+. Use CKShare, CKModifyRecordsOperation, and CloudSharingView. Support both invite-only and public-link sharing modes. Make it accessible (VoiceOver labels on all share controls). Handle incoming shares via onContinueUserActivity. Add a #Preview with realistic sample data.
In Soarias's Build phase, paste this prompt into a new feature task — Claude Code will scaffold
the CloudKit container setup, Observable view model, and SwiftUI sheet wiring in one shot, letting
you iterate on participant UI without leaving your local environment.
Related
FAQ
Does this work on iOS 16?
Partially. CloudSharingView is available from iOS 16, but the
async/await overloads of CKContainer.accept(shareMetadatas:) and
CKModifyRecordsOperation completion handlers require iOS 17+. If you target iOS 16,
use the completion-handler-based CloudKit API wrapped in withCheckedThrowingContinuation,
and guard the async paths with #available(iOS 17, *).
Can I share records stored in SwiftData?
SwiftData's ModelContext does not expose the underlying CKRecord directly. To share
SwiftData-backed content you have two options: (1) use SwiftData's built-in
ModelConfiguration(cloudKitDatabase: .automatic) which syncs privately, or (2) mirror
critical records to CloudKit manually by fetching them from the private database by a known
CKRecord.ID you stored alongside your SwiftData model, then follow the CKShare
flow above. Full SwiftData + CKShare integration is not yet available as of iOS 17.
What's the UIKit equivalent?
In UIKit you present UICloudSharingController(share:container:) modally. Set its
delegate to receive cloudSharingController(_:failedToSaveShareWithError:)
and itemThumbnailData(for:) callbacks. The SwiftUI CloudSharingView
wraps this UIKit controller internally, so the behavior is identical — SwiftUI just removes the
boilerplate delegate wiring.
Last reviewed: 2026-05-11 by the Soarias team.