How To Send Document As Multipart/form-data Frim Rest Web Service
Welcome to a new, hopefully heady tutorial! In a previous post I showed to y'all the process of creating a custom form that manages web requests and RESTful APIs. Today, we volition keep edifice on it, every bit I would like to focus on a specific apply instance: How to upload files to a server!
Uploading files might not be one of the most common things when dealing with web services. Yet, it can be proved to be a ho-hum chore to perform when it's time to transport files to a server. In the implementation steps that follow we will try to break things down and shed light to the key points and the details of the uploading process. Before we go in that location though, information technology's necessary to have a quick discussion about some footing cognition that we all should take on this topic.
A Quick Intro To "Multipart/form-data" Content Type
Before we start doing actual work, it's necessary some important things to be mentioned offset. Permit me start past proverb that in order to upload files to a server, multipart/form-data is the content type that should exist specified in the spider web request. This content type allows to send files or big amounts of data in combination with other usual data that should be posted. "Multipart/course-information" content type tells to HTTP asking that posted data should be cleaved into parts, equally if they were to be posted by a spider web course that expects from users to make full in various fields and select files that should be submitted to a server.
Since posted data is broken into parts, it's necessary for the server to know where a role starts and where it ends. For that purpose, a special and unique string is provided along with the content type, called boundary. That string should not occur in the bodily information, so information technology must be equally much unique equally possible. It always starts with two dashes ("–"), with an capricious combination of other alphanumeric characters coming afterwards. Usually, boundaries first with multiple dashes, and then they have an alphanumeric suffix (eastward.1000. —————–abc123).
Each part of a multipart trunk necessarily starts with a Content-Disposition header, with the form-data value coming in pair with information technology. An attribute chosen "proper name" should also be provided in the header, as information technology specifies the name of the role. Notice that names don't need to exist unique, and sometimes server sets the rules that apply to the "proper noun" aspect. These two key-value pairs are enough when adding single data (meaning no files) to the request's HTTP trunk. When appending files information, the filename should be besides included in the "Content-Disposition" header with the original proper noun of the file, as well as the content blazon (MIME blazon) of each file that is well-nigh to exist uploaded.
The following is a false example of a HTTP request body that uses the "multipart/form-information" content blazon:
1 2 3 iv 5 6 7 8 nine x eleven 12 xiii xiv 15 16 17 18 19 xx 21 22 23 | Content-Type : multipart/form-data ; boundary=-----------------------------abc123 -----------------------------abc123 Content-Disposition : form-data ; name="username" usernameValue -----------------------------abc123 Content-Disposition : course-data ; name="countersign" passwordValue -----------------------------abc123 Content-Disposition : form-information ; name="aFile" ; filename="avatar.png" Content-Type : image/png . . . contents of avatar . png file . . . -----------------------------abc123 Content-Disposition : form-data ; name="anotherFile" ; filename="info.pdf" Content-Type : application/pdf . . . contents of info . pdf file . . . -----------------------------abc123-- |
Observe how everything mentioned in the previous paragraphs is used. At first, the "multipart/form-information" content type is specified along with the boundary string that separates the information parts. Run into how purlieus indicates the beginning of each part and likewise encounter how semicolon (";") separates attributes in headers. Line breaks are also important when building a HTTP body such the above i. In single fields, an empty line exists between the "Content-Disposition" header and the actual field value, while the boundary of the side by side part comes correct after in the side by side line. In file parts, the "filename" aspect contains the name of the file, while an boosted empty line exists betwixt the file contents and the next purlieus. The torso ending is highlighted past the purlieus, plus two more dashes as a suffix to information technology.
I am encouraging you to take a look at the W3C HTML Specification and read more most encoding content types and the "multipart/form-data" especially. You don't have to stop at that place of form; a full general search on the web will render lots of resources to read about this topic.
About The Demo App
Then, as I said in the beginning of this post, we are going to go along edifice on the custom grade nosotros created in the previous tutorial, called RestManager
. To go started, please download a starter package which contains a Xcode project with that class and ane more directory with a demo server implementation (see next role). In Xcode project y'all will find three files that we'll use to test file uploading afterward we finish all implementation steps:
- A text file named SampleText.txt with "lorem ipsum" information generated here.
- A PDF file named SamplePDF.pdf taken from File Examples.
- An epitome file named SampleImage.jpg downloaded from Pexels (Photo by Oleg Magni from Pexels).
No UI volition exist in our app, and the results of our final tests will be printed in Xcode console and in Terminal. Whatsoever input values volition exist hard-coded. Therefore, nosotros'll entirely focus on the file uploading feature that we'll add together to the RestManager
grade. Patently, y'all are gratuitous to create whatsoever UI you want if y'all want to create a more than dynamic demo application.
Almost The Server
After we finish implementing all the new code nosotros'll run into in the post-obit parts, we'll need to exam if file uploading is actually working. For that purpose, a unproblematic server implemented in Node.js is included in the starter packet that yous downloaded; you lot will find it in the Server subdirectory. Y'all can keep it in the location that currently is, or copy information technology anywhere else yous want in your deejay.
In order to run the server, you lot must have Node.js installed on your computer. If you don't, please check here or here on how to do that. Open up Terminal and type the following command:
There is a space character afterward the cd
control. Then switch to Finder, and drag and drop the Server directory to terminal and printing the Return key:

By doing so, you don't accept to type the path to the server directory; information technology'south automatically appended to the command in terminal.

To verify that you are successfully in the server directory, just type:
This command volition testify the current directory contents, and if you meet something similar to the side by side one, then you're just fine:

To beginning the server just type:
Yous should see the message:
Server started successfully on port 3000!

The server is now running at accost http://localhost:3000. Yous can also verify that if you paste that address in a new tab in your browser. You'll run across a message coming from the server.
Note: If yous are already running another server at port 3000, edit the alphabetize.js file and prepare a custom port number to the port
variable. So restart the server with the node alphabetize.js
command.
Requests made to "http" addresses are not immune by default in iOS equally they are considered insecure. However, for the sake of the tutorial, localhost has been whitelisted in the Info.plist file of the starter project and then you will see no problem in testing the app after.
Representing Files
The first thing we need to have care of is how files are going to be represented in the RestManager
class. For whatsoever file that is about to be uploaded, we need to take the following information bachelor at the fourth dimension of the HTTP body preparation:
- The actual file contents.
- The original file proper name. Recall that the filename attribute must exist in the "Content-Disposition" header of each part that represents a file.
- The function's name for the name aspect in the "Content-Disposition" header.
- The content type (MIME type) of the file.
Plainly, all that information could be stored in a dictionary, but that wouldn't be the best arroyo in Swift. To do it better, allow'southward create a struct which we'll call FileInfo
. Open the RestManager.swift file in the starter Xcode project, and become to the finish of it. Yous will find the post-obit empty extension:
// Marking: - File Upload Related Implementation extension RestManager { } |
This is where we'll add almost all new code regarding the file uploading feature. Inside this extension, add the following structure:
struct FileInfo { var fileContents : Data ? var mimetype : Cord ? var filename : Cord ? var name : String ? } |
The four properties volition keep the information described before. Every bit you will come across later, if whatever of the above properties is zero the file won't be added to the HTTP body for submission to the server.
We can make the initialization of a FileInfo
object more friendly if we add the following custom initializer:
struct FileInfo { . . . init ( withFileURL url : URL ? , filename : String , name : String , mimetype : String ) { guard permit url = url else { render } fileContents = endeavour ? Data ( contentsOf : url ) self . filename = filename self . name = name cocky . mimetype = mimetype } } |
With this initializer, it won't be necessary to provide the actual file contents when creating a FileInfo
object. Specifying the URL of the file volition be enough. File contents will exist read in the in a higher place initializer.
Creating The Boundary
Having a solution on our hands about how to represent files, let'south create a method which will be responsible of creating the boundary string. Remember that a purlieus must be unique and definitely not an ordinary string that could exist potentially found in the bodily information that will be uploaded. As I said in the start of the post, even though boundaries start with two dashes ("–"), they usually have several more dashes post-obit and a random alphanumeric cord at the stop. That'south non mandatory, only it's the logic we will follow here.
Right after the FileInfo
struct, define the following private method:
private func createBoundary ( ) -> String ? { } |
I volition prove yous ii unlike ways to generate the random purlieus cord.
Using A UUID Cord
The fastest way to get a random string is to generate a UUID value:
var uuid = UUID ( ) . uuidString |
The in a higher place will generate something similar to this:
D41568F4-7175-42BB-9503-DAA282180D70 |
Let'southward 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 volition at present look like this:
d41568f4717542bb9503daa282180d70 |
Let's construct the boundary string. It volition be a concatenation of twenty dashes at the get-go and the transformed UUID value:
let boundary = String ( repeating : "-" , count : twenty ) + uuid |
If you like exaggerating, add the electric current timestamp to the terminate also:
let boundary = String ( repeating : "-" , count : 20 ) + uuid + "\ ( Int ( Engagement . timeIntervalSinceReferenceDate ))" |
A boundary string created with the above will wait like:
--------------------d41568f4717542bb9503daa282180d70579430569 |
Well, that looks quite unique and random, no?
Here'southward 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 purlieus = String ( repeating : "-" , count : 20 ) + uuid + "\ ( Int ( Date . timeIntervalSinceReferenceDate ))" render purlieus } |
Using Random Characters
Equally an alternative to the to a higher place we can create a machinery which will pick random characters from a drove of bachelor characters, and using them to grade a cord which will exist appended to the purlieus string. The drove of available characters will be parted past all messages ranging from upper cased "A" to "Z", lower cased "a" to "z", and all digits from "0" to "9".
Nosotros won't actually demand to difficult-code anything, as we tin programmatically construct everything. We will be based on the ASCII table for that.
We'll start by specifying the range of the lower cased characters ("a" to "z") in the ASCII tabular array as shown beneath:
let lowerCaseLettersInASCII = UInt8 ( ascii : "a" ) . . . UInt8 ( ascii : "z" ) |
The above is equivalent to this:
allow lowerCaseLettersInASCII = 97 . . . 122 |
where 97 is the position of the "a" character and "122" is the position of the "z" character in the ASCII table.
Still, the 2d line of lawmaking requires from us to search for an ASCII table online and and so locate the position of the characters we are interested in into the table. Okay, it's easy, just it's definitely not the recommended fashion, since we can get the values nosotros want past using the UInt8(ascii:)
initializer. And that's we do in the beginning place.
Similarly, we get the ranges of the upper cased A-Z and of the digits:
permit upperCaseLettersInASCII = UInt8 ( ascii : "A" ) . . . UInt8 ( ascii : "Z" ) let digitsInASCII = UInt8 ( ascii : "0" ) . . . UInt8 ( ascii : "9" ) |
Now, let'south bring together all these ranges into a collection, or in other words a sequence of ranges (airtight ranges more particularly) with aim to get the actual characters afterward:
allow sequenceOfRanges = [ lowerCaseLettersInASCII , upperCaseLettersInASCII , digitsInASCII ] . joined ( ) |
If nosotros print the value of the sequenceOfRanges
to the console at runtime we'll go this:
FlattenSequence < Array < ClosedRange < UInt8 > > > ( _base : [ ClosedRange ( 97 . . . 122 ) , ClosedRange ( 65 . . . 90 ) , ClosedRange ( 48 . . . 57 ) ] ) |
Even though it'due south not obvious unless someone looks up for it, the above can be easily converted into a String value:
guard let toString = String ( information : Data ( sequenceOfRanges ) , encoding : . utf8 ) else { render nil } |
Data
struct provides several initializers for creating a data object and there is one amid them that accepts a sequence as an argument, exactly as nosotros practice in the Information(sequenceOfRanges)
expression. From that data object, we tin can create the following string which is assigned to the toString
constant:
abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789 |
That cool! Allow's generate a string of 20 random characters at present:
var randomString = "" for _ in 0 . . < 20 { randomString += String ( toString . randomElement ( ) ! ) } |
At first we initialize a string value called randomString
. And so, we create a loop that will be executed twenty times. In it, we selection a random character from the toString
cord using the randomElement()
method, and we generate a new String value (String(toString.randomElement()!)
). This new String value is appended to the randomString
.
Note that is safety to forcefulness unwrap the value of the randomElement()
method, equally it returns nil but in cases of empty collections. Hither nosotros know that toString
won't be empty.
The post-obit is a random value of the randomString
:
Finally, nosotros tin build the boundary string:
let boundary = String ( repeating : "-" , count : 20 ) + randomString + "\ ( Int ( Date . timeIntervalSinceReferenceDate ))" |
Here is a sample of the boundary:
--------------------ZveNCE7Ptg3J2HaVLDfN579434247 |
The createBoundary()
method with the second implementation in 1 place:
individual func createBoundary ( ) -> String ? { let lowerCaseLettersInASCII = UInt8 ( ascii : "a" ) . . . UInt8 ( ascii : "z" ) permit upperCaseLettersInASCII = UInt8 ( ascii : "A" ) . . . UInt8 ( ascii : "Z" ) permit digitsInASCII = UInt8 ( ascii : "0" ) . . . UInt8 ( ascii : "9" ) allow sequenceOfRanges = [ lowerCaseLettersInASCII , upperCaseLettersInASCII , digitsInASCII ] . joined ( ) guard let toString = String ( data : Data ( sequenceOfRanges ) , encoding : . utf8 ) else { render cipher } var randomString = "" for _ in 0 . . < twenty { randomString += String ( toString . randomElement ( ) ! ) } let boundary = String ( repeating : "-" , count : xx ) + randomString + "\ ( Int ( Date . timeIntervalSinceReferenceDate ))" return boundary } |
Utilise the implementation you prefer the most. The second ane is more "Swifty" merely it requires a bit of more code. At the finish of the mean solar day, both approaches are going to work equally well.
An important note: I've mentioned already that the boundary string which separates the parts of a multipart torso starts with 2 dashes ("–"). These ii dashes are not included in the dashes of the boundary string we generated in both approaches hither. This cord volition be provided as-is to the asking as a request header along with the content blazon and server will try to locate it after the two dashes prefix. Also, a boundary string can exist with no dashes at all; we just add them to minimize the possibility to find like cord in the uploaded data. As you will see later, the two dashes prefix volition be manually appended whenever necessary.
Extending Data Structure
Our next steps involve the preparation of the HTTP body using any arbitrary data provided to the class, too as using the files data. But before we go 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 usa easily suspend the values of the values
collection to the data object that calls information technology. And as you'll run across, we'll exist interested for String
and Data
types simply.
Just for clarification, we could avoid implementing this method. However, the code that we will add to information technology would take to be repeated multiple times in unlike points in the RestManager
class, and that definitely would not be a wise movement.
So, to go along go to the cease of the RestManager.swift file where you volition find a Data
extension:
Add together the new method'south definition in it:
extension Information { mutating func append < T > ( values : [ T ] ) -> Bool { } } |
At commencement, we'll declare the following two local variables:
var newData = Data ( ) var status = truthful |
Next, nosotros'll distinguish the type of the given values. Let's start with the String type. In this instance, we'll make a loop to access all values in the values
parameter collection:
if T . self == String . self { for value in values { } } |
In each repetition we volition convert the string value into a Information
object and we volition append it to the local newData
variable. If for some reason the string value cannot be converted into a Data
object, we'll set the status
flag to false and we'll break the loop.
baby-sit let convertedString = ( value as ! Cord ) . data ( using : . utf8 ) else { condition = false ; suspension } newData . append ( convertedString ) |
We will follow a quite similar approach in case of Data
input values. Of course, at that place is no need to initialize any new Data
object or brand a conversion of any type. We are appending one data value to some other:
else if T . self == Data . self { for value in values { newData . suspend ( value every bit ! Information ) } } |
Lastly, let's point that we don't care about whatever other type of values:
else { status = imitation } |
Next, we'll check the status
value. If it'south true, so we tin can append the newData
local variable to the self
object (the Data
object that is used to call this method).
if status { self . append ( newData ) } |
At the end, we should not forget to return the status
as the upshot of the method:
Here'south the entire implementation. Nosotros are going to put it in action starting from the next office.
1 2 iii four five 6 seven 8 ix 10 11 12 13 fourteen 15 16 17 xviii 19 twenty 21 22 23 24 25 | extension Data { mutating func append < T > ( values : [ T ] ) -> Bool { var newData = Data ( ) var status = true if T . self == String . cocky { for value in values { guard permit convertedString = ( value every bit ! String ) . information ( using : . utf8 ) else { status = false ; pause } newData . append ( convertedString ) } } else if T . self == Data . self { for value in values { newData . append ( value as ! Information ) } } else { condition = simulated } if status { self . suspend ( newData ) } return status } } |
Creating the HTTP Body
In the current implementation of RestManager
there is a method named getHttpBody()
. Its purpose is to prepare the HTTP trunk with the data that volition be posted to the server. Although this method works great in whatsoever other case, unfortunately information technology'south not of much help in instance of file uploading. There is the boundary string we have to accept into account, likewise equally 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 string as an argument (too known as method overloading).
In the new extension of the RestManager
form, right below the createBoundary
method, add the following:
individual func getHttpBody ( withBoundary boundary : String ) -> Data { var body = Data ( ) return torso } |
Go on in mind that the HTTP body must be a Information
value, so we are initializing such a value in this method, and this is as well what the method returns. In this method we'll deal with whatsoever information that should be posted to the server except for files. That's the data that would be normally submitted if there were no files to upload at the same fourth dimension, and it's kept in the httpBodyParameters
property (as a reminder, httpBodyParameters
is a holding in the RestManager
class and it'southward 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 data as a dictionary (a [String: Cord]
lexicon). Nosotros'll employ it to admission all values that should be sent to the server and append them to the body
variable. Right afterwards the var body = Data()
line add together the post-obit:
for ( key , value ) in httpBodyParameters . allValues ( ) { } |
A modest stop hither now as we have to hash out what exactly we'll be appending to the trunk. Let's see again function of the example presented in the beginning of this post:
-----------------------------abc123 Content-Disposition : form-data ; name="username" usernameValue -----------------------------abc123 Content-Disposition : course-information ; proper noun="password" passwordValue |
In this example the information is the username and the countersign. The following apply to each piece of data:
- At first there is the boundary cord, and right after that a line break. In HTTP headers, a line break is marked with "\r\n" (wagon return and new line character), not simply the "\n" that nosotros are mostly used to. Programmatically, this could be written like:
"--\(boundary)\r\north"
(meet the two dashes before the boundary cord). - Next, in that location is the "Content-Disposition" header with the
name
attribute only in it. Header is followed by a line break 2 times. We could write this similar and then:"Content-Disposition: form-information; name=\"\(primal)\"\r\n\r\n"
. - Lastly, information technology'due south the actual value followed by a line interruption. That's easy:
"\(value)\r\n"
.
We will add together the code that represents each step described higher up into an array:
let values = [ "--\ ( boundary )\r\n" , "Content-Disposition: form-data; name=\"\ ( key )\"\r\n\r\n" , "\ ( value )\r\n" ] |
Nosotros will use for first time the suspend(values:)
custom method nosotros implemented in the previous step in guild to convert these strings into Data
objects and append them to the trunk
variable:
_ = torso . append ( values : values ) |
And that's the terminal thing we had to do in this method. Permit's see it birthday at present:
individual func getHttpBody ( withBoundary boundary : String ) -> Data { var body = Information ( ) for ( key , value ) in httpBodyParameters . allValues ( ) { let values = [ "--\ ( boundary )\r\n" , "Content-Disposition: grade-data; proper noun=\"\ ( cardinal )\"\r\n\r\north" , "\ ( value )\r\n" ] _ = body . suspend ( values : values ) } return body } |
We'll use the results of this method in a while. For at present, we have to add the files information to the HTTP body also.
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 have to do in gild to make file uploading possible. And that would be pretty much true, as nosotros've built all the helper methods nosotros need and now we are dealing with the core functionality.
So, continuing on building the HTTP body, permit'due south define the following new method:
individual func add ( files : [ FileInfo ] , toBody body : inout Data , withBoundary boundary : String ) -> [ String ] ? { } |
Let's talk beginning virtually the parameters. The get-go 1 is a drove of FileInfo
objects, and information technology contains the data for all files that are about to be uploaded. The second parameter value is the data object that represents the HTTP trunk. Whatever changes that will be made to that object inside this method volition be reflected out of it besides because it's marked with the inout
keyword. The last parameter is the purlieus string, as we necessarily need it to separate data parts.
You might be wondering why this method returns an optional array of String values. Well, in case at that place are files whose information cannot be added to the HTTP body, then we'll go along their names into an array, which in plough the method will render. In normal weather condition this method should return nil, pregnant that data from all files was successfully appended to the HTTP body data.
Let's start adding some lawmaking, with the get-go ane beingness the following local variables:
var status = truthful var failedFilenames : [ String ] ? |
status
will bespeak whether all pieces of data for each single file in the files
collection were successfully combined in one Information
object, which can exist and then appended to the body
inout parameter. If status
is fake, we'll be appending the proper noun of the matching file to the failedFilenames
array.
Allow's beginning a loop now:
The first thing we have to do is to make sure that all properties of each file
object take bodily values so nosotros can proceed:
guard let filename = file . filename , let content = file . fileContents , let mimetype = file . mimetype , permit name = file . name else { continue } |
Next, we will set the initial value of the status
flag on each repetition of the loop to false, and we'll initialize a new Data
object.
status = false var information = Data ( ) |
Now, let'southward see again the example presented in the starting time of the tutorial and then we sympathise what we have to practise:
-----------------------------abc123 Content-Disposition : form-data ; name="aFile" ; filename="avatar.png" Content-Type : image/png . . . contents of avatar . png file . . . -----------------------------abc123 Content-Disposition : form-data ; name="anotherFile" ; filename="info.pdf" Content-Blazon : application/pdf . . . contents of info . pdf file . . . -----------------------------abc123-- |
Going step by footstep through the lines that describe a file part:
- At first in that location is the boundary with the line interruption at the end. We already know how to write that in lawmaking.
- Side by side, we take the "Content-Disposition" header. The addition here (comparing to the header in the previous part) is the new
filename
attribute which contains the actual file proper name. In code such a header is written like this:"Content-Disposition: form-data; name=\"\(proper name)\"; filename=\"\(filename)\"\r\n"
. - Correct after we accept the content type of the file. See all the bachelor MIME Media Types. In code this is similar and so:
"Content-Type: \(mimetype)\r\n\r\n"
.
Allow's brand a break here and let'south suspend all the above to an array:
let formattedFileInfo = [ "--\ ( boundary )\r\northward" , "Content-Disposition: class-data; proper noun=\"\ ( name )\"; filename=\"\ ( filename )\"\r\n" , "Content-Blazon: \ ( mimetype )\r\n\r\n" ] |
Let's convert all strings in that array into Information
objects and append them to the data
variable:
if data . append ( values : formattedFileInfo ) { } |
Let'south continue where nosotros had stopped from. The next item in a file office is the actual file contents. Remember that file contents are represented by the fileContents
holding in a FileInfo
object, which is a Information
object. So far we were dealing with strings only. File contents must be appended to the information
variable also:
if information . append ( values : [ content ] ) { } |
Retrieve that append(values:)
method expects for an array of values, so it's necessary to include content
into the array's opening and closing brackets above.
Lastly, discover in the above case that there is an empty line right afterwards the file contents which should be added to the data
as well:
if information . append ( values : [ "\r\northward" ] ) { } |
These iii conditions we wrote must be embedded into each other. If all of them are true, then all data pieces for the current file were successfully added to the information
object, and nosotros'll indicate that by making the status
truthful:
if data . append ( values : formattedFileInfo ) { if data . append ( values : [ content ] ) { if data . suspend ( values : [ "\r\n" ] ) { status = true } } } |
Run into that we used the custom suspend(values:)
custom method three times in a row hither. I hope you lot agree that its implementation was meaningful since we apply it again and again.
Adjacent, let's check the status
value for each file. While still being on the loop:
if status { torso . append ( information ) } else { if failedFilenames == zilch { failedFilenames = [ String ] ( ) } failedFilenames ? . suspend ( filename ) } |
If status
is true, we suspend the information
variable to the body
which represents the HTTP torso. If non, then nosotros initialize the failedFilenames
array in instance it's non initialized already, and we keep the name of the current file in it.
One last matter remaining, to return the failedFilenames
from the method:
Our new method should at present look similar this:
1 2 iii 4 v 6 seven eight nine x xi 12 thirteen 14 15 16 17 18 nineteen 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 | individual func add ( files : [ FileInfo ] , toBody trunk : inout Data , withBoundary purlieus : String ) -> [ String ] ? { var status = true var failedFilenames : [ String ] ? for file in files { guard let filename = file . filename , permit content = file . fileContents , allow mimetype = file . mimetype , permit name = file . name else { go on } condition = false var information = Data ( ) let formattedFileInfo = [ "--\ ( boundary )\r\n" , "Content-Disposition: course-data; proper noun=\"\ ( name )\"; filename=\"\ ( filename )\"\r\due north" , "Content-Type: \ ( mimetype )\r\n\r\n" ] if data . append ( values : formattedFileInfo ) { if data . append ( values : [ content ] ) { if information . suspend ( values : [ "\r\due north" ] ) { status = true } } } if status { body . suspend ( information ) } else { if failedFilenames == nil { failedFilenames = [ String ] ( ) } failedFilenames ? . append ( filename ) } } render failedFilenames } |
Endmost The HTTP Body
At present that we created methods which build the HTTP body past appending whatever post data and file data, we must create one more which will close the body. Remember that in "multipart/class-data" the HTTP body closing is marked by the boundary string and two dashes as a suffix to it:
-----------------------------abc123-- |
As y'all can judge, doing and so doesn't require much of work as all it takes is this:
private func shut ( body : inout Data , usingBoundary boundary : Cord ) { _ = trunk . suspend ( values : [ "\r\n--\ ( purlieus )--\r\n" ] ) } |
For ane more time here the torso
parameter is marked every bit inout
, so the data argument volition exist passed by reference and the changes made to information technology inside this method will become visible to the caller too. Besides that, detect the line breaks before and later on the closing string which ensure that the endmost boundary volition exist the only content in the line.
It's actually of import non to forget to telephone call this method and betoken the end of parts in the multipart HTTP body.
Uploading Files
It'southward about fourth dimension to put everything together and brand file uploading possible. The method we'll write here will be public, and so you can go and add information technology to the peak of the course along with other ii public methods existing already. Here is its definition:
func upload ( files : [ FileInfo ] , toURL url : URL , withHttpMethod httpMethod : HttpMethod , completion : @ escaping ( _ outcome : Results , _ failedFiles : [ String ] ? ) -> Void ) { } |
In accord to what we did to the other 2 existing public methods, we are going to perform all actions in this method asynchronously. We won't run anything on the chief thread since file uploading could take significant amount of time and we don't desire apps to bear witness frozen. In lawmaking that means:
DispatchQueue . global ( qos : . userInitiated ) . async { [ weak self ] in } |
With userInitiated
value in the quality of service parameter we requite our task a relatively loftier priority in execution. Note that we mark cocky
equally weak in the closure since the RestManager
instance used to perform the file uploading can potentially become nil, and that practically means that self
is from at present on an optional. This introduces a couple of new needs every bit you will run across side by side.
The get-go actual activeness we have to have is to add whatsoever 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 ) |
Adjacent, let'due south call the createBoundary()
method we implemented today and let'due south create the boundary string:
guard let purlieus = cocky ? . createBoundary ( ) else { completion ( Results ( withError : CustomError . failedToCreateBoundary ) , nil ) ; return } |
Notice that since self
is used as an optional, purlieus
becomes an optional value likewise, regardless of the fact that createBoundary()
does not return an optional. Then, in case there'southward no boundary string to continue, we call the completion handler passing the fault 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.
Allow'due south get going, and in the next step let's add the "multipart/grade-data" along with the boundary cord to the collection of the request headers:
self ? . requestHttpHeaders . add ( value : "multipart/form-data; purlieus=\ ( boundary )" , forKey : "content-type" ) |
To refresh your memory, requestHttpHeaders
is a RestEntity
property which keeps all HTTP request headers as key-value pairs. It's important to highlight that since we specify the content type header here, there is no need to provide a content type header manually while preparing the request. Non only it's redundant, it'due south also dangerous as it could create conflicts and make the server reject the request.
Next, let's first preparing the HTTP trunk. We'll offset by calling the getHttpBody(withBoundary:)
method:
guard var trunk = self ? . getHttpBody ( withBoundary : boundary ) else { completion ( Results ( withError : CustomError . failedToCreateHttpBody ) , nil ) ; return } |
Once again, since self
is an optional, trunk
might be nothing in case cocky
is nil. So, in that instance we call the completion handler with another custom fault and we return from the method.
Time to add the files to exist uploaded to the HTTP body. Observe in the next line that nosotros laissez passer the body
variable with the "&" symbol as that's an inout
parameter value:
permit failedFilenames = self ? . add ( files : files , toBody : &torso , withBoundary : purlieus ) |
failedFilenames
is either nil if all files are successfully added to the HTTP trunk, or it contains the names of those files that failed to be appended to the body.
Nosotros should not forget to close the HTTP trunk properly:
cocky ? . shut ( body : &body , usingBoundary : boundary ) |
Nosotros are set now to create the URL request:
guard let request = self ? . prepareRequest ( withURL : targetURL , httpBody : trunk , httpMethod : httpMethod ) else { completion ( Results ( withError : CustomError . failedToCreateRequest ) , nil ) ; render } |
The method we use hither is already implemented in the RestManager
form and we discussed nearly it in the previous tutorial. Discover that we pass the URL with any potential query items (targetURL
) and the HTTP torso as arguments.
Finally, nosotros'll create a new URLSession
and an upload task to brand the request. Upon completion, we'll telephone call the completion handler and we'll pass a Results
object with data regarding the results of the asking, and the failedFiles
assortment.
permit sessionConfiguration = URLSessionConfiguration . default let session = URLSession ( configuration : sessionConfiguration ) allow job = session . uploadTask ( with : asking , from : zilch , completionHandler : { ( data , response , error ) in completion ( Results ( withData : information , response : Response ( fromURLResponse : response ) , fault : error ) , failedFilenames ) } ) task . resume ( ) |
The upload method is now ready:
1 2 three 4 5 half-dozen vii eight ix x 11 12 thirteen fourteen fifteen xvi 17 18 19 20 21 22 23 24 25 26 27 | func upload ( files : [ FileInfo ] , toURL url : URL , withHttpMethod httpMethod : HttpMethod , completion : @ escaping ( _ result : Results , _ failedFiles : [ Cord ] ? ) -> Void ) { DispatchQueue . global ( qos : . userInitiated ) . async { [ weak cocky ] in let targetURL = self ? . addURLQueryParameters ( toURL : url ) guard permit boundary = self ? . createBoundary ( ) else { completion ( Results ( withError : CustomError . failedToCreateBoundary ) , nil ) ; return } self ? . requestHttpHeaders . add ( value : "multipart/form-data; boundary=\ ( boundary )" , forKey : "content-type" ) guard var body = cocky ? . getHttpBody ( withBoundary : boundary ) else { completion ( Results ( withError : CustomError . failedToCreateHttpBody ) , nil ) ; return } let failedFilenames = self ? . add ( files : files , toBody : &body, withBoundary: boundary) self?.close(trunk: &torso, usingBoundary: boundary) guard allow request = self?.prepareRequest(withURL: targetURL, httpBody: torso, httpMethod: httpMethod) else { completion(Results(withError: CustomError.failedToCreateRequest), nil); render } allow sessionConfiguration = URLSessionConfiguration . default let session = URLSession ( configuration : sessionConfiguration ) let task = session . uploadTask ( with : request , from : goose egg , completionHandler : { ( data , response , error ) in completion ( Results ( withData : information , response : Response ( fromURLResponse : response ) , fault : error ) , failedFilenames ) } ) task . resume ( ) } } |
At that place is one terminal affair to do earlier we test out everything. To add the two new custom errors to the CustomError
enum. Notice it in the RestManager
class and update information technology every bit shown next:
enum CustomError : Error { case failedToCreateRequest case failedToCreateBoundary case failedToCreateHttpBody } |
Update its extension right below accordingly with the description of the messages:
extension RestManager . CustomError : LocalizedError { public var localizedDescription : String { switch self { case . failedToCreateRequest : render NSLocalizedString ( "Unable to create the URLRequest object" , comment : "" ) case . failedToCreateBoundary : return NSLocalizedString ( "Unable to create boundary cord" , comment : "" ) case . failedToCreateHttpBody : return NSLocalizedString ( "Unable to create HTTP body parameters information" , comment : "" ) } } } |
That's it! Fourth dimension to upload files!
Testing File Uploading
The fourth dimension to test file uploading has finally come. Switch to the ViewController.swift
file and add the following method definition:
func uploadSingleFile ( ) { } |
For starters, we are going to upload a single file only, and here we will prepare the FileInfo
object that volition comprise its information.
Earlier we proceed, permit me remind you lot that in the starter Xcode project you downloaded in that location are iii files for testing: "sampleText.txt", "samplePDF.txt" and "sampleImage.pdf". We'll employ the "sampleText.txt" here, but feel free to modify and apply any other file y'all desire. Sample files exist in the application's bundle just for making the instance as simple as possible, but in real apps the you'll near ever fetch them from the documents directory.
And so, allow's commencement by creating a FileInfo
object:
func uploadSingleFile ( ) { let fileURL = Bundle . main . url ( forResource : "sampleText" , withExtension : "txt" ) let fileInfo = RestManager . FileInfo ( withFileURL : fileURL , filename : "sampleText.txt" , name : "uploadedFile" , mimetype : "text/plain" ) } |
Run across that we are using the custom initializer nosotros created in the FileInfo
structure here. However, in case yous don't want to initialize a FileInfo
object that manner and yous adopt to manually set all values including the files contents, hither's your alternative:
var fileInfo = RestManager . FileInfo ( ) fileInfo . filename = "sampleText.txt" fileInfo . proper name = "uploadedFile" fileInfo . mimetype = "text/patently" if let fileURL = Bundle . master . url ( forResource : "sampleText" , withExtension : "txt" ) { fileInfo . fileContents = endeavor ? Data ( contentsOf : fileURL ) } |
Annotation: Server is implemented in a way that requires the proper noun
attribute in every part of the multipart torso to have the "uploadedFile" value. Therefore, that's the value that we'll be setting in the proper noun
property of each FileInfo
object we 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 along with an array that will contain the fileInfo
object equally arguments to a new method (nosotros'll implement information technology right next):
upload ( files : [ fileInfo ] , toURL : URL ( string : "http://localhost:3000/upload" ) ) |
upload(files:toURL:)
is a pocket-size method responsible for making the asking as you tin see next. We could take put that lawmaking in the uploadSingleFile()
method, merely nosotros'll use it again in a while when nosotros'll upload multiple files. So, nosotros'd better avoid repeating lawmaking.
1 ii iii 4 5 half dozen 7 eight 9 x 11 12 13 14 15 16 17 xviii 19 20 21 22 23 | func upload ( files : [ RestManager . FileInfo ] , toURL url : URL ? ) { if let uploadURL = url { remainder . upload ( files : files , toURL : uploadURL , withHttpMethod : . postal service ) { ( results , failedFilesList ) in print ( "HTTP condition code:" , results . response ? . httpStatusCode ? ? 0 ) if let error = results . error { print ( fault ) } if let data = results . data { if let toDictionary = try ? 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 annihilation particular. Nosotros simply impress the HTTP status code, nosotros display any potential errors, and the server's response after we convert it from JSON to a dictionary object. Of form, we also print the list of failed to be uploaded files (in instance there is whatsoever).
In the viewDidLoad()
method call the uploadSingleFile()
:
override func viewDidLoad ( ) { super . viewDidLoad ( ) uploadSingleFile ( ) } |
Run the app now and look at both in Xcode console and in the concluding where the server's output is printed. If you followed everything pace past footstep up until here, you lot should get this in Xcode:
HTTP status code : 200 { outcome = 1 ; } |
At the same fourth dimension, in terminal you should take the details of the uploaded file:
{ fieldname : 'uploadedFile' , originalname : 'sampleText.txt' , encoding : '7bit' , mimetype : 'text/plain' , destination : 'uploads/' , filename : 'sampleText.txt' , path : 'uploads/sampleText.txt' , size : 5575 } |
I wanted to make the small demo server and the file uploading process conduct as much naturally as possible, so files sent to this server implementation are actually… existence uploaded! In Finder, get to the Server directory that you downloaded in the starter package and and then into the subdirectory chosen "uploads". The uploaded file is at that place which proves that file uploading is actually working!

Let's make our testing more interesting by too sending additional data along with the request. Right after the initialization of the FileInfo
object in the uploadSingleFile()
method add together the following two lines:
remainder . httpBodyParameters . add ( value : "Hello 😀 !!!" , forKey : "greeting" ) rest . httpBodyParameters . add ( value : "AppCoda" , forKey : "user" ) |
Run the app again. In the terminal you should encounter the boosted uploaded data as well:
{ fieldname : 'uploadedFile' , originalname : 'sampleText.txt' , encoding : '7bit' , mimetype : 'text/plainly' , destination : 'uploads/' , filename : 'sampleText.txt' , path : 'uploads/sampleText.txt' , size : 5575 } [ Object : null prototype ] { user : 'AppCoda' , greeting : 'Hello 😀 !!!' } |
Let's upload multiple files now. We'll do that by creating a new method similar to the previous i, with the difference beingness that instead of initializing one FileInfo
object, we'll initialize three of them then we can upload all sample files nosotros have. Hither information technology is:
func uploadMultipleFiles ( ) { let textFileURL = Packet . master . url ( forResource : "sampleText" , withExtension : "txt" ) let textFileInfo = RestManager . FileInfo ( withFileURL : textFileURL , filename : "sampleText.txt" , name : "uploadedFile" , mimetype : "text/plain" ) allow pdfFileURL = Bundle . main . url ( forResource : "samplePDF" , withExtension : "pdf" ) let pdfFileInfo = RestManager . FileInfo ( withFileURL : pdfFileURL , filename : "samplePDF.pdf" , name : "uploadedFile" , mimetype : "application/pdf" ) let imageFileURL = Packet . main . url ( forResource : "sampleImage" , withExtension : "jpg" ) permit imageFileInfo = RestManager . FileInfo ( withFileURL : imageFileURL , filename : "sampleImage.jpg" , name : "uploadedFile" , mimetype : "paradigm/jpg" ) upload ( files : [ textFileInfo , pdfFileInfo , imageFileInfo ] , toURL : URL ( string : "http://localhost:3000/multiupload" ) ) } |
At the end we phone call again the upload(files:toURL:)
method which volition trigger the actual upload request. Notice that the upload endpoint is dissimilar this time ("multiupload"). To see it working, don't forget to call it in the viewDidLoad()
:
override func viewDidLoad ( ) { super . viewDidLoad ( ) //uploadSingleFile() uploadMultipleFiles ( ) } |
This fourth dimension you should run into the names of the uploaded files in terminal:
Received files : -sampleText . txt -samplePDF . pdf -sampleImage . jpg |
Note that the current server implementation supports up to 10 simultaneous files to exist uploaded. Of course yous are gratis to change that limit according to your preference.
Summary
Starting in the previous tutorial where we created the first version of the RestManager
class and standing in this one where we added the file uploading feature, we have managed to build a small and lightweight class capable of roofing our needs in making web requests. "Multipart/form-information" content type and the mode HTTP torso is built can be sometimes disruptive, only if you break things down then everything gets easy. I hope what I shared with you here today to be of some value, and I wish you lot are enjoying RESTful services fifty-fifty more at present. You are ever welcome to add together more than features or conform the current implementation co-ordinate to your needs. Run across you next fourth dimension!
For reference, you tin download the full project on GitHub.
How To Send Document As Multipart/form-data Frim Rest Web Service,
Source: https://www.appcoda.com/restful-api-tutorial-how-to-upload-files-to-server/
Posted by: alfarothelover.blogspot.com
0 Response to "How To Send Document As Multipart/form-data Frim Rest Web Service"
Post a Comment