Sample CodeiOS, iPadOS, Mac CatalystReviewed 2026-07-21View on Apple Developer

Synchronizing documents in the iCloud environment

At a glance

Item Summary
Purpose Discover iCloud documents, open package-based UIDocument instances, coordinate file changes, and surface conflicts in a split interface.
App architecture MetadataProvider owns NSMetadataQuery and publishes typed change notifications to MainViewController; selecting an item creates a Document for DetailViewController, where package state, peer changes, and conflicts remain behind the UIDocument boundary.
Main patterns Metadata query publisher, Document boundary, Coordinated file access
Project style Eighteen Swift files grouped into Main and Detail slices, plus app/scene lifecycle files.

Project structure

Source bundle/
└── SimpleiCloudDocument/
    ├── AppDelegate.swift
    ├── Main/
    │   ├── MainViewController.swift
    │   ├── MetadataProvider.swift
    │   ├── MainViewController+TableView.swift
    │   └── DiffableMetadataSource.swift
    ├── Detail/
    │   ├── DetailViewController.swift
    │   ├── ImageViewController.swift
    │   ├── DetailViewController+CollectionView.swift
    │   ├── Document.swift
    │   ├── ImageCVCell.swift
    │   └── DiffableImageSource.swift
    └── SceneDelegate.swift

Structure observations

  • The Main slice owns container discovery and the document list; the Detail slice owns an open document and its images/conflicts.
  • MetadataProvider converts Foundation query notifications into app-specific metadata updates on the main queue.
  • Document serializes package-wrapper and pending-change access with a private concurrent queue and barrier writes.

Overall architecture

Reference code

SimpleiCloudDocument/Main/MetadataProvider.swift:52 — query-notification adapter

        let names: [NSNotification.Name] = [
            .NSMetadataQueryDidFinishGathering, .NSMetadataQueryDidUpdate
        ]
        let publishers = names.map { NotificationCenter.default.publisher(for: $0) }
        querySubscriber = Publishers.MergeMany(publishers)
            .receive(on: DispatchQueue.main).sink { notification in
                guard notification.object as? NSMetadataQuery === self.metadataQuery else { return }
                let userInfo: MetadataDidChangeUserInfo = [.queryResults: self.metadataItemList()]
                NotificationCenter.default.post(name: .sicdMetadataDidChange,
                                                object: self, userInfo: userInfo)
            }

Interpretation

Metadata discovery and document content are separate lifecycles. The provider reports which URLs exist; the main controller snapshots that read model. Only after navigation does the detail controller own an open UIDocument, whose subclass governs package serialization and peer-change reconciliation.

Ownership and state

Ownership evidence

SimpleiCloudDocument/Main/MainViewController.swift:15 — main-list collaborators

    private(set) var metadataProvider: MetadataProvider?

    lazy var diffableMetadataSource: DiffableMetadataSource! = {
        DiffableMetadataSource(tableView: tableView) { tableView, indexPath, metadata in
            let cell = tableView.dequeueReusableCell(
                withIdentifier: "UITableViewCell", for: indexPath)
            cell.textLabel!.text = metadata.url.lastPathComponent
            return cell
        }
    }()
Owner Object or state Relationship Mutation authority
MainViewController Metadata provider and diffable metadata source Creates both for its table-list lifetime It applies metadata snapshots and creates/removes documents.
MetadataProvider Container root URL, metadata query, Combine subscriber Owns query and subscription; externally exposes read-only root URL Only the provider starts/stops and reads query results.
DetailViewController Optional selected Document and image-list presentation Receives the document before segue completion and opens/closes it It coordinates UI with document state and conflicts.
Document Package wrappers, pending user/peer changes, access queue Owns private serialized state UIDocument callbacks and public mutation methods are the content authority.
ImageCVCell Weak ImagCVCellDelegate Non-owning callback reference The detail controller decides deletion; the cell only reports intent.

Composition denotes source-visible construction and retained value state. Aggregation denotes a stored or injected collaborator whose exclusive lifetime the source does not prove.

Class and protocol design

SimpleiCloudDocument/Detail/ImageCVCell.swift:10 — local cell-action contract

protocol ImagCVCellDelegate: AnyObject {
    func deleteCell(_ cell: UICollectionViewCell)
}

class ImageCVCell: UICollectionViewCell {
    weak var delegate: ImagCVCellDelegate?

    @IBAction func deleteAction(_ sender: UIButton) {
        delegate?.deleteCell(self)
    }
}
Type Responsibility Depends on or conforms to
MetadataProvider Discovers and normalizes iCloud metadata NSMetadataQuery, Combine, NotificationCenter
MainViewController Lists metadata and owns document create/delete/navigation UITableViewController
Document Reads/writes the document package and tracks unsaved/peer changes UIDocument
DetailViewController Opens/closes one document and presents image/conflict state UICollectionViewController, ImagCVCellDelegate
DiffableMetadataSource / DiffableImageSource Translate stable model identity into list snapshots UIKit diffable data-source subclasses

ImagCVCellDelegate is a local, class-bound protocol and its weak property makes the non-ownership explicit. UIDocument and collection/table protocols remain framework callback boundaries.

Access control

Symbol Access Verified effect Design reason
MainViewController.metadataProvider private(set) All module code may read it, but only the declaring type/file can assign it Document helpers can inspect the container while provider replacement stays controller-owned.
MetadataProvider.containerRootURL private(set) Consumers read the resolved root; only the provider writes it Preserves the provider as authority for asynchronous container resolution.
metadataQuery, querySubscriber private Hidden within MetadataProvider Prevents callers from bypassing query batching and typed notification publication.
Document.accessQueue and underscored storage private Reachable only by Document’s lexical implementation All package-state reads/writes must pass through serialized accessors.
Feature types and local protocol implicit internal Visible inside the app module The sample has no external framework API.

Reference code

SimpleiCloudDocument/Detail/Document.swift:34 — private synchronized document storage

    private lazy var accessQueue: DispatchQueue = {
        DispatchQueue(label: "Document", attributes: .concurrent)
    }()

    private var _fileWrappersUnderRoot: [String: FileWrapper]?
    private var _unsavedUserChanges = Changes()

    private(set) var unpresentedPeerChanges = Changes()

private(set) communicates split authority directly: broad read access with restricted mutation. No public, open, or fileprivate feature surface is required for this single app target.

Logic ownership and placement

Logic Owning type or file Why it lives there
Container resolution and metadata normalization MetadataProvider It owns query lifecycle and converts raw NSMetadataItem values.
Metadata snapshot and document routing MainViewController The list controller owns table state and the selected URL transition.
Package serialization and peer-change reconciliation Document These are content invariants driven by UIDocument callbacks.
Open/close, image interaction, conflict presentation DetailViewController extensions They depend on one visible document session and its UI.
Coordinated delete MainViewController+Document The feature command is list-owned but uses NSFileCoordinator for safe external mutation.

Design patterns

Pattern Source evidence Purpose or tradeoff
Metadata query publisher SimpleiCloudDocument/Main/MetadataProvider.swift:52 Adapts Foundation notifications into a typed app event on the main queue.
Document boundary SimpleiCloudDocument/Detail/Document.swift:10 Concentrates package persistence, errors, conflicts, and synchronization state.
Coordinated file access SimpleiCloudDocument/Main/MainViewController+Document.swift:83 Serializes destructive file-system work with other presenters.

Naming conventions

  • Types identify layer roles (MetadataProvider, Document, DiffableMetadataSource, DetailViewController).
  • Private backing storage uses an underscore only where synchronized computed accessors wrap it.
  • Notification names are sample-prefixed (sicdMetadataDidChange) and user-info keys are strongly typed.
  • Files group controller responsibilities into focused extensions such as Document, Conflict, CollectionView, and ImagePicker.

Architecture takeaways

  • Treat metadata discovery as a read model and an open UIDocument as a separate content session.
  • Use private(set) when collaborators need state visibility but must not replace the authority-owned value.
  • Keep package state behind serialized private access and use coordinated writes for external file operations.

Source map

Source file Architectural role
SimpleiCloudDocument/Main/MetadataProvider.swift iCloud container and metadata-query lifecycle
SimpleiCloudDocument/Main/MainViewController.swift Metadata list, snapshots, and document routing
SimpleiCloudDocument/Main/MainViewController+Document.swift Document create/delete and coordinated access
SimpleiCloudDocument/Detail/Document.swift Package serialization, synchronized state, and peer changes
SimpleiCloudDocument/Detail/DetailViewController.swift Open-document and image UI ownership
SimpleiCloudDocument/Detail/ImageCVCell.swift Weak local deletion callback