Canopy
A library that helps you isolate CloudKit dependency and write testable code using CloudKit.
Install / Use
/learn @tact/CanopyREADME
Canopy
Canopy helps you write better, more testable CloudKit apps.
Installing Canopy
Canopy is distributed as a Swift Package Manager package.
If you use Xcode UI to manage your dependencies, add https://github.com/Tact/Canopy as the dependency for your project.
If you use SPM Package.swift, add this:
dependencies: [
.package(
url: "https://github.com/Tact/Canopy",
from: "0.5.0"
)
]
Using Canopy
One-line example
To fetch a record from CloudKit private database which has the record ID exampleID, use this Canopy call:
let result = await Canopy().databaseAPI(usingDatabaseScope: .private).fetchRecords(with: [CKRecord.ID(recordName: "exampleID")])
switch result {
case .success(let fetchRecordsResult):
// handle fetchRecordsResult. Examine its foundRecords and notFoundRecordIDs properties.
case .failure(let ckRecordError):
// handle error
}
Using throwing return type
Canopy provides all its API as async Result. Many people prefer to instead use throwing API. It’s easy to convert Canopy API calls to throwing style at the call site with the get() API. For the above example, follow this approach:
do {
let result = try await Canopy().databaseAPI(usingDatabaseScope: .private).fetchRecords(…).get()
// use result
} catch {
// handle thrown error
}
Dependency injection for testability
Canopy is designed for enabling your code to be testable. You do your part by using dependency injection pattern in most of your code and features. Most of your code should not instantiate Canopy directly, but should receive it from outside. For example:
actor MyService {
private let canopy: CanopyType
init(canopy: CanopyType) {
self.canopy = canopy
}
func someFeature() async {
let databaseAPI = await canopy.getDatabaseAPI(usingDatabaseScope: .private)
// call databaseAPI functions to
// retrieve and modify records, zones, subscriptions …
}
}
In live use of your app, you initiate and inject the live Canopy object that talks to CloudKit. When independently testing your features, you instead inject a mock Canopy object that doesn’t talk to any cloud services, but instead plays back mock responses.
Read more: Testable CloudKit apps with Canopy
Dependency injection with swift-dependency
Canopy implements cloudKit dependency key for swift-dependencies. If you use swift-dependencies, you use Canopy like this:
struct MyFeature {
@Dependency(\.cloudKit) private var canopy
func myFeature() async {
let recordsResult = await canopy.databaseAPI(usingDatabaseScope: .private).fetchRecords(…)
}
}
See swift-dependencies documentation for more info about how to use dependencies, and inject desired values for the Canopy dependency for your previews and tests.
Understanding Canopy
The Canopy package has three parts.
Libraries
Libraries provide the main Canopy functionality and value. Canopy is the main library, and CanopyTestTools helps you build tests.
Documentation
The Canopy documentation site at https://canopy-docs.justtact.com has documentation for the libraries, as well as information about the library motivation and some ideas and best practices about using CloudKit. The documentation is generated by DocC from this repository, and can also be used inline in Xcode.
Some highlights from documentation:
Testable CloudKit apps with Canopy
iCloud Advanced Data Protection
Example app
The Thoughts example app showcases using Canopy in a real app, and demonstrates some best practices for modern multi-platform, multi-window app development.
Authors and credits
Canopy was built, and continues to be built, as part of Tact app.
Major contributors: Jaanus Kase, Andrew Tetlaw, Henry Cooper
Thanks to: Priidu Zilmer, Roger Sheen, Margus Holland
Related Skills
node-connect
340.5kDiagnose OpenClaw node connection and pairing failures for Android, iOS, and macOS companion apps
frontend-design
84.2kCreate distinctive, production-grade frontend interfaces with high design quality. Use this skill when the user asks to build web components, pages, or applications. Generates creative, polished code that avoids generic AI aesthetics.
openai-whisper-api
340.5kTranscribe audio via OpenAI Audio Transcriptions API (Whisper).
commit-push-pr
84.2kCommit, push, and open a PR
