Javascript required
Skip to content Skip to sidebar Skip to footer

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

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

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! 🚀

hoadleysece1966.blogspot.com

Source: https://www.swiftbysundell.com/articles/working-with-files-and-folders-in-swift