```html SwiftUI: How to Implement CloudKit Sharing (iOS 17+, 2026)

How to implement CloudKit sharing in SwiftUI

iOS 17+ Xcode 16+ Advanced APIs: CKShare Updated: May 11, 2026
TL;DR

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

  1. CKShare + CKModifyRecordsOperation — Both the root CKRecord ("Note") and the CKShare must be saved together in a single CKModifyRecordsOperation. Saving them separately causes a "share already exists" server error or leaves the record unlinked to the share.
  2. 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 the CKShare and the CKContainer that owns it.
  3. share.publicPermission = .none — Setting this to .none before saving means only explicitly invited participants can access the record. Use .readWrite to create a public link that anyone can join.
  4. 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.
  5. onContinueUserActivity for acceptance — When a recipient taps an invitation link, iOS delivers a NSUserActivity with type NSUserActivityTypes.cloudKitShareMetadata. The onContinueUserActivity modifier extracts the CKShare.Metadata and calls CKContainer.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

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.

```