What Data Takes When Upload Image To Post Api Call
Welcome to a new, hopefully heady tutorial! In a previous mail service I showed to you lot the process of creating a custom class that manages web requests and RESTful APIs. Today, nosotros volition continue building on information technology, every bit I would like to focus on a specific use example: How to upload files to a server!
Uploading files might not be one of the about mutual things when dealing with web services. However, it can exist proved to exist a tedious chore to perform when it'southward time to send files to a server. In the implementation steps that follow we will try to interruption things downwardly and shed lite to the key points and the details of the uploading process. Before nosotros get there though, it'southward necessary to have a quick discussion about some ground cognition that nosotros all should have on this topic.
A Quick Intro To "Multipart/form-data" Content Type
Before we kickoff doing actual work, information technology's necessary some important things to exist mentioned starting time. Let me get-go by saying that in order to upload files to a server, multipart/form-data is the content blazon that should be specified in the web request. This content type allows to ship files or large amounts of data in combination with other usual data that should exist posted. "Multipart/course-data" content type tells to HTTP request that posted data should exist cleaved into parts, as if they were to be posted by a spider web form 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 office starts and where information technology 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 actual data, so it must exist as much unique as possible. Information technology ever starts with two dashes ("–"), with an capricious combination of other alphanumeric characters coming afterwards. Unremarkably, boundaries showtime 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 it. An attribute called "proper noun" should also be provided in the header, as it specifies the proper noun of the part. Notice that names don't need to be unique, and sometimes server sets the rules that apply to the "name" attribute. These two central-value pairs are enough when calculation single data (meaning no files) to the request's HTTP torso. When appending files data, the filename should be as well included in the "Content-Disposition" header with the original proper name of the file, as well as the content type (MIME blazon) of each file that is about to be uploaded.
The following is a imitation example of a HTTP request body that uses the "multipart/form-information" content type:
1 two three 4 five vi 7 8 9 10 11 12 thirteen fourteen 15 sixteen 17 18 nineteen 20 21 22 23 | Content-Type : multipart/form-data ; boundary=-----------------------------abc123 -----------------------------abc123 Content-Disposition : form-data ; proper name="username" usernameValue -----------------------------abc123 Content-Disposition : course-data ; proper name="password" passwordValue -----------------------------abc123 Content-Disposition : form-data ; name="aFile" ; filename="avatar.png" Content-Type : image/png . . . contents of avatar . png file . . . -----------------------------abc123 Content-Disposition : form-data ; proper noun="anotherFile" ; filename="info.pdf" Content-Blazon : application/pdf . . . contents of info . pdf file . . . -----------------------------abc123-- |
Notice how everything mentioned in the previous paragraphs is used. At first, the "multipart/form-data" content type is specified along with the boundary string that separates the data parts. Run across how purlieus indicates the starting time of each part and likewise run across how semicolon (";") separates attributes in headers. Line breaks are also important when building a HTTP body such the above i. In unmarried fields, an empty line exists between the "Content-Disposition" header and the actual field value, while the purlieus of the next function comes correct subsequently in the side by side line. In file parts, the "filename" attribute contains the proper noun of the file, while an boosted empty line exists between the file contents and the adjacent boundary. The torso ending is highlighted by the boundary, plus 2 more dashes equally a suffix to it.
I am encouraging you to take a look at the W3C HTML Specification and read more virtually encoding content types and the "multipart/form-data" peculiarly. You lot don't have to stop there of form; a general search on the spider web will return lots of resources to read about this topic.
Nearly The Demo App
So, as I said in the commencement of this post, we are going to go on edifice on the custom class we created in the previous tutorial, called RestManager
. To get started, please download a starter package which contains a Xcode project with that class and i more than directory with a demo server implementation (meet next part). In Xcode project yous will find iii files that we'll utilise to test file uploading afterwards 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 image file named SampleImage.jpg downloaded from Pexels (Photo by Oleg Magni from Pexels).
No UI will exist in our app, and the results of our final tests will be printed in Xcode panel and in Terminal. Whatever input values will be difficult-coded. Therefore, we'll entirely focus on the file uploading feature that we'll add to the RestManager
class. Obviously, you are free to create any UI you desire if y'all want to create a more than dynamic demo application.
About The Server
Subsequently nosotros end implementing all the new lawmaking we'll encounter in the following parts, we'll demand to examination if file uploading is really working. For that purpose, a unproblematic server implemented in Node.js is included in the starter package that you downloaded; you lot will find it in the Server subdirectory. You tin can keep it in the location that currently is, or copy it anywhere else you lot desire in your deejay.
In order to run the server, yous must have Node.js installed on your figurer. If yous don't, please check here or here on how to do that. Open Last and type the post-obit command:
In that location is a space character after the cd
control. And so switch to Finder, and drag and drop the Server directory to terminal and press the Return key:
By doing so, y'all don't have to type the path to the server directory; it's automatically appended to the command in terminal.
To verify that you are successfully in the server directory, merely type:
This command volition bear witness the current directory contents, and if you see something similar to the next ane, then you lot're only fine:
To start the server only blazon:
You should see the bulletin:
Server started successfully on port 3000!
The server is now running at address http://localhost:3000. Yous tin can too verify that if you paste that address in a new tab in your browser. You'll see a message coming from the server.
Notation: If yous are already running another server at port 3000, edit the index.js file and set a custom port number to the port
variable. So restart the server with the node alphabetize.js
control.
Requests made to "http" addresses are not immune past default in iOS equally they are considered insecure. Notwithstanding, for the sake of the tutorial, localhost has been whitelisted in the Info.plist file of the starter projection and so you volition come across no trouble in testing the app later.
Representing Files
The first thing we need to take care of is how files are going to be represented in the RestManager
form. For whatever file that is about to be uploaded, nosotros need to have the following data bachelor at the time of the HTTP body preparation:
- The actual file contents.
- The original file name. Remember that the filename aspect must exist in the "Content-Disposition" header of each office that represents a file.
- The part's name for the proper noun attribute in the "Content-Disposition" header.
- The content type (MIME blazon) of the file.
Evidently, all that information could be stored in a dictionary, but that wouldn't be the best approach in Swift. To do information technology better, let's create a struct which we'll telephone call FileInfo
. Open up the RestManager.swift file in the starter Xcode project, and go to the terminate of it. Yous will find the following empty extension:
// Marking: - File Upload Related Implementation extension RestManager { } |
This is where we'll add almost all new lawmaking regarding the file uploading feature. Inside this extension, add the following structure:
struct FileInfo { var fileContents : Information ? var mimetype : String ? var filename : String ? var name : Cord ? } |
The four properties will keep the data described earlier. As yous will see later, if any of the above backdrop is nil 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 nosotros add the post-obit custom initializer:
struct FileInfo { . . . init ( withFileURL url : URL ? , filename : String , name : String , mimetype : Cord ) { guard allow url = url else { render } fileContents = try ? Data ( contentsOf : url ) self . 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 will be plenty. File contents will be read in the higher up initializer.
Creating The Boundary
Having a solution on our hands almost how to correspond files, allow'south create a method which will exist responsible of creating the boundary cord. Remember that a boundary must be unique and definitely not an ordinary cord that could be potentially found in the actual data that will be uploaded. As I said in the beginning of the postal service, even though boundaries get-go with 2 dashes ("–"), they normally have several more than dashes following and a random alphanumeric string at the end. That's not mandatory, but it's the logic nosotros will follow hither.
Right afterward the FileInfo
struct, ascertain the following private method:
private func createBoundary ( ) -> String ? { } |
I volition show you two different ways to generate the random purlieus string.
Using A UUID String
The fastest way to go a random cord is to generate a UUID value:
var uuid = UUID ( ) . uuidString |
The above will generate something similar to this:
D41568F4-7175-42BB-9503-DAA282180D70 |
Let'south go rid of the dashes in that string, and let'south convert all letters to lowercase:
uuid = uuid . replacingOccurrences ( of : "-" , with : "" ) uuid = uuid . map { $ 0 . lowercased ( ) } . joined ( ) |
The original UUID will now expect like this:
d41568f4717542bb9503daa282180d70 |
Let's construct the purlieus cord. It will be a concatenation of 20 dashes at the kickoff and the transformed UUID value:
let boundary = String ( repeating : "-" , count : 20 ) + uuid |
If you like exaggerating, add together the current timestamp to the finish equally well:
let purlieus = String ( repeating : "-" , count : 20 ) + uuid + "\ ( Int ( Date . timeIntervalSinceReferenceDate ))" |
A boundary string created with the higher up will look like:
--------------------d41568f4717542bb9503daa282180d70579430569 |
Well, that looks quite unique and random, no?
Here's the implementation of the entire method:
private func createBoundary ( ) -> String ? { var uuid = UUID ( ) . uuidString uuid = uuid . replacingOccurrences ( of : "-" , with : "" ) uuid = uuid . map { $ 0 . lowercased ( ) } . joined ( ) permit boundary = String ( repeating : "-" , count : 20 ) + uuid + "\ ( Int ( Date . timeIntervalSinceReferenceDate ))" render boundary } |
Using Random Characters
Equally an culling to the above we can create a mechanism which will selection random characters from a drove of available characters, and using them to form a string which will be appended to the boundary string. The collection 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 "ix".
Nosotros won't really need to hard-code anything, every bit we can programmatically construct everything. We will be based on the ASCII table for that.
We'll first by specifying the range of the lower cased characters ("a" to "z") in the ASCII table as shown below:
allow lowerCaseLettersInASCII = UInt8 ( ascii : "a" ) . . . UInt8 ( ascii : "z" ) |
The above is equivalent to this:
let 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.
However, the second line of code requires from the states to search for an ASCII table online then locate the position of the characters nosotros are interested in into the table. Okay, it's easy, simply it'south definitely not the recommended way, since nosotros can get the values we desire by using the UInt8(ascii:)
initializer. And that'south we do in the get-go 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, permit'south join all these ranges into a collection, or in other words a sequence of ranges (closed ranges more peculiarly) with aim to go the actual characters subsequently:
let sequenceOfRanges = [ lowerCaseLettersInASCII , upperCaseLettersInASCII , digitsInASCII ] . joined ( ) |
If we print the value of the sequenceOfRanges
to the console at runtime nosotros'll get this:
FlattenSequence < Assortment < ClosedRange < UInt8 > > > ( _base : [ ClosedRange ( 97 . . . 122 ) , ClosedRange ( 65 . . . 90 ) , ClosedRange ( 48 . . . 57 ) ] ) |
Even though information technology's non obvious unless someone looks up for information technology, the above tin can exist easily converted into a String value:
guard let toString = String ( data : Data ( sequenceOfRanges ) , encoding : . utf8 ) else { render nil } |
Information
struct provides several initializers for creating a information object and there is i amid them that accepts a sequence as an statement, exactly as we practice in the Information(sequenceOfRanges)
expression. From that data object, we can create the following string which is assigned to the toString
constant:
abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789 |
That cool! Let's generate a cord of 20 random characters now:
var randomString = "" for _ in 0 . . < xx { randomString += String ( toString . randomElement ( ) ! ) } |
At kickoff we initialize a string value called randomString
. And so, nosotros create a loop that will be executed 20 times. In it, we pick a random character from the toString
string using the randomElement()
method, and we generate a new String value (Cord(toString.randomElement()!)
). This new String value is appended to the randomString
.
Note that is safe to force unwrap the value of the randomElement()
method, as information technology returns nix only 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 : 20 ) + 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 ? { let lowerCaseLettersInASCII = UInt8 ( ascii : "a" ) . . . UInt8 ( ascii : "z" ) let upperCaseLettersInASCII = UInt8 ( ascii : "A" ) . . . UInt8 ( ascii : "Z" ) allow digitsInASCII = UInt8 ( ascii : "0" ) . . . UInt8 ( ascii : "9" ) let sequenceOfRanges = [ lowerCaseLettersInASCII , upperCaseLettersInASCII , digitsInASCII ] . joined ( ) guard let toString = Cord ( data : Data ( sequenceOfRanges ) , encoding : . utf8 ) else { render naught } var randomString = "" for _ in 0 . . < 20 { randomString += String ( toString . randomElement ( ) ! ) } allow purlieus = String ( repeating : "-" , count : xx ) + randomString + "\ ( Int ( Date . timeIntervalSinceReferenceDate ))" return boundary } |
Utilize the implementation you prefer the most. The second 1 is more than "Swifty" but it requires a flake of more code. At the stop of the mean solar day, both approaches are going to piece of work equally well.
An important notation: I've mentioned already that the boundary string which separates the parts of a multipart body starts with two dashes ("–"). These 2 dashes are not included in the dashes of the boundary string we generated in both approaches here. This cord volition exist provided as-is to the request as a asking header along with the content type and server will try to locate it afterward the ii dashes prefix. Likewise, a purlieus string tin exist with no dashes at all; we just add them to minimize the possibility to discover similar cord in the uploaded information. As you volition meet later, the two dashes prefix will exist manually appended whenever necessary.
Extending Information Construction
Our next steps involve the preparation of the HTTP body using any arbitrary data provided to the class, as well equally using the files data. But before we become into that, we will extend the Data
structure and we volition create the following generic method:
mutating func append < T > ( values : [ T ] ) -> Bool { } |
The purpose of this method is to let us hands suspend the values of the values
drove to the data object that calls it. And as you'll encounter, nosotros'll exist interested for String
and Data
types only.
Just for description, nosotros could avoid implementing this method. Yet, the code that we will add to information technology would have to be repeated multiple times in different points in the RestManager
class, and that definitely would not be a wise move.
So, to continue go to the end of the RestManager.swift file where you volition find a Information
extension:
Add the new method'south definition in it:
extension Information { mutating func append < T > ( values : [ T ] ) -> Bool { } } |
At first, we'll declare the following two local variables:
var newData = Data ( ) var status = true |
Next, we'll distinguish the type of the given values. Permit's first with the String type. In this instance, we'll make a loop to access all values in the values
parameter collection:
if T . self == Cord . self { for value in values { } } |
In each repetition nosotros volition convert the string value into a Information
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, nosotros'll set the status
flag to false and we'll suspension the loop.
guard permit convertedString = ( value as ! Cord ) . data ( using : . utf8 ) else { status = fake ; break } newData . append ( convertedString ) |
We volition follow a quite like approach in case of Data
input values. Of course, there is no need to initialize whatsoever new Information
object or make a conversion of any type. Nosotros are appending one data value to another:
else if T . self == Data . self { for value in values { newData . append ( value every bit ! Data ) } } |
Lastly, let'due south indicate that nosotros don't intendance most whatever other type of values:
else { status = false } |
Next, we'll bank check the status
value. If it'due south truthful, then we tin suspend the newData
local variable to the self
object (the Information
object that is used to call this method).
if condition { self . append ( newData ) } |
At the terminate, nosotros should not forget to return the status
equally the result of the method:
Here's the entire implementation. We are going to put it in action starting from the adjacent part.
1 ii 3 4 5 half-dozen 7 8 nine 10 11 12 13 14 xv 16 17 eighteen nineteen 20 21 22 23 24 25 | extension Information { mutating func suspend < T > ( values : [ T ] ) -> Bool { var newData = Data ( ) var condition = true if T . self == String . cocky { for value in values { guard let convertedString = ( value as ! String ) . data ( using : . utf8 ) else { status = faux ; pause } newData . append ( convertedString ) } } else if T . self == Information . self { for value in values { newData . suspend ( value as ! Data ) } } else { status = false } 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 body with the information that volition be posted to the server. Although this method works cracking in any other example, unfortunately information technology's non of much help in case of file uploading. There is the boundary string we have to accept into account, also as the special headers and formatting required when using the "multipart/grade-data" content type. To serve our new needs, we'll implement a similarly named method which will exist accepting the boundary string equally an argument (also known as method overloading).
In the new extension of the RestManager
class, right below the createBoundary
method, add the following:
private func getHttpBody ( withBoundary boundary : Cord ) -> Data { var body = Data ( ) return body } |
Proceed in mind that the HTTP torso must be a Data
value, then we 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 be posted to the server except for files. That's the data that would be normally submitted if at that place were no files to upload at the same time, and it'southward kept in the httpBodyParameters
property (as a reminder, httpBodyParameters
is a property in the RestManager
class and it's of RestEntity
type, a custom construction – find information technology in RestManager
and read more in the previous tutorial most it).
httpBodyParameters
has a method called allValues()
and returns all data equally a dictionary (a [Cord: Cord]
dictionary). We'll use it to access all values that should be sent to the server and suspend them to the body
variable. Right subsequently the var body = Information()
line add the following:
for ( key , value ) in httpBodyParameters . allValues ( ) { } |
A small stop here at present as we have to talk over what exactly we'll exist appending to the body. Let's run into once more function of the case presented in the first of this post:
-----------------------------abc123 Content-Disposition : form-data ; name="username" usernameValue -----------------------------abc123 Content-Disposition : form-data ; name="password" passwordValue |
In this example the data is the username and the password. The following apply to each piece of data:
- At offset there is the boundary string, and right afterward that a line break. In HTTP headers, a line interruption is marked with "\r\north" (carriage return and new line character), not just the "\due north" that nosotros are mostly used to. Programmatically, this could be written like:
"--\(boundary)\r\n"
(see the ii dashes before the boundary string). - Next, there is the "Content-Disposition" header with the
name
attribute only in information technology. Header is followed by a line interruption two times. We could write this like so:"Content-Disposition: class-information; name=\"\(fundamental)\"\r\n\r\north"
. - Lastly, it'southward the actual value followed by a line interruption. That'south easy:
"\(value)\r\north"
.
We will add the code that represents each step described above into an array:
allow values = [ "--\ ( boundary )\r\n" , "Content-Disposition: class-data; name=\"\ ( key )\"\r\n\r\northward" , "\ ( value )\r\n" ] |
We will use for first time the append(values:)
custom method we implemented in the previous step in order to catechumen these strings into Information
objects and append them to the body
variable:
_ = body . suspend ( values : values ) |
And that's the terminal thing we had to do in this method. Let'south run into it birthday now:
individual func getHttpBody ( withBoundary boundary : String ) -> Data { var body = Data ( ) for ( key , value ) in httpBodyParameters . allValues ( ) { let values = [ "--\ ( boundary )\r\north" , "Content-Disposition: form-information; name=\"\ ( key )\"\r\n\r\north" , "\ ( value )\r\northward" ] _ = torso . append ( values : values ) } return body } |
We'll utilise the results of this method in a while. For at present, nosotros accept to add together the files information to the HTTP torso as well.
Adding Files To HTTP Body
One could say that the getHttpBody(withBoundary:)
method we just implemented along with the new i we will implement hither consist of the most important function of the overall work we have to practice in order to make file uploading possible. And that would be pretty much true, as we've congenital all the helper methods we demand and now we are dealing with the core functionality.
And so, standing on edifice the HTTP body, let's ascertain the following new method:
private func add ( files : [ FileInfo ] , toBody body : inout Data , withBoundary boundary : String ) -> [ String ] ? { } |
Allow's talk first about the parameters. The first ane is a collection of FileInfo
objects, and it contains the data for all files that are about to exist uploaded. The 2d parameter value is the data object that represents the HTTP body. Whatever changes that volition exist made to that object inside this method volition be reflected out of it also because it'southward marked with the inout
keyword. The last parameter is the boundary string, every bit we necessarily need it to divide data parts.
Y'all 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 proceed their names into an array, which in turn the method will return. In normal conditions this method should return nil, meaning that information from all files was successfully appended to the HTTP body information.
Let's beginning adding some lawmaking, with the first one being the following local variables:
var status = truthful var failedFilenames : [ Cord ] ? |
status
volition indicate whether all pieces of data for each single file in the files
drove were successfully combined in one Data
object, which tin be so appended to the body
inout parameter. If status
is false, we'll be appending the proper name of the matching file to the failedFilenames
array.
Let'south start a loop now:
The first thing we have to do is to make sure that all properties of each file
object have actual values and then we can go along:
guard let filename = file . filename , let content = file . fileContents , let mimetype = file . mimetype , allow name = file . name else { continue } |
Next, we will set up the initial value of the status
flag on each repetition of the loop to imitation, and we'll initialize a new Information
object.
status = simulated var data = Data ( ) |
Now, let's come across once again the instance presented in the beginning of the tutorial so we understand what we accept to do:
-----------------------------abc123 Content-Disposition : form-data ; name="aFile" ; filename="avatar.png" Content-Blazon : image/png . . . contents of avatar . png file . . . -----------------------------abc123 Content-Disposition : form-information ; name="anotherFile" ; filename="info.pdf" Content-Type : application/pdf . . . contents of info . pdf file . . . -----------------------------abc123-- |
Going stride past step through the lines that describe a file role:
- At get-go there is the boundary with the line break at the end. We already know how to write that in code.
- Next, nosotros have the "Content-Disposition" header. The addition here (comparison to the header in the previous part) is the new
filename
aspect which contains the actual file name. In code such a header is written like this:"Content-Disposition: form-information; name=\"\(proper name)\"; filename=\"\(filename)\"\r\due north"
. - Right later on we have the content blazon of the file. See all the available MIME Media Types. In code this is like so:
"Content-Type: \(mimetype)\r\n\r\n"
.
Permit's make a interruption here and allow's suspend all the above to an array:
let formattedFileInfo = [ "--\ ( boundary )\r\n" , "Content-Disposition: class-data; name=\"\ ( name )\"; filename=\"\ ( filename )\"\r\n" , "Content-Blazon: \ ( mimetype )\r\n\r\n" ] |
Let's convert all strings in that array into Data
objects and append them to the data
variable:
if data . append ( values : formattedFileInfo ) { } |
Permit's continue where we had stopped from. The next particular in a file part is the bodily file contents. Remember that file contents are represented by the fileContents
property in a FileInfo
object, which is a Data
object. And so far we were dealing with strings only. File contents must be appended to the data
variable also:
if data . append ( values : [ content ] ) { } |
Call up that suspend(values:)
method expects for an array of values, so it's necessary to include content
into the array's opening and endmost brackets to a higher place.
Lastly, notice in the above case that there is an empty line right after the file contents which should be added to the information
also:
if information . append ( values : [ "\r\n" ] ) { } |
These iii atmospheric condition we wrote must be embedded into each other. If all of them are truthful, then all data pieces for the current file were successfully added to the data
object, and we'll bespeak that by making the status
truthful:
if data . append ( values : formattedFileInfo ) { if information . append ( values : [ content ] ) { if data . append ( values : [ "\r\n" ] ) { status = true } } } |
Run across that we used the custom append(values:)
custom method three times in a row here. I hope you agree that its implementation was meaningful since we utilise information technology again and again.
Next, allow'due south cheque the status
value for each file. While nevertheless being on the loop:
if condition { torso . append ( data ) } else { if failedFilenames == nil { failedFilenames = [ String ] ( ) } failedFilenames ? . suspend ( filename ) } |
If status
is truthful, we append the data
variable to the torso
which represents the HTTP torso. If not, then nosotros initialize the failedFilenames
array in example it's not initialized already, and we go along the proper name of the current file in it.
One last thing remaining, to return the failedFilenames
from the method:
Our new method should at present look like this:
1 2 3 four 5 half dozen vii 8 9 10 11 12 thirteen 14 15 16 17 xviii 19 xx 21 22 23 24 25 26 27 28 29 thirty 31 32 33 34 | private func add ( files : [ FileInfo ] , toBody body : inout Data , withBoundary boundary : String ) -> [ String ] ? { var status = true var failedFilenames : [ Cord ] ? for file in files { baby-sit allow filename = file . filename , permit content = file . fileContents , allow mimetype = file . mimetype , permit proper name = file . proper name else { go on } condition = false var information = Information ( ) let formattedFileInfo = [ "--\ ( boundary )\r\n" , "Content-Disposition: grade-data; proper name=\"\ ( proper name )\"; filename=\"\ ( filename )\"\r\northward" , "Content-Blazon: \ ( mimetype )\r\n\r\northward" ] if information . append ( values : formattedFileInfo ) { if data . append ( values : [ content ] ) { if information . suspend ( values : [ "\r\n" ] ) { condition = true } } } if status { body . append ( data ) } else { if failedFilenames == nil { failedFilenames = [ Cord ] ( ) } failedFilenames ? . append ( filename ) } } return failedFilenames } |
Endmost The HTTP Trunk
Now that nosotros created methods which build the HTTP body by appending whatever mail information and file data, we must create one more than which will close the body. Remember that in "multipart/course-information" the HTTP body closing is marked by the purlieus string and two dashes as a suffix to it:
-----------------------------abc123-- |
As you can guess, doing and then doesn't require much of work as all it takes is this:
private func shut ( body : inout Data , usingBoundary boundary : String ) { _ = torso . append ( values : [ "\r\north--\ ( boundary )--\r\north" ] ) } |
For one more than time here the body
parameter is marked every bit inout
, so the information statement will be passed by reference and the changes made to it within this method will become visible to the caller too. Besides that, notice the line breaks earlier and after the closing cord which ensure that the closing boundary volition exist the only content in the line.
It's actually important not to forget to call this method and indicate the end of parts in the multipart HTTP trunk.
Uploading Files
It's most time to put everything together and make file uploading possible. The method we'll write here will exist public, so you can go and add it to the top of the form along with other two public methods existing already. Hither is its definition:
func upload ( files : [ FileInfo ] , toURL url : URL , withHttpMethod httpMethod : HttpMethod , completion : @ escaping ( _ result : Results , _ failedFiles : [ String ] ? ) -> Void ) { } |
In accord to what we did to the other two existing public methods, we are going to perform all actions in this method asynchronously. We won't run annihilation on the chief thread since file uploading could accept pregnant corporeality of fourth dimension and nosotros don't want apps to show frozen. In code that ways:
DispatchQueue . global ( qos : . userInitiated ) . async { [ weak self ] in } |
With userInitiated
value in the quality of service parameter we give our task a relatively loftier priority in execution. Note that nosotros marking self
as weak in the closure since the RestManager
instance used to perform the file uploading can potentially become nil, and that practically means that cocky
is from now on an optional. This introduces a couple of new needs as you will see side by side.
The first actual action we have to take is to add together whatever 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 = cocky ? . addURLQueryParameters ( toURL : url ) |
Next, let'south call the createBoundary()
method we implemented today and let's create the boundary string:
guard let boundary = self ? . createBoundary ( ) else { completion ( Results ( withError : CustomError . failedToCreateBoundary ) , nil ) ; return } |
Notice that since cocky
is used as an optional, purlieus
becomes an optional value also, regardless of the fact that createBoundary()
does non return an optional. Then, in instance at that place'south no purlieus string to continue, nosotros telephone call the completion handler passing the fault shown above and we render from the method. This custom error doesn't be yet in the course, we'll add it in a while.
Allow'southward get going, and in the next step allow's add the "multipart/form-information" along with the purlieus 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
property which keeps all HTTP request headers as fundamental-value pairs. It's important to highlight that since we specify the content type header hither, there is no need to provide a content blazon header manually while preparing the asking. Not only it'due south redundant, it's as well dangerous as it could create conflicts and make the server reject the request.
Side by side, permit's starting time preparing the HTTP body. Nosotros'll start past calling the getHttpBody(withBoundary:)
method:
guard var body = self ? . getHttpBody ( withBoundary : boundary ) else { completion ( Results ( withError : CustomError . failedToCreateHttpBody ) , cypher ) ; return } |
Once once again, since cocky
is an optional, body
might be nil in case cocky
is zero. So, in that case we telephone call the completion handler with some other custom error and nosotros return from the method.
Time to add the files to exist uploaded to the HTTP body. Notice in the adjacent line that nosotros pass the trunk
variable with the "&" symbol equally that's an inout
parameter value:
let failedFilenames = self ? . add ( files : files , toBody : &body , withBoundary : purlieus ) |
failedFilenames
is either nil 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 trunk.
We should non forget to close the HTTP trunk properly:
cocky ? . close ( body : &trunk , usingBoundary : boundary ) |
Nosotros are set now to create the URL request:
guard allow 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 about it in the previous tutorial. Notice that we laissez passer 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 make the request. Upon completion, we'll call the completion handler and we'll pass a Results
object with data regarding the results of the asking, and the failedFiles
array.
allow sessionConfiguration = URLSessionConfiguration . default let session = URLSession ( configuration : sessionConfiguration ) let task = session . uploadTask ( with : asking , from : nil , completionHandler : { ( data , response , error ) in completion ( Results ( withData : data , response : Response ( fromURLResponse : response ) , mistake : error ) , failedFilenames ) } ) task . resume ( ) |
The upload method is now ready:
1 2 iii four 5 6 vii eight 9 x 11 12 13 14 15 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 : [ String ] ? ) -> Void ) { DispatchQueue . global ( qos : . userInitiated ) . async { [ weak cocky ] in let targetURL = self ? . addURLQueryParameters ( toURL : url ) baby-sit permit purlieus = self ? . createBoundary ( ) else { completion ( Results ( withError : CustomError . failedToCreateBoundary ) , nil ) ; return } self ? . requestHttpHeaders . add together ( value : "multipart/class-data; purlieus=\ ( boundary )" , forKey : "content-type" ) baby-sit var trunk = self ? . getHttpBody ( withBoundary : purlieus ) else { completion ( Results ( withError : CustomError . failedToCreateHttpBody ) , zero ) ; render } permit failedFilenames = cocky ? . add together ( files : files , toBody : &body, withBoundary: boundary) cocky?.close(body: &body, usingBoundary: boundary) guard let request = self?.prepareRequest(withURL: targetURL, httpBody: trunk, httpMethod: httpMethod) else { completion(Results(withError: CustomError.failedToCreateRequest), zippo); return } let sessionConfiguration = URLSessionConfiguration . default let session = URLSession ( configuration : sessionConfiguration ) let job = session . uploadTask ( with : request , from : nil , completionHandler : { ( data , response , mistake ) in completion ( Results ( withData : data , response : Response ( fromURLResponse : response ) , mistake : error ) , failedFilenames ) } ) task . resume ( ) } } |
There is one concluding matter to do earlier nosotros exam out everything. To add the 2 new custom errors to the CustomError
enum. Notice it in the RestManager
class and update it as 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 : Cord { switch cocky { case . failedToCreateRequest : return NSLocalizedString ( "Unable to create the URLRequest object" , comment : "" ) instance . failedToCreateBoundary : return NSLocalizedString ( "Unable to create boundary string" , comment : "" ) example . failedToCreateHttpBody : return NSLocalizedString ( "Unable to create HTTP body parameters data" , comment : "" ) } } } |
That's it! Time to upload files!
Testing File Uploading
The fourth dimension to examination 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 simply, and here we will prepare the FileInfo
object that will contain its data.
Before we proceed, let me remind you lot that in the starter Xcode projection yous downloaded in that location are iii files for testing: "sampleText.txt", "samplePDF.txt" and "sampleImage.pdf". We'll utilise the "sampleText.txt" hither, but experience gratuitous to modify and use any other file yous desire. Sample files be in the application'due south bundle just for making the instance equally simple equally possible, just in real apps the you lot'll nigh always fetch them from the documents directory.
So, let'south beginning by creating a FileInfo
object:
func uploadSingleFile ( ) { let fileURL = Parcel . main . url ( forResource : "sampleText" , withExtension : "txt" ) permit fileInfo = RestManager . FileInfo ( withFileURL : fileURL , filename : "sampleText.txt" , proper name : "uploadedFile" , mimetype : "text/plain" ) } |
See that we are using the custom initializer we created in the FileInfo
structure here. However, in case you don't want to initialize a FileInfo
object that style and you adopt to manually set all values including the files contents, here's your alternative:
var fileInfo = RestManager . FileInfo ( ) fileInfo . filename = "sampleText.txt" fileInfo . proper noun = "uploadedFile" fileInfo . mimetype = "text/plain" if let fileURL = Bundle . main . url ( forResource : "sampleText" , withExtension : "txt" ) { fileInfo . fileContents = try ? Data ( contentsOf : fileURL ) } |
Notation: Server is implemented in a mode that requires the name
attribute in every part of the multipart body to have the "uploadedFile" value. Therefore, that'due south the value that we'll be setting in the name
property of each FileInfo
object we create here.
The URL where nosotros'll make the request to upload the file is: http://localhost:3000/upload
. We volition pass a URL object forth with an array that volition incorporate the fileInfo
object equally arguments to a new method (we'll implement it right side by side):
upload ( files : [ fileInfo ] , toURL : URL ( cord : "http://localhost:3000/upload" ) ) |
upload(files:toURL:)
is a small method responsible for making the request every bit you lot can see next. We could have put that lawmaking in the uploadSingleFile()
method, merely we'll use information technology again in a while when we'll upload multiple files. So, nosotros'd better avoid repeating code.
i 2 3 4 5 6 vii viii 9 10 11 12 thirteen 14 15 16 17 18 19 20 21 22 23 | func upload ( files : [ RestManager . FileInfo ] , toURL url : URL ? ) { if let uploadURL = url { remainder . upload ( files : files , toURL : uploadURL , withHttpMethod : . mail ) { ( results , failedFilesList ) in print ( "HTTP status code:" , results . response ? . httpStatusCode ? ? 0 ) if let error = results . error { print ( mistake ) } if let information = results . information { if permit toDictionary = try ? JSONSerialization . jsonObject ( with : data , options : . mutableContainers ) { print ( toDictionary ) } } if allow failedFiles = failedFilesList { for file in failedFiles { print ( file ) } } } } } |
In the completion handler we don't do anything detail. We just print the HTTP status lawmaking, nosotros brandish any potential errors, and the server's response after nosotros convert information technology from JSON to a dictionary object. Of form, we besides print the list of failed to be uploaded files (in case there is any).
In the viewDidLoad()
method phone call the uploadSingleFile()
:
override func viewDidLoad ( ) { super . viewDidLoad ( ) uploadSingleFile ( ) } |
Run the app now and expect at both in Xcode panel and in the terminal where the server'south output is printed. If you lot followed everything pace by step up until hither, y'all should get this in Xcode:
HTTP status lawmaking : 200 { event = 1 ; } |
At the same time, in terminal y'all should have the details of the uploaded file:
{ fieldname : 'uploadedFile' , originalname : 'sampleText.txt' , encoding : '7bit' , mimetype : 'text/obviously' , destination : 'uploads/' , filename : 'sampleText.txt' , path : 'uploads/sampleText.txt' , size : 5575 } |
I wanted to make the pocket-sized demo server and the file uploading process bear as much naturally as possible, then files sent to this server implementation are actually… being uploaded! In Finder, go to the Server directory that you lot downloaded in the starter package and then into the subdirectory chosen "uploads". The uploaded file is there which proves that file uploading is actually working!
Let's make our testing more interesting past also sending additional data forth with the asking. Right subsequently the initialization of the FileInfo
object in the uploadSingleFile()
method add the following two lines:
residue . httpBodyParameters . add together ( value : "Hello 😀 !!!" , forKey : "greeting" ) residue . httpBodyParameters . add together ( value : "AppCoda" , forKey : "user" ) |
Run the app again. In the last y'all should run across the additional uploaded data also:
{ fieldname : 'uploadedFile' , originalname : 'sampleText.txt' , encoding : '7bit' , mimetype : 'text/plain' , destination : 'uploads/' , filename : 'sampleText.txt' , path : 'uploads/sampleText.txt' , size : 5575 } [ Object : null prototype ] { user : 'AppCoda' , greeting : 'Hello 😀 !!!' } |
Let'southward upload multiple files now. Nosotros'll do that by creating a new method similar to the previous one, with the difference existence that instead of initializing one FileInfo
object, we'll initialize iii of them so we tin upload all sample files nosotros take. Here it is:
func uploadMultipleFiles ( ) { let textFileURL = Bundle . main . url ( forResource : "sampleText" , withExtension : "txt" ) permit textFileInfo = RestManager . FileInfo ( withFileURL : textFileURL , filename : "sampleText.txt" , proper noun : "uploadedFile" , mimetype : "text/patently" ) let pdfFileURL = Bundle . chief . url ( forResource : "samplePDF" , withExtension : "pdf" ) allow pdfFileInfo = RestManager . FileInfo ( withFileURL : pdfFileURL , filename : "samplePDF.pdf" , name : "uploadedFile" , mimetype : "awarding/pdf" ) let imageFileURL = Package . main . url ( forResource : "sampleImage" , withExtension : "jpg" ) let imageFileInfo = RestManager . FileInfo ( withFileURL : imageFileURL , filename : "sampleImage.jpg" , name : "uploadedFile" , mimetype : "image/jpg" ) upload ( files : [ textFileInfo , pdfFileInfo , imageFileInfo ] , toURL : URL ( string : "http://localhost:3000/multiupload" ) ) } |
At the cease nosotros call over again the upload(files:toURL:)
method which will trigger the actual upload request. Find that the upload endpoint is different this time ("multiupload"). To meet it working, don't forget to call it in the viewDidLoad()
:
override func viewDidLoad ( ) { super . viewDidLoad ( ) //uploadSingleFile() uploadMultipleFiles ( ) } |
This time you should come across 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 ten simultaneous files to exist uploaded. Of course you lot are free to modify that limit according to your preference.
Summary
Starting in the previous tutorial where we created the start version of the RestManager
grade and continuing in this one where nosotros added the file uploading characteristic, nosotros take managed to build a modest and lightweight form capable of covering our needs in making web requests. "Multipart/form-data" content type and the way HTTP body is built tin can be sometimes confusing, but if y'all break things downwardly then everything gets like shooting fish in a barrel. I hope what I shared with you here today to exist of some value, and I wish you are enjoying RESTful services even more now. You are always welcome to add more than features or adjust the current implementation co-ordinate to your needs. See you side by side time!
For reference, you can download the full project on GitHub.
Source: https://www.appcoda.com/restful-api-tutorial-how-to-upload-files-to-server/
Posted by: alleyarerest.blogspot.com
0 Response to "What Data Takes When Upload Image To Post Api Call"
Post a Comment