How to Select File From Storage and Upload on Server in Swift
Reading and writing files and folders is i of those tasks that almost every app needs to perform at some bespeak. While many apps these days, particularly on iOS, might not give users transparent access to open, save, and update documents every bit they please — whenever we're dealing with some form of long-term data persistence, or a bundled resources, we always have to interact with the file system one way or some other.
So this calendar week, allow'south take a closer look at diverse ways to use the many file organisation-related APIs that Swift offers — both on Apple's own platforms, and on platforms like Linux — and a few things that can be good to go on in mind when working with those APIs.
Bitrise: Easily gear up fast, rock-solid continuous integration for your project with Bitrise. In just a few minutes, y'all can set up builds, tests, and automatic App Shop and beta deployments for your project, all running in the deject on every pull request and commit. Try information technology for free today.
URLs, locations, and data
Fundamentally, in that location are two Foundation types that are especially important when reading and writing files in Swift — URL
and Data
. Just like when performing network calls, URLs are used to signal to various locations on disk, which we can then either read binary data from, or write new data into.
For example, here we're retrieving a file path passed as an statement to a Swift command line tool, which we and then turn into a file organization URL in guild to load that file'due south data:
// This lets us easily access whatever command line argument passed // into our plan as "-path": guard let path = UserDefaults.standard.cord(forKey: "path") else { throw Error.noPathGiven } let url = URL(fileURLWithPath: path) practise { let data = try Data(contentsOf: url) ... } catch { throw Error.failedToLoadData }
To acquire more nigh the above style of using UserDefaults
, and using command line arguments in general, check out "Launch arguments in Swift".
One thing that's typically expert to go on in heed when working with cord-based paths is that sure characters are expected to be interpreted in specific ways, such as the tilde character (~
), which is commonly used to refer to the current user's home directory.
While that's not something that nosotros typically take to handle manually when dealing with command line tool input (as concluding shells tend to expand such symbols automatically), within other contexts we can enlist the assistance of the Cord
type'due south Objective-C "cousin", NSString
, to aid united states aggrandize any tilde character found inside a given string into the user's full home directory path:
var path = resolvePath() path = (path as NSString).expandingTildeInPath
Worth noting is that NSString
is also available on Linux, through the open up source, Swift-based version of Foundation.
Bundles and modules
On Apple'south platforms, apps are distributed as bundles, which ways that in order to access internal files that we've included (or bundled) within our own app, nosotros'll commencement need to resolve their actual URLs past searching for them within our app'south main bundle.
That main parcel can be accessed using Packet.main
, which lets us call back any resources file that was included within our main app target, such as a bundled JSON file, like this:
struct ContentLoader { enum Mistake: Swift.Error { instance fileNotFound(name: String) case fileDecodingFailed(name: String, Swift.Error) } func loadBundledContent(fromFileNamed name: String) throws -> Content { baby-sit allow url = Bundle.principal.url( forResource: proper name, withExtension: "json" ) else { throw Error.fileNotFound(name: name) } practice { permit data = try Data(contentsOf: url) let decoder = JSONDecoder() return effort decoder.decode(Content.self, from: data) } take hold of { throw Error.fileDecodingFailed(name: name, mistake) } } ... }
While it might at outset seem like Bundle.main
is the just bundle that nosotros'll ever need to work with, that'southward typically not the case. For instance, let's say that we at present want to write a unit test that verifies the above ContentLoader
past having it load specific file that was bundled within our test packet:
form ContentLoaderTests: XCTestCase { func testLoadingContentFromBundledFile() throws { let loader = ContentLoader() let content = attempt loader.loadBundledContent(fromFileNamed: "testContent") XCTAssertEqual(content.title, "This is a examination") } ... }
When running the above exam, nosotros'll terminate up getting an error, which might initially seem a bit foreign (assuming that we've bundled a file called testContent.json
within our test target). The problem is that our unit of measurement testing suite has its own bundle, that's separate from Packet.main
, and since our ContentLoader
currently always uses the main
packet, our examination file won't be constitute.
So, in order to exist able to perform the above examination, we first demand to add together a flake of parameter-based dependency injection to enable ContentLoader
to load files from any Bundle
(while yet keeping main
equally the default):
struct ContentLoader { ... func loadBundledContent(fromFileNamed name: String, in packet: Package = .chief) throws -> Content { guard let url = bundle.url( forResource: name, withExtension: "json" ) else { throw Error.fileNotFound(name: proper noun) } ... } ... }
With the above in place, we tin at present resolve the right parcel within our unit of measurement tests — past asking the organisation for the bundle that contains our current test class — which nosotros'll then inject when calling our loadBundledContent
method:
class ContentLoaderTests: XCTestCase { func testLoadingContentFromBundledFile() throws { let loader = ContentLoader() let bundle = Bundle(for: Self.self) permit content = try loader.loadBundledContent( fromFileNamed: "testContent", in: bundle ) XCTAssertEqual(content.title, "This is a test") } ... }
Along those same lines, when using the Swift Packet Manager's new (equally of Swift v.3) adequacy that lets u.s.a. embed arranged resources within a Swift package, we also can't assume that Bundle.main
will comprise all of our app'south resources — since whatsoever file bundled within a Swift package will be accessible through the new module
belongings, which refers to the current module's bundle, rather than the one for the app itself.
So, in general, whenever we're designing an API that uses Bundle
to load local resources, it's typically a good idea to enable any Parcel
instance to be injected, rather than hard-coding our logic to always utilize the master
ane.
Storing files within organisation-defined folders
So far, nosotros've been exploring various ways to read files, either from whatsoever file system location through a control line tool (running on either macOS or Linux), or from a file bundled within an application. But at present, permit's accept a await at how nosotros tin can write files besides — in a style that's both predictable, and compatible with the tighter sandboxing rules found on platforms like iOS.
Actually writing binary information to deejay is every bit piece of cake as calling the write(to:)
method on any Data
value, but the question is how to resolve what URL to write to — particularly if we want to write a file to a system-defined folder, such every bit Library
or Documents
.
The answer is to utilize Foundation'southward FileManager
API, which lets us resolve URLs for system folders in a cross-platform manner. For example, here's how we could encode and write whatsoever Encodable
value to file within the current user's Documents
folder:
struct FileIOController { func write<T: Encodable>( _ value: T, toDocumentNamed documentName: Cord, encodedUsing encoder: JSONEncoder = .init() ) throws { permit folderURL = try FileManager.default.url( for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: false ) let fileURL = folderURL.appendingPathComponent(documentName) let data = endeavour encoder.encode(value) try information.write(to: fileURL) } ... }
On macOS, the above folderURL
will point to ~/Documents
, just as we'd wait, but on iOS it'll instead bespeak to our app's ain version of that folder that's located within the app's sandbox.
Similarly, we tin also utilise the above FileManager
API to resolve other kinds of system folders as well — for example the folder that the system deems the most appropriate to use for disk-based caching:
let cacheFolderURL = attempt FileManager.default.url( for: .cachesDirectory, in: .userDomainMask, appropriateFor: nil, create: false )
If all that nosotros're looking for is a URL for a temporary folder, however, nosotros can use the much simpler NSTemporaryDirectory
function — which returns a URL for a organization binder can be used to shop data that we only wish to persist for a short period of time:
permit temporaryFolderURL = URL(fileURLWithPath: NSTemporaryDirectory())
The aforementioned URL tin can likewise be retrieved using FileManager.default.temporaryDirectory
.
The do good of using the to a higher place APIs, rather than hard-coding specific folder paths inside our code, is that we're letting the arrangement determine what folders that are the almost appropriate for the chore at hand, which typically goes a long manner toward making code dealing with the file system more than portable and much more future-proof.
Managing custom folders
Although storing files direct inside folders that are managed past the organisation does take its use cases, chances are that we'll instead want to encapsulate our files within a folder of our own — specially when writing files to shared system folders (such as Documents
or Library
) on macOS, which could cause conflicts with other apps or user information if we're not careful.
This is another area in which FileManager
is really useful, as information technology provides a number of APIs that let us create, modify and delete custom folders. For instance, here'due south how nosotros could alter our FileIOController
from before to now shop its files within a nested MyAppFiles
folder, rather than within the Documents
folder directly:
struct FileIOController { var managing director = FileManager.default func write<T: Encodable>( _ object: T, toDocumentNamed documentName: String, encodedUsing encoder: JSONEncoder = .init() ) throws { permit rootFolderURL = try director.url( for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: imitation ) let nestedFolderURL = rootFolderURL.appendingPathComponent("MyAppFiles") try manager.createDirectory( at: nestedFolderURL, withIntermediateDirectories: false, attributes: nil ) let fileURL = nestedFolderURL.appendingPathComponent(documentName) let data = try encoder.encode(object) try information.write(to: fileURL) } ... }
The above code does have a quite major problem, though, and that's that we're currently attempting to create our nested folder every fourth dimension that our write
method is called — which will cause an error to be thrown if that folder already exists.
While nosotros could only prefix our telephone call to createDirectory
with try?
, rather than endeavour
, to fix that trouble — doing then would also silence any legitimate errors that could be thrown when we actually desire to create that folder, which wouldn't be ideal. So let's instead use some other FileManager
API, fileExists
, which tin also be used to check if a folder exists at a given path:
if !director.fileExists(atPath: nestedFolderURL.relativePath) { try manager.createDirectory( at: nestedFolderURL, withIntermediateDirectories: false, attributes: nil ) }
An optional isDirectory
parameter can also be passed to the fileExists
method if nosotros'd also also similar to check if the particular at the given path is indeed a folder, but doing so feels a chip redundant in the higher up instance.
Note how we're using the relativePath
property to convert our to a higher place nestedFolderURL
to a string-based path, rather than using absoluteString
, which is typically used when working with URLs pointing to a location on the internet. That's considering absoluteString
would yield a URL cord prefixed with the file://
scheme, which is not what nosotros want when passing a file URL to an API that accepts a file path.
Besides worth noting is that the above approach is really only condom within single-threaded contexts, or when our program is in complete control over the directories that it creates, since otherwise there's a risk that the folder in question could finish upwardly being created in between our fileExists
cheque and our call to createDirectory
. One style to handle such situations would be to ever endeavor to create the directory, and and then ignore any resulting error only if that fault matches the one thrown when a binder already existed — like this:
exercise { try manager.createDirectory( at: nestedFolderURL, withIntermediateDirectories: fake, attributes: null ) } catch CocoaError.fileWriteFileExists { // Folder already existed } take hold of { throw error }
Bitrise: Easily gear up up fast, rock-solid continuous integration for your project with Bitrise. In just a few minutes, you can set up builds, tests, and automated App Store and beta deployments for your project, all running in the cloud on every pull request and commit. Try it for free today.
Determination
Swift, and more specifically Foundation, ships with a quite comprehensive suite of file system APIs that enable usa to perform a big number of operations in ways that piece of work across all of Apple'southward platforms — and many of them are too fully Linux-compatible as well. While this article didn't aim to cover every unmarried API (that's what Apple'southward official documentation is for, after all), I hope that it has provided a somewhat concise overview of the various key APIs that are involved when information technology comes to working with files and folders in Swift.
For practical examples of some of the above APIs, and many more, feel costless to also check out my Files library, which acts as an object-oriented wrapper around system APIs similar FileManager
. And, if y'all have questions, comments, or feedback, then you lot're always welcome to contact me.
Thanks for reading! 🚀
Source: https://www.swiftbysundell.com/articles/working-with-files-and-folders-in-swift