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.
MetadataProviderconverts Foundation query notifications into app-specific metadata updates on the main queue.Documentserializes package-wrapper and pending-change access with a private concurrent queue and barrier writes.
Overall architecture
flowchart LR
ICLOUD["iCloud container"] --> QUERY["MetadataProvider / NSMetadataQuery"]
QUERY --> NOTICE["sicdMetadataDidChange notification"]
NOTICE --> MAIN["MainViewController"]
MAIN --> LIST["DiffableMetadataSource snapshot"]
LIST --> SELECT["Selected MetadataItem URL"]
SELECT --> DOC["Document UIDocument"]
DOC --> DETAIL["DetailViewController"]
DETAIL --> IMAGES["DiffableImageSource and conflict UI"]
DOC --> COORD["Coordinated package read/write and peer changes"]
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
classDiagram
MainViewController *-- MetadataProvider : creates
MainViewController *-- DiffableMetadataSource : lazy
MetadataProvider *-- NSMetadataQuery : private
MetadataProvider *-- AnyCancellable : private
DetailViewController o-- Document : assigned for selected URL
Document *-- DispatchQueue : private accessQueue
Document *-- FileWrapperMap : package state
ImageCVCell o-- ImagCVCellDelegate : weak
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
UIDocumentas 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 |