How to Build an Api for Uploading the Picture
Welcome to a new, hopefully heady tutorial! In a previous mail I showed to you the process of creating a custom class that manages spider web requests and RESTful APIs. Today, we volition keep building on it, as I would similar to focus on a specific apply example: How to upload files to a server!
Uploading files might not be i of the well-nigh common things when dealing with web services. However, it tin exist proved to be a tedious task to perform when information technology's time to send files to a server. In the implementation steps that follow we will try to interruption things downwards and shed calorie-free to the cardinal points and the details of the uploading procedure. Before we get there though, it's necessary to have a quick word about some footing knowledge that nosotros all should take on this topic.
A Quick Intro To "Multipart/form-information" Content Type
Before nosotros start doing actual piece of work, it'south necessary some important things to be mentioned first. Allow me offset by saying that in order to upload files to a server, multipart/form-information is the content type that should be specified in the spider web request. This content type allows to ship files or large amounts of data in combination with other usual data that should be posted. "Multipart/form-information" content blazon tells to HTTP request that posted data should exist broken into parts, as if they were to be posted by a spider web grade that expects from users to fill up in various fields and select files that should be submitted to a server.
Since posted data is broken into parts, it'south necessary for the server to know where a part starts and where it ends. For that purpose, a special and unique string is provided along with the content blazon, chosen boundary. That string should not occur in the actual data, so it must be as much unique equally possible. It e'er starts with two dashes ("–"), with an arbitrary combination of other alphanumeric characters coming afterwards. Unremarkably, boundaries start with multiple dashes, and then they have an alphanumeric suffix (e.g. —————–abc123).
Each role of a multipart body necessarily starts with a Content-Disposition header, with the class-data value coming in pair with information technology. An attribute called "name" should besides exist provided in the header, as information technology specifies the proper name of the part. Observe that names don't need to be unique, and sometimes server sets the rules that apply to the "name" aspect. These two cardinal-value pairs are enough when adding unmarried data (meaning no files) to the asking'south HTTP trunk. When appending files information, the filename should be besides included in the "Content-Disposition" header with the original name of the file, besides as the content blazon (MIME blazon) of each file that is about to be uploaded.
The following is a fake instance of a HTTP request body that uses the "multipart/form-data" content blazon:
ane ii 3 4 5 half-dozen 7 8 9 10 11 12 thirteen 14 xv sixteen 17 18 19 20 21 22 23 | Content-Type : multipart/form-information ; boundary=-----------------------------abc123 -----------------------------abc123 Content-Disposition : form-data ; name="username" usernameValue -----------------------------abc123 Content-Disposition : form-data ; name="countersign" passwordValue -----------------------------abc123 Content-Disposition : form-data ; name="aFile" ; filename="avatar.png" Content-Type : paradigm/png . . . contents of avatar . png file . . . -----------------------------abc123 Content-Disposition : form-data ; proper name="anotherFile" ; filename="info.pdf" Content-Type : awarding/pdf . . . contents of info . pdf file . . . -----------------------------abc123-- |
Notice how everything mentioned in the previous paragraphs is used. At showtime, the "multipart/class-data" content blazon is specified forth with the boundary cord that separates the data parts. See how purlieus indicates the beginning of each part and also meet how semicolon (";") separates attributes in headers. Line breaks are also important when building a HTTP trunk such the above i. In single fields, an empty line exists between the "Content-Disposition" header and the actual field value, while the purlieus of the side by side office comes right after in the next line. In file parts, the "filename" attribute contains the proper name of the file, while an additional empty line exists between the file contents and the side by side boundary. The torso catastrophe is highlighted past the purlieus, plus ii more than dashes equally a suffix to it.
I am encouraging you to take a await at the W3C HTML Specification and read more about encoding content types and the "multipart/course-information" especially. You don't take to terminate at that place of form; a full general search on the web will return lots of resources to read about this topic.
About The Demo App
And then, as I said in the get-go of this mail service, we are going to keep building on the custom class we created in the previous tutorial, called RestManager
. To go started, please download a starter packet which contains a Xcode projection with that course and one more directory with a demo server implementation (run into next part). In Xcode project yous will observe iii files that we'll utilize to test file uploading after we finish all implementation steps:
- A text file named SampleText.txt with "lorem ipsum" data generated here.
- A PDF file named SamplePDF.pdf taken from File Examples.
- An epitome file named SampleImage.jpg downloaded from Pexels (Photograph by Oleg Magni from Pexels).
No UI will exist in our app, and the results of our last tests will be printed in Xcode panel and in Terminal. Any input values will exist hard-coded. Therefore, we'll entirely focus on the file uploading characteristic that nosotros'll add together to the RestManager
class. Plainly, you are free to create any UI you desire if y'all want to create a more than dynamic demo application.
About The Server
After we finish implementing all the new lawmaking nosotros'll meet in the post-obit parts, nosotros'll need to test if file uploading is really working. For that purpose, a uncomplicated server implemented in Node.js is included in the starter parcel that y'all downloaded; you will notice information technology in the Server subdirectory. Yous can keep it in the location that currently is, or re-create it anywhere else yous want in your disk.
In gild to run the server, yous must have Node.js installed on your computer. If you don't, delight check here or here on how to do that. Open Terminal and type the following control:
There is a space character after the cd
command. And then switch to Finder, and drag and drib the Server directory to terminal and printing the Return key:
By doing and so, you don't have to type the path to the server directory; information technology'south automatically appended to the command in concluding.
To verify that you are successfully in the server directory, just blazon:
This command will evidence the electric current directory contents, and if you lot see something like to the side by side one, so you're merely fine:
To get-go the server merely type:
You should see the message:
Server started successfully on port 3000!
The server is now running at address http://localhost:3000. Y'all can also verify that if you paste that address in a new tab in your browser. You'll run into a message coming from the server.
Annotation: If y'all are already running another server at port 3000, edit the index.js file and set a custom port number to the port
variable. Then restart the server with the node index.js
command.
Requests made to "http" addresses are non allowed by default in iOS as they are considered insecure. However, for the sake of the tutorial, localhost has been whitelisted in the Info.plist file of the starter projection then you will encounter no trouble in testing the app later.
Representing Files
The first thing we demand to take care of is how files are going to be represented in the RestManager
class. For any file that is about to be uploaded, we need to have the post-obit data available at the time of the HTTP body grooming:
- The bodily file contents.
- The original file name. Call back that the filename aspect must exist in the "Content-Disposition" header of each part that represents a file.
- The office's name for the name aspect in the "Content-Disposition" header.
- The content type (MIME type) of the file.
Plainly, all that data could exist stored in a dictionary, just that wouldn't exist the best arroyo in Swift. To practice it better, permit'due south create a struct which we'll call FileInfo
. Open the RestManager.swift file in the starter Xcode project, and go to the finish of it. You will find the post-obit empty extension:
// MARK: - File Upload Related Implementation extension RestManager { } |
This is where we'll add almost all new code regarding the file uploading feature. Within this extension, add the post-obit construction:
struct FileInfo { var fileContents : Data ? var mimetype : String ? var filename : String ? var name : String ? } |
The four properties will go on the data described before. As you will see later, if any of the above properties is nil the file won't exist added to the HTTP body for submission to the server.
Nosotros can make the initialization of a FileInfo
object more friendly if we add together the following custom initializer:
struct FileInfo { . . . init ( withFileURL url : URL ? , filename : Cord , name : String , mimetype : String ) { guard let url = url else { render } fileContents = try ? Data ( contentsOf : url ) cocky . filename = filename self . name = name self . mimetype = mimetype } } |
With this initializer, it won't exist necessary to provide the actual file contents when creating a FileInfo
object. Specifying the URL of the file volition be plenty. File contents volition be read in the above initializer.
Creating The Purlieus
Having a solution on our hands virtually how to stand for files, permit's create a method which volition be responsible of creating the boundary string. Call up that a boundary must be unique and definitely non an ordinary string that could exist potentially institute in the actual data that will be uploaded. As I said in the beginning of the post, even though boundaries start with two dashes ("–"), they usually have several more dashes following and a random alphanumeric string at the end. That'south not mandatory, but information technology'southward the logic we volition follow here.
Right after the FileInfo
struct, define the following private method:
individual func createBoundary ( ) -> String ? { } |
I will show y'all two different ways to generate the random boundary string.
Using A UUID String
The fastest fashion to get a random string is to generate a UUID value:
var uuid = UUID ( ) . uuidString |
The in a higher place will generate something like to this:
D41568F4-7175-42BB-9503-DAA282180D70 |
Let's get rid of the dashes in that string, and let's convert all letters to lowercase:
uuid = uuid . replacingOccurrences ( of : "-" , with : "" ) uuid = uuid . map { $ 0 . lowercased ( ) } . joined ( ) |
The original UUID will now look like this:
d41568f4717542bb9503daa282180d70 |
Let's construct the boundary string. It will be a chain of twenty dashes at the start and the transformed UUID value:
let boundary = String ( repeating : "-" , count : 20 ) + uuid |
If you like exaggerating, add together the current timestamp to the end as well:
let boundary = String ( repeating : "-" , count : 20 ) + uuid + "\ ( Int ( Date . timeIntervalSinceReferenceDate ))" |
A purlieus string created with the above volition look like:
--------------------d41568f4717542bb9503daa282180d70579430569 |
Well, that looks quite unique and random, no?
Here's the implementation of the unabridged method:
private func createBoundary ( ) -> String ? { var uuid = UUID ( ) . uuidString uuid = uuid . replacingOccurrences ( of : "-" , with : "" ) uuid = uuid . map { $ 0 . lowercased ( ) } . joined ( ) let boundary = String ( repeating : "-" , count : 20 ) + uuid + "\ ( Int ( Appointment . timeIntervalSinceReferenceDate ))" render boundary } |
Using Random Characters
As an alternative to the above we tin create a machinery which will pick random characters from a collection of available characters, and using them to form a string which volition exist appended to the boundary string. The collection of available characters will exist parted by all messages ranging from upper cased "A" to "Z", lower cased "a" to "z", and all digits from "0" to "9".
Nosotros won't really need to hard-code anything, as nosotros tin programmatically construct everything. Nosotros will be based on the ASCII tabular array for that.
Nosotros'll start by specifying the range of the lower cased characters ("a" to "z") in the ASCII table as shown below:
let lowerCaseLettersInASCII = UInt8 ( ascii : "a" ) . . . UInt8 ( ascii : "z" ) |
The higher up is equivalent to this:
permit lowerCaseLettersInASCII = 97 . . . 122 |
where 97 is the position of the "a" character and "122" is the position of the "z" graphic symbol in the ASCII tabular array.
Still, the 2d line of code requires from us to search for an ASCII tabular array online and and then locate the position of the characters we are interested in into the table. Okay, it'due south piece of cake, but information technology'southward definitely not the recommended manner, since we can get the values nosotros desire by using the UInt8(ascii:)
initializer. And that's we practise in the first identify.
Similarly, we become the ranges of the upper cased A-Z and of the digits:
permit upperCaseLettersInASCII = UInt8 ( ascii : "A" ) . . . UInt8 ( ascii : "Z" ) permit digitsInASCII = UInt8 ( ascii : "0" ) . . . UInt8 ( ascii : "9" ) |
Now, permit's join all these ranges into a collection, or in other words a sequence of ranges (closed ranges more particularly) with aim to get the bodily characters afterwards:
let sequenceOfRanges = [ lowerCaseLettersInASCII , upperCaseLettersInASCII , digitsInASCII ] . joined ( ) |
If we print the value of the sequenceOfRanges
to the console at runtime we'll get this:
FlattenSequence < Array < ClosedRange < UInt8 > > > ( _base : [ ClosedRange ( 97 . . . 122 ) , ClosedRange ( 65 . . . xc ) , ClosedRange ( 48 . . . 57 ) ] ) |
Fifty-fifty though it's non obvious unless someone looks upwards for it, the above tin be easily converted into a String value:
baby-sit allow toString = Cord ( information : Data ( sequenceOfRanges ) , encoding : . utf8 ) else { return zip } |
Data
struct provides several initializers for creating a data object and at that place is one among them that accepts a sequence as an argument, exactly equally nosotros practise in the Information(sequenceOfRanges)
expression. From that data object, we can create the following cord which is assigned to the toString
constant:
abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789 |
That cool! Permit'southward generate a string of 20 random characters now:
var randomString = "" for _ in 0 . . < 20 { randomString += Cord ( toString . randomElement ( ) ! ) } |
At first we initialize a string value chosen randomString
. Then, we create a loop that will be executed 20 times. In information technology, nosotros pick a random grapheme from the toString
string using the randomElement()
method, and we generate a new String value (String(toString.randomElement()!)
). This new String value is appended to the randomString
.
Annotation that is safe to force unwrap the value of the randomElement()
method, every bit it returns nil just in cases of empty collections. Here we know that toString
won't be empty.
The following is a random value of the randomString
:
Finally, we can build the boundary string:
let boundary = String ( repeating : "-" , count : xx ) + randomString + "\ ( Int ( Date . timeIntervalSinceReferenceDate ))" |
Here is a sample of the boundary:
--------------------ZveNCE7Ptg3J2HaVLDfN579434247 |
The createBoundary()
method with the second implementation in one place:
private func createBoundary ( ) -> String ? { permit lowerCaseLettersInASCII = UInt8 ( ascii : "a" ) . . . UInt8 ( ascii : "z" ) let upperCaseLettersInASCII = UInt8 ( ascii : "A" ) . . . UInt8 ( ascii : "Z" ) let digitsInASCII = UInt8 ( ascii : "0" ) . . . UInt8 ( ascii : "nine" ) let sequenceOfRanges = [ lowerCaseLettersInASCII , upperCaseLettersInASCII , digitsInASCII ] . joined ( ) baby-sit let toString = String ( data : Data ( sequenceOfRanges ) , encoding : . utf8 ) else { return zilch } var randomString = "" for _ in 0 . . < 20 { randomString += String ( toString . randomElement ( ) ! ) } let boundary = String ( repeating : "-" , count : 20 ) + randomString + "\ ( Int ( Date . timeIntervalSinceReferenceDate ))" return boundary } |
Use the implementation you prefer the about. The second one is more "Swifty" but information technology requires a fleck of more lawmaking. At the end of the day, both approaches are going to piece of work every bit well.
An important note: I've mentioned already that the boundary string which separates the parts of a multipart body starts with two dashes ("–"). These two dashes are non included in the dashes of the boundary string nosotros generated in both approaches here. This string volition be provided as-is to the request as a request header along with the content type and server volition try to locate it after the two dashes prefix. As well, a boundary string can exist with no dashes at all; nosotros just add them to minimize the possibility to find similar string in the uploaded information. Every bit you will encounter subsequently, the two dashes prefix volition be manually appended whenever necessary.
Extending Data Structure
Our next steps involve the preparation of the HTTP trunk using whatever arbitrary data provided to the grade, as well as using the files information. Only before we get into that, nosotros will extend the Data
structure and we volition create the following generic method:
mutating func suspend < T > ( values : [ T ] ) -> Bool { } |
The purpose of this method is to let us easily append the values of the values
drove to the data object that calls it. And as yous'll run into, we'll exist interested for Cord
and Data
types only.
Just for clarification, we could avoid implementing this method. However, the code that we volition add to it would have to be repeated multiple times in different points in the RestManager
class, and that definitely would not be a wise motion.
So, to continue go to the end of the RestManager.swift file where you will find a Information
extension:
Add the new method'south definition in it:
extension Data { mutating func append < T > ( values : [ T ] ) -> Bool { } } |
At first, we'll declare the following two local variables:
var newData = Data ( ) var status = true |
Side by side, we'll distinguish the blazon of the given values. Permit'south start with the String type. In this case, nosotros'll make a loop to admission all values in the values
parameter collection:
if T . self == Cord . self { for value in values { } } |
In each repetition nosotros will convert the string value into a Data
object and we will suspend information technology to the local newData
variable. If for some reason the string value cannot be converted into a Data
object, we'll set the condition
flag to faux and nosotros'll suspension the loop.
guard let convertedString = ( value as ! String ) . data ( using : . utf8 ) else { status = false ; break } newData . append ( convertedString ) |
We will follow a quite like approach in case of Data
input values. Of form, there is no need to initialize any new Data
object or make a conversion of any blazon. We are appending i information value to some other:
else if T . self == Data . self { for value in values { newData . append ( value as ! Data ) } } |
Lastly, permit's signal that we don't care near any other type of values:
else { status = false } |
Next, we'll check the condition
value. If it's truthful, then we tin can append the newData
local variable to the self
object (the Data
object that is used to telephone call this method).
if status { self . append ( newData ) } |
At the terminate, we should not forget to return the status
as the outcome of the method:
Here's the entire implementation. We are going to put it in action starting from the next part.
one 2 3 four 5 6 7 8 9 10 11 12 thirteen 14 xv 16 17 18 19 twenty 21 22 23 24 25 | extension Information { mutating func suspend < T > ( values : [ T ] ) -> Bool { var newData = Data ( ) var status = true if T . self == String . cocky { for value in values { guard permit convertedString = ( value as ! String ) . data ( using : . utf8 ) else { status = faux ; break } newData . append ( convertedString ) } } else if T . self == Information . cocky { for value in values { newData . suspend ( value as ! Data ) } } else { status = false } if status { self . append ( newData ) } return status } } |
Creating the HTTP Body
In the electric current implementation of RestManager
there is a method named getHttpBody()
. Its purpose is to fix the HTTP body with the data that will exist posted to the server. Although this method works not bad in whatsoever other instance, unfortunately it's not of much help in case of file uploading. There is the boundary string nosotros accept to take into account, likewise as the special headers and formatting required when using the "multipart/form-data" content type. To serve our new needs, we'll implement a similarly named method which will be accepting the boundary cord every bit an argument (also known as method overloading).
In the new extension of the RestManager
form, right below the createBoundary
method, add the post-obit:
private func getHttpBody ( withBoundary purlieus : String ) -> Information { var body = Information ( ) render body } |
Continue in heed that the HTTP trunk must exist a Data
value, and so nosotros are initializing such a value in this method, and this is also what the method returns. In this method we'll bargain with any data that should exist posted to the server except for files. That's the data that would be normally submitted if in that location were no files to upload at the same fourth dimension, and information technology's kept in the httpBodyParameters
property (as a reminder, httpBodyParameters
is a holding in the RestManager
course and it's of RestEntity
type, a custom structure – find information technology in RestManager
and read more in the previous tutorial about information technology).
httpBodyParameters
has a method called allValues()
and returns all information equally a dictionary (a [Cord: Cord]
dictionary). We'll employ it to admission all values that should be sent to the server and append them to the trunk
variable. Right after the var body = Data()
line add the following:
for ( primal , value ) in httpBodyParameters . allValues ( ) { } |
A small stop here now equally we have to discuss what exactly we'll be appending to the body. Allow'south see once more part of the example presented in the beginning of this mail service:
-----------------------------abc123 Content-Disposition : form-data ; name="username" usernameValue -----------------------------abc123 Content-Disposition : grade-data ; name="password" passwordValue |
In this example the information is the username and the countersign. The post-obit apply to each piece of information:
- At first in that location is the boundary string, and right subsequently that a line suspension. In HTTP headers, a line interruption is marked with "\r\northward" (carriage return and new line character), not simply the "\n" that we are by and large used to. Programmatically, this could exist written like:
"--\(purlieus)\r\n"
(see the ii dashes before the boundary string). - Next, at that place is the "Content-Disposition" header with the
proper name
aspect only in it. Header is followed by a line pause 2 times. We could write this like and then:"Content-Disposition: form-data; proper noun=\"\(primal)\"\r\due north\r\north"
. - Lastly, it'south the bodily value followed by a line break. That'south easy:
"\(value)\r\due north"
.
We volition add the code that represents each step described higher up into an array:
permit values = [ "--\ ( purlieus )\r\n" , "Content-Disposition: form-information; name=\"\ ( cardinal )\"\r\north\r\n" , "\ ( value )\r\n" ] |
We will apply for first time the append(values:)
custom method we implemented in the previous step in gild to convert these strings into Data
objects and append them to the body
variable:
_ = body . append ( values : values ) |
And that'due south the last matter we had to do in this method. Let's run across information technology altogether at present:
private func getHttpBody ( withBoundary boundary : Cord ) -> Data { var body = Data ( ) for ( key , value ) in httpBodyParameters . allValues ( ) { allow values = [ "--\ ( boundary )\r\n" , "Content-Disposition: form-data; proper noun=\"\ ( key )\"\r\n\r\northward" , "\ ( value )\r\northward" ] _ = torso . append ( values : values ) } return body } |
We'll employ the results of this method in a while. For now, we have to add the files data to the HTTP body every bit well.
Adding Files To HTTP Body
One could say that the getHttpBody(withBoundary:)
method we just implemented along with the new one we will implement here consist of the most important part of the overall piece of work we take to do in order to make file uploading possible. And that would be pretty much true, as we've built all the helper methods we demand and at present nosotros are dealing with the core functionality.
Then, continuing on building the HTTP torso, let'due south ascertain the following new method:
private func add together ( files : [ FileInfo ] , toBody torso : inout Data , withBoundary boundary : String ) -> [ String ] ? { } |
Permit's talk first well-nigh the parameters. The starting time one is a collection of FileInfo
objects, and it contains the data for all files that are about to exist uploaded. The second parameter value is the data object that represents the HTTP body. Any changes that will exist made to that object inside this method will be reflected out of it likewise because it'southward marked with the inout
keyword. The last parameter is the purlieus string, every bit we necessarily demand information technology to separate data parts.
You might be wondering why this method returns an optional array of String values. Well, in case there are files whose data cannot be added to the HTTP trunk, then we'll keep their names into an assortment, which in turn the method volition return. In normal conditions this method should return naught, meaning that data from all files was successfully appended to the HTTP trunk information.
Let'southward start adding some code, with the get-go one existence the following local variables:
var status = true var failedFilenames : [ String ] ? |
status
volition point whether all pieces of data for each single file in the files
collection were successfully combined in one Data
object, which can be and then appended to the torso
inout parameter. If status
is false, we'll be appending the name of the matching file to the failedFilenames
array.
Let's offset a loop now:
The commencement thing we have to do is to make sure that all backdrop of each file
object have actual values then we tin can keep:
guard let filename = file . filename , let content = file . fileContents , allow mimetype = file . mimetype , let name = file . name else { continue } |
Side by side, we will set the initial value of the status
flag on each repetition of the loop to fake, and we'll initialize a new Data
object.
status = false var data = Data ( ) |
At present, let'due south run across again the instance presented in the beginning of the tutorial so we understand what we have to do:
-----------------------------abc123 Content-Disposition : form-data ; name="aFile" ; filename="avatar.png" Content-Type : image/png . . . contents of avatar . png file . . . -----------------------------abc123 Content-Disposition : grade-information ; proper name="anotherFile" ; filename="info.pdf" Content-Type : application/pdf . . . contents of info . pdf file . . . -----------------------------abc123-- |
Going pace past step through the lines that describe a file role:
- At kickoff in that location is the purlieus with the line intermission at the finish. Nosotros already know how to write that in lawmaking.
- Next, we have the "Content-Disposition" header. The addition here (comparison to the header in the previous part) is the new
filename
attribute which contains the actual file name. In code such a header is written like this:"Content-Disposition: grade-data; name=\"\(name)\"; filename=\"\(filename)\"\r\due north"
. - Right after we take the content type of the file. Come across all the available MIME Media Types. In code this is similar so:
"Content-Type: \(mimetype)\r\due north\r\n"
.
Allow's brand a suspension hither and allow'due south append all the above to an array:
let formattedFileInfo = [ "--\ ( purlieus )\r\north" , "Content-Disposition: grade-data; proper name=\"\ ( proper name )\"; filename=\"\ ( filename )\"\r\n" , "Content-Type: \ ( mimetype )\r\n\r\n" ] |
Let's convert all strings in that array into Data
objects and append them to the information
variable:
if data . append ( values : formattedFileInfo ) { } |
Let's keep where we had stopped from. The next item in a file part is the actual file contents. Think that file contents are represented by the fileContents
belongings in a FileInfo
object, which is a Data
object. So far nosotros were dealing with strings only. File contents must exist appended to the data
variable likewise:
if data . append ( values : [ content ] ) { } |
Remember that append(values:)
method expects for an array of values, and then it'southward necessary to include content
into the assortment'south opening and endmost brackets to a higher place.
Lastly, notice in the above instance that there is an empty line right after the file contents which should be added to the data
besides:
if information . append ( values : [ "\r\north" ] ) { } |
These three conditions we wrote must exist embedded into each other. If all of them are true, so all data pieces for the electric current file were successfully added to the information
object, and we'll betoken that by making the status
true:
if data . suspend ( values : formattedFileInfo ) { if data . append ( values : [ content ] ) { if data . append ( values : [ "\r\due north" ] ) { status = true } } } |
See that nosotros used the custom append(values:)
custom method iii times in a row here. I promise you agree that its implementation was meaningful since we use it once more and once more.
Next, let'due south bank check the status
value for each file. While nevertheless existence on the loop:
if condition { trunk . append ( data ) } else { if failedFilenames == zilch { failedFilenames = [ String ] ( ) } failedFilenames ? . append ( filename ) } |
If status
is true, we append the data
variable to the body
which represents the HTTP body. If not, then nosotros initialize the failedFilenames
array in case information technology's not initialized already, and we keep the proper noun of the electric current file in it.
Ane last thing remaining, to return the failedFilenames
from the method:
Our new method should now await like this:
1 2 3 4 5 6 7 viii 9 ten eleven 12 xiii 14 15 sixteen 17 18 nineteen 20 21 22 23 24 25 26 27 28 29 thirty 31 32 33 34 | individual func add ( files : [ FileInfo ] , toBody body : inout Information , withBoundary boundary : String ) -> [ String ] ? { var status = truthful var failedFilenames : [ String ] ? for file in files { guard allow filename = file . filename , permit content = file . fileContents , permit mimetype = file . mimetype , permit name = file . name else { continue } status = simulated var data = Information ( ) let formattedFileInfo = [ "--\ ( boundary )\r\due north" , "Content-Disposition: form-data; name=\"\ ( name )\"; filename=\"\ ( filename )\"\r\due north" , "Content-Type: \ ( mimetype )\r\n\r\n" ] if data . append ( values : formattedFileInfo ) { if information . append ( values : [ content ] ) { if information . suspend ( values : [ "\r\n" ] ) { status = true } } } if status { body . suspend ( data ) } else { if failedFilenames == nil { failedFilenames = [ String ] ( ) } failedFilenames ? . suspend ( filename ) } } return failedFilenames } |
Closing The HTTP Body
Now that we created methods which build the HTTP body past appending any post data and file data, nosotros must create one more than which volition shut the body. Remember that in "multipart/grade-data" the HTTP torso closing is marked by the purlieus string and two dashes as a suffix to it:
-----------------------------abc123-- |
Every bit you can gauge, doing then doesn't require much of work as all it takes is this:
private func close ( body : inout Data , usingBoundary boundary : String ) { _ = body . append ( values : [ "\r\n--\ ( boundary )--\r\n" ] ) } |
For one more than time here the body
parameter is marked as inout
, so the information argument will be passed past reference and the changes made to it inside this method will get visible to the caller likewise. Likewise that, notice the line breaks before and afterwards the closing cord which ensure that the closing boundary will be the only content in the line.
Information technology's really important non to forget to call this method and point the end of parts in the multipart HTTP trunk.
Uploading Files
It'southward about time to put everything together and make file uploading possible. The method we'll write hither volition exist public, so you can go and add it to the top of the class forth with other two public methods existing already. Here is its definition:
func upload ( files : [ FileInfo ] , toURL url : URL , withHttpMethod httpMethod : HttpMethod , completion : @ escaping ( _ result : Results , _ failedFiles : [ String ] ? ) -> Void ) { } |
In accordance to what nosotros did to the other two existing public methods, we are going to perform all actions in this method asynchronously. Nosotros won't run annihilation on the main thread since file uploading could have pregnant amount of fourth dimension and nosotros don't want apps to bear witness frozen. In code that ways:
DispatchQueue . global ( qos : . userInitiated ) . async { [ weak self ] in } |
With userInitiated
value in the quality of service parameter we requite our chore a relatively high priority in execution. Annotation that we marker self
every bit weak in the closure since the RestManager
instance used to perform the file uploading can potentially get zippo, and that practically ways that self
is from at present on an optional. This introduces a couple of new needs as you will run into next.
The kickoff actual action nosotros have to have is to add together any URL query parameters specified in the urlQueryParameters
property to the URL. This will happen by calling the addURLQueryParameters(toURL:)
method which we implemented in the previous tutorial:
allow targetURL = self ? . addURLQueryParameters ( toURL : url ) |
Next, let's call the createBoundary()
method nosotros implemented today and let'south create the boundary string:
guard allow purlieus = self ? . createBoundary ( ) else { completion ( Results ( withError : CustomError . failedToCreateBoundary ) , null ) ; return } |
Find that since cocky
is used as an optional, purlieus
becomes an optional value as well, regardless of the fact that createBoundary()
does not return an optional. So, in case there's no boundary string to continue, we telephone call the completion handler passing the mistake shown above and we render from the method. This custom error doesn't exist yet in the class, we'll add it in a while.
Let's get going, and in the next step let's add together the "multipart/form-information" along with the boundary string to the collection of the request headers:
self ? . requestHttpHeaders . add ( value : "multipart/form-information; boundary=\ ( boundary )" , forKey : "content-type" ) |
To refresh your memory, requestHttpHeaders
is a RestEntity
holding which keeps all HTTP request headers as fundamental-value pairs. Information technology'south important to highlight that since we specify the content type header here, there is no need to provide a content blazon header manually while preparing the request. Not simply it'southward redundant, it's also dangerous as information technology could create conflicts and brand the server reject the request.
Next, let's first preparing the HTTP body. We'll start by calling the getHttpBody(withBoundary:)
method:
guard var body = self ? . getHttpBody ( withBoundary : purlieus ) else { completion ( Results ( withError : CustomError . failedToCreateHttpBody ) , nil ) ; return } |
Over again, since self
is an optional, body
might exist naught in example self
is nil. So, in that case nosotros call the completion handler with another custom error and we return from the method.
Time to add the files to be uploaded to the HTTP body. Notice in the side by side line that nosotros pass the trunk
variable with the "&" symbol as that's an inout
parameter value:
let failedFilenames = cocky ? . add ( files : files , toBody : &body , withBoundary : boundary ) |
failedFilenames
is either nothing if all files are successfully added to the HTTP torso, or it contains the names of those files that failed to be appended to the body.
We should not forget to close the HTTP body properly:
self ? . close ( body : &torso , usingBoundary : boundary ) |
We are prepare now to create the URL request:
baby-sit let request = cocky ? . prepareRequest ( withURL : targetURL , httpBody : body , httpMethod : httpMethod ) else { completion ( Results ( withError : CustomError . failedToCreateRequest ) , nil ) ; return } |
The method we use here is already implemented in the RestManager
class and we discussed about it in the previous tutorial. Observe that we laissez passer the URL with whatsoever potential query items (targetURL
) and the HTTP body as arguments.
Finally, we'll create a new URLSession
and an upload task to make the request. Upon completion, nosotros'll call the completion handler and we'll laissez passer a Results
object with data regarding the results of the asking, and the failedFiles
array.
let sessionConfiguration = URLSessionConfiguration . default let session = URLSession ( configuration : sessionConfiguration ) let task = session . uploadTask ( with : request , from : nil , completionHandler : { ( data , response , error ) in completion ( Results ( withData : information , response : Response ( fromURLResponse : response ) , fault : error ) , failedFilenames ) } ) task . resume ( ) |
The upload method is at present set:
1 2 iii 4 five half dozen 7 8 9 10 11 12 13 14 xv sixteen 17 18 nineteen 20 21 22 23 24 25 26 27 | func upload ( files : [ FileInfo ] , toURL url : URL , withHttpMethod httpMethod : HttpMethod , completion : @ escaping ( _ result : Results , _ failedFiles : [ String ] ? ) -> Void ) { DispatchQueue . global ( qos : . userInitiated ) . async { [ weak self ] in permit targetURL = self ? . addURLQueryParameters ( toURL : url ) baby-sit let boundary = cocky ? . createBoundary ( ) else { completion ( Results ( withError : CustomError . failedToCreateBoundary ) , naught ) ; render } self ? . requestHttpHeaders . add ( value : "multipart/class-data; boundary=\ ( boundary )" , forKey : "content-type" ) guard var body = cocky ? . getHttpBody ( withBoundary : boundary ) else { completion ( Results ( withError : CustomError . failedToCreateHttpBody ) , naught ) ; return } permit failedFilenames = self ? . add ( files : files , toBody : &body, withBoundary: boundary) self?.close(body: &body, usingBoundary: purlieus) guard let request = self?.prepareRequest(withURL: targetURL, httpBody: body, httpMethod: httpMethod) else { completion(Results(withError: CustomError.failedToCreateRequest), nil); render } let sessionConfiguration = URLSessionConfiguration . default let session = URLSession ( configuration : sessionConfiguration ) let task = session . uploadTask ( with : asking , from : nil , completionHandler : { ( data , response , error ) in completion ( Results ( withData : information , response : Response ( fromURLResponse : response ) , error : error ) , failedFilenames ) } ) job . resume ( ) } } |
There is one last thing to do earlier nosotros test out everything. To add the 2 new custom errors to the CustomError
enum. Find it in the RestManager
form and update information technology as shown side by side:
enum CustomError : Error { case failedToCreateRequest case failedToCreateBoundary case failedToCreateHttpBody } |
Update its extension right below appropriately with the description of the messages:
extension RestManager . CustomError : LocalizedError { public var localizedDescription : String { switch cocky { instance . failedToCreateRequest : render NSLocalizedString ( "Unable to create the URLRequest object" , comment : "" ) case . failedToCreateBoundary : return NSLocalizedString ( "Unable to create purlieus string" , annotate : "" ) case . failedToCreateHttpBody : return NSLocalizedString ( "Unable to create HTTP body parameters information" , comment : "" ) } } } |
That's it! Time to upload files!
Testing File Uploading
The time to test file uploading has finally come. Switch to the ViewController.swift
file and add together the following method definition:
func uploadSingleFile ( ) { } |
For starters, we are going to upload a unmarried file only, and here nosotros will prepare the FileInfo
object that volition contain its data.
Before nosotros go on, let me remind you that in the starter Xcode project y'all downloaded in that location are three files for testing: "sampleText.txt", "samplePDF.txt" and "sampleImage.pdf". We'll utilise the "sampleText.txt" here, but feel free to alter and apply any other file you want. Sample files exist in the application'due south bundle just for making the example as simple as possible, only in real apps the yous'll about always fetch them from the documents directory.
So, allow's start past creating a FileInfo
object:
func uploadSingleFile ( ) { let fileURL = Bundle . main . url ( forResource : "sampleText" , withExtension : "txt" ) let fileInfo = RestManager . FileInfo ( withFileURL : fileURL , filename : "sampleText.txt" , proper noun : "uploadedFile" , mimetype : "text/plain" ) } |
Run into that we are using the custom initializer we created in the FileInfo
construction hither. However, in example y'all don't desire to initialize a FileInfo
object that way and you prefer to manually set all values including the files contents, here's your alternative:
var fileInfo = RestManager . FileInfo ( ) fileInfo . filename = "sampleText.txt" fileInfo . name = "uploadedFile" fileInfo . mimetype = "text/patently" if let fileURL = Bundle . main . url ( forResource : "sampleText" , withExtension : "txt" ) { fileInfo . fileContents = try ? Data ( contentsOf : fileURL ) } |
Notation: Server is implemented in a manner that requires the name
attribute in every part of the multipart body to have the "uploadedFile" value. Therefore, that'southward the value that we'll be setting in the proper name
property of each FileInfo
object nosotros create here.
The URL where we'll make the asking to upload the file is: http://localhost:3000/upload
. We will pass a URL object forth with an array that will contain the fileInfo
object as arguments to a new method (we'll implement it right next):
upload ( files : [ fileInfo ] , toURL : URL ( cord : "http://localhost:3000/upload" ) ) |
upload(files:toURL:)
is a small method responsible for making the asking every bit you can meet next. Nosotros could have put that code in the uploadSingleFile()
method, but we'll use it again in a while when we'll upload multiple files. And so, nosotros'd improve avoid repeating code.
i two iii four five six 7 eight 9 10 eleven 12 13 14 fifteen 16 17 eighteen 19 20 21 22 23 | func upload ( files : [ RestManager . FileInfo ] , toURL url : URL ? ) { if let uploadURL = url { rest . upload ( files : files , toURL : uploadURL , withHttpMethod : . postal service ) { ( results , failedFilesList ) in print ( "HTTP status lawmaking:" , results . response ? . httpStatusCode ? ? 0 ) if allow fault = results . error { print ( error ) } if let data = results . data { if let toDictionary = endeavor ? JSONSerialization . jsonObject ( with : data , options : . mutableContainers ) { print ( toDictionary ) } } if let failedFiles = failedFilesList { for file in failedFiles { print ( file ) } } } } } |
In the completion handler we don't do anything particular. We merely impress the HTTP status lawmaking, we brandish whatever potential errors, and the server's response subsequently nosotros convert it from JSON to a dictionary object. Of course, nosotros also impress the list of failed to exist uploaded files (in case in that location is any).
In the viewDidLoad()
method telephone call the uploadSingleFile()
:
override func viewDidLoad ( ) { super . viewDidLoad ( ) uploadSingleFile ( ) } |
Run the app now and look at both in Xcode console and in the terminal where the server's output is printed. If yous followed everything step by step up until here, you should become this in Xcode:
HTTP status lawmaking : 200 { result = 1 ; } |
At the same time, in terminal you should have the details of the uploaded file:
{ fieldname : 'uploadedFile' , originalname : 'sampleText.txt' , encoding : '7bit' , mimetype : 'text/plainly' , destination : 'uploads/' , filename : 'sampleText.txt' , path : 'uploads/sampleText.txt' , size : 5575 } |
I wanted to make the small demo server and the file uploading procedure behave every bit much naturally equally possible, and then files sent to this server implementation are actually… being uploaded! In Finder, become to the Server directory that you downloaded in the starter package and then into the subdirectory called "uploads". The uploaded file is there which proves that file uploading is actually working!
Let'south make our testing more than interesting by as well sending boosted data along with the request. Right later on the initialization of the FileInfo
object in the uploadSingleFile()
method add together the following two lines:
rest . httpBodyParameters . add ( value : "Hello 😀 !!!" , forKey : "greeting" ) rest . httpBodyParameters . add ( value : "AppCoda" , forKey : "user" ) |
Run the app again. In the terminal you should see the additional uploaded data besides:
{ fieldname : 'uploadedFile' , originalname : 'sampleText.txt' , encoding : '7bit' , mimetype : 'text/obviously' , destination : 'uploads/' , filename : 'sampleText.txt' , path : 'uploads/sampleText.txt' , size : 5575 } [ Object : null prototype ] { user : 'AppCoda' , greeting : 'Hello 😀 !!!' } |
Allow'south upload multiple files at present. Nosotros'll do that by creating a new method like to the previous one, with the departure existence that instead of initializing one FileInfo
object, nosotros'll initialize three of them so we tin upload all sample files we have. Here it is:
func uploadMultipleFiles ( ) { let textFileURL = Bundle . main . url ( forResource : "sampleText" , withExtension : "txt" ) let textFileInfo = RestManager . FileInfo ( withFileURL : textFileURL , filename : "sampleText.txt" , name : "uploadedFile" , mimetype : "text/plainly" ) permit pdfFileURL = Bundle . main . url ( forResource : "samplePDF" , withExtension : "pdf" ) let pdfFileInfo = RestManager . FileInfo ( withFileURL : pdfFileURL , filename : "samplePDF.pdf" , name : "uploadedFile" , mimetype : "application/pdf" ) let imageFileURL = Parcel . main . url ( forResource : "sampleImage" , withExtension : "jpg" ) let imageFileInfo = RestManager . FileInfo ( withFileURL : imageFileURL , filename : "sampleImage.jpg" , name : "uploadedFile" , mimetype : "epitome/jpg" ) upload ( files : [ textFileInfo , pdfFileInfo , imageFileInfo ] , toURL : URL ( cord : "http://localhost:3000/multiupload" ) ) } |
At the end we telephone call once more the upload(files:toURL:)
method which volition trigger the actual upload asking. Notice that the upload endpoint is different this fourth dimension ("multiupload"). To see it working, don't forget to telephone call it in the viewDidLoad()
:
override func viewDidLoad ( ) { super . viewDidLoad ( ) //uploadSingleFile() uploadMultipleFiles ( ) } |
This fourth dimension y'all should run across the names of the uploaded files in last:
Received files : -sampleText . txt -samplePDF . pdf -sampleImage . jpg |
Note that the current server implementation supports up to 10 simultaneous files to be uploaded. Of course y'all are gratis to change that limit according to your preference.
Summary
Starting in the previous tutorial where we created the commencement version of the RestManager
class and continuing in this ane where we added the file uploading feature, we have managed to build a pocket-size and lightweight class capable of covering our needs in making web requests. "Multipart/class-information" content type and the way HTTP body is congenital can exist sometimes confusing, but if you intermission things downwardly then everything gets easy. I promise what I shared with you lot here today to be of some value, and I wish yous are enjoying RESTful services even more than now. You are always welcome to add together more features or adjust the electric current implementation according to your needs. Come across you next time!
For reference, you can download the full projection on GitHub.
Source: https://www.appcoda.com/restful-api-tutorial-how-to-upload-files-to-server/
0 Response to "How to Build an Api for Uploading the Picture"
Post a Comment