Author: Drew McCormack (Mastodon: @[email protected])
An easy to use Swift wrapper around iCloud Drive.
SwiftCloudDrive handles complexities like file coordination, accessing security scoped resources,
file conflicts, and cloud metadata queries, to provide straightforward async functions
for working with files in iCloud. It makes handling files in the cloud almost
as easy as working locally with FileManager
.
For advanced uses of iCloud, you should probably use Apple's frameworks directly. This gives you most control, in exchange for a steep learning curve.
For example, if you need complete control over when files in iCloud Drive get downloaded to a device, or have an app like Apple's Photos, where it is often desirable to leave files in the cloud until they are requested by the user, you should not use SwiftCloudDrive.
If you want all of your app's iCloud Drive files on device, as well as
in the cloud, SwiftCloudDrive can get you up and running much faster.
You don't have to worry about file coordination, metadata queries, or conflict
resolution, which are all part of working with iCloud Drive. SwiftCloudDrive
will give you a simple class to upload, download, query, and update, which
works much the same as the FileManager
type you are already familiar with.
To get started, add the SwiftCloudDrive package to your Xcode project, and then enable iCloud Drive in the Signing & Capabilities tab of your app's target in Xcode. You can accept the default container identifier, or choose a custom one.
If you are using the default iCloud container, setting up a CloudDrive
in
your app's source code can be as simple as this
import SwiftCloudDrive
let drive = try await CloudDrive()
If you have a custom iCloud container, simply pass the identifier in.
let customDrive = try await CloudDrive(storage: .iCloudContainer(containerIdentifier: "iCloud.com.yourcompany.app"))
In the cases above, you will be accessing files directly in the root of
the container, but you can also anchor your CloudDrive
at a particular
subdirectory of the container, like this
let subDrive = try await CloudDrive(relativePathToRootInContainer: "Sub/Directory/Of/Choice")
The CloudDrive
will create the directory for you, if it doesn't exist.
Once you have a CloudDrive
object, you can query file status just like you
do with FileManager
. There is one big difference though: because files may
be remote and have to download, all function calls are async
.
To determine if a directory exists, you would do this
let path = RootRelativePath(path: "Path/To/Dir")
let exists = try await drive.directoryExists(at: path)
If you want to know if a particular file exists, you would use this
let exists = try await drive.fileExists(at: path)
As you can see in the previous section, the RootRelativePath
struct
is used to reference paths relative to the root of the CloudDrive
.
If you use particular fixed paths often, it is useful to extend RootRelativePath
to define static constants.
extension RootRelativePath {
static let images = Self(path: "Images")
}
let imagesExist = try await drive.directoryExists(at: .images)
let dogImageExists = try await drive.fileExists(at: .images.appending("Dog.jpeg"))
As you can see, the RootRelativePath
also defines an appending
function
which makes it simple to extend an existing path.
Creating a directory in the cloud is just as easy. Note that if there are missing intermediate directories, these are always created too.
try await drive.createDirectory(at: .images)
To move files into the cloud, you can 'upload' a local file to the container,
or you can write Data
directly into a file.
let data = "Some text".data(using: .utf8)!
try await drive.writeFile(with: data, at: .root.appending("file.txt"))
In this case we use the built in static constant root
to build a path, but
we could also have just used RootRelativePath("file.txt")
.
To upload a file from outside of the cloud container, you use code like this
try await drive.upload(from: "/Users/eddy/Desktop/image.jpeg", to: .images.appending("image.jpeg"))
Once you have a file in the cloud, you can change it by uploading again, but you must first delete the old version, otherwise an error will arise.
let cloudPath: RootRelativePath = .images.appending("image.jpeg")
try? await drive.removeFile(at: cloudPath)
try await drive.upload(from: "/Users/eddy/Desktop/image_new.jpeg", to: cloudPath)
Alternatively, you can write the contents without first removing the existing file.
try await drive.writeFile(with: newImageData, at: cloudPath)
If you need even finer control, you can make any in-place update you favor, like this
try await drive.updateFile(at: imagePath) { fileURL in
// Make any changes you like to the file at `fileURL`
// You can also throw an error
}
As we have already seen, you can very easily remove files and directories.
try await drive.removeFile(at: cloudFilePath)
try await drive.removeDirectory(at: cloudDirPath)
If the wrong type of item is encountered, the removal fails and an error is thrown.
When working with changes on remote devices, it is important to know when
new files have downloaded, or updates have been applied. You can register a CloudDriveObserver
for this purpose.
class Controller: CloudDriveObserver {
func cloudDriveDidChange(_ drive: CloudDrive, rootRelativePaths: [RootRelativePath]) {
// Decide if data needs refetching due to remote changes,
// or if changes need to be applied in the UI
}
}
let controller = Controller()
drive.observer = controller
Note that the relative paths passed to the observer are not necessarily new files, but could be files with updates, or even files that were removed. Your code should take these possibilities into account, and adapt appropriately.