How to easily create ZIP files in Swift without third-party dependencies

If you’ve ever worked on an iOS app with complex networking, sooner or later you’ll probably need to create ZIP archives from your files. In fact, zipping large files can significantly reduce the size of your HTTP payloads, giving you better networking performance & data usage. It is also arguably the best way to send over multiple files at once, instead of building out a huge multi-part request or sending a separate request for each file.
For most developers, the first instinct when approaching this problem is to reach for a third-party dependency that does it for you. “Zip” by marmelroy is a great Swift library that does just that. However, if you only need to create ZIP files, not unzip them, using Apple’s system API is enough.
Here’s how you can do it using Apple’s NSFileCoordinatorReadingForUploading API:
enum CreateZipError: Swift.Error {
case urlNotADirectory(URL)
case failedToCreateZIP(Swift.Error)
}
func createZip(
zipFinalURL: URL,
fromDirectory directoryURL: URL
) throws -> URL {
// see URL extension below
guard directoryURL.isDirectory else {
throw CreateZipError.urlNotADirectory(directoryURL)
}
var fileManagerError: Swift.Error?
var coordinatorError: NSError?
let coordinator = NSFileCoordinator()
coordinator.coordinate(
readingItemAt: directoryURL,
options: .forUploading,
error: &coordinatorError
) { zipCreatedURL in
do {
// will fail if file already exists at finalURL
// use `replaceItem` instead if you want "overwrite" behavior
try FileManager.default.moveItem(at: zipCreatedURL, to: zipFinalURL)
} catch {
fileManagerError = error
}
}
if let error = coordinatorError ?? fileManagerError {
throw CreateZipError.failedToCreateZIP(error)
}
return zipFinalURL
}
extension URL {
var isDirectory: Bool {
(try? resourceValues(forKeys: [.isDirectoryKey]))?.isDirectory == true
}
}
let mediaDirectoryURL = /* ... */
let zipURL = try createZip(
zipFinalURL: FileManager.default.temporaryDirectory.appending(path: "new_archive.zip"),
fromDirectory: mediaDirectoryURL
)
Per Apple’s documentation, the ZIP file will only be created if a provided URL is a directory:
If the item being read is a directory (such as a document package), then the snapshot is a new file containing the zipped contents of the directory. The URL passed to the accessor block points to the zipped file.
This is why we’re using guard directoryURL.isDirectory
at the top of the function.
A helper function to easily create zip files in the tmp
directory is also a good idea:
func createZipAtTmp(
zipFilename: String,
zipExtension: String = "zip",
fromDirectory directoryURL: URL
) throws -> URL {
let finalURL = FileManager.default.temporaryDirectory
.appending(path: zipFilename)
.appendingPathExtension(zipExtension)
return try createZip(
zipFinalURL: finalURL,
fromDirectory: directoryURL
)
}
let mediaDirectoryURL = /* ... */
let zipURL = try createZipAtTmp(
zipFilename: "new_archive",
fromDirectory: mediaDirectoryURL
)
Bonus: simplifying ZIP directory composition
To simplify creating the “directory to be zipped”, at Parable we built an additional layer called FileToZip
enum FileToZip {
case data(Data, filename: String)
case existingFile(URL)
case renamedFile(URL, toFilename: String)
}
extension FileToZip {
func prepareInDirectory(directoryURL: URL) throws {
switch self {
case .data(let data, filename: let filename):
let fileURL = directoryURL.appendingPathComponent(filename)
try data.write(to: fileURL)
case .existingFile(let existingFileURL):
let filename = existingFileURL.lastPathComponent
let newFileURL = directoryURL.appendingPathComponent(filename)
try FileManager.default.copyItem(at: existingFileURL, to: newFileURL)
case .renamedFile(let existingFileURL, toFilename: let filename):
let newFileURL = directoryURL.appendingPathComponent(filename)
try FileManager.default.copyItem(at: existingFileURL, to: newFileURL)
}
}
}
prepareInDirectory
will then be used by our zip function to create a “directory to be zipped” for us
func createZipAtTmp(
zipFilename: String,
zipExtension: String = "zip",
filesToZip: [FileToZip]
) throws -> URL {
let directoryToZipURL = FileManager.default.temporaryDirectory
.appending(path: UUID().uuidString)
.appending(path: zipFilename)
try FileManager.default.createDirectory(at: directoryToZipURL, withIntermediateDirectories: true, attributes: [:])
for fileToZip in filesToZip {
try fileToZip.prepareInDirectory(directoryURL: directoryToZipURL)
}
return try createZipAtTmp(
zipFilename: zipFilename,
zipExtension: zipExtension,
fromDirectory: directoryToZipURL
)
}
let pngImageData: Data = /* ... */
let videoURL: URL = /* ... */
let dbFileURL: URL = /* ... */
let zipURL = try createZipAtTmp(
zipFilename: "new_archive",
filesToZip: [
.data(imageData, filename: "image.png"),
.existingFile(videoURL),
.renamedFile(dbFileURL, toFilename: "local_db.sqlite")
]
)
A few important things to note in there:
- As you see, we’re creating our “directory to be zipped” inside another directory with a UUID name — this is done to avoid possible naming collisions. You can remove this part, if you prefer.
- If at least one “file to zip” can’t be prepared in a directory, the entire operation will fail. This can happen, for example, if two or more files will have the same filename. You might want to handle errors differently there.
- In all of the functions, if the “zip filename” you’re trying to create already exists in the final destination, the operation will fail. If you don’t want that, use “replaceItem” instead of “moveItem” in the original
createZip
function
Conclusion
As you can see, it’s quite straightforward to create ZIP files on iOS using nothing but system APIs. Important to note that for unzipping, however, you will need to use a third-party solution.
A nicely packaged and extended version of this solution is available as a GitHub gist: