I am an intermediate student in iOS development, I am trying to make a method that uploads an image to a server. I understand the server side scripting in PHP.
But when I am following a tutorial to upload an image in Xcode, I don't really grasp about NSData, NSObject, NSMutableData, NSString, it seems the tutorial doesn't really the fundamental aspect of NSData, NSMutableData, NSString...
If want to take beautiful display, I should learn about auto layout, collection view etc.
So, what kind of topic in iOS development that I should learn to really understand about these things step by step? It seems that I never learn specifically about these things. I don't know where to start.
The code to upload an image is like this:
func createBodyWithParameters(parameters: [String: String]?, filePathKey: String?, imageDataKey: NSData, boundary: String) -> NSData {
let body = NSMutableData();
if parameters != nil {
for (key, value) in parameters! {
body.appendString("--\(boundary)\r\n")
body.appendString("Content-Disposition: form-data; name=\"\(key)\"\r\n\r\n")
body.appendString("\(value)\r\n")
}
}
let filename = "user-profile.jpg"
let mimetype = "image/jpg"
body.appendString("--\(boundary)\r\n")
body.appendString("Content-Disposition: form-data; name=\"\(filePathKey!)\"; filename=\"\(filename)\"\r\n")
body.appendString("Content-Type: \(mimetype)\r\n\r\n")
body.appendData(imageDataKey)
body.appendString("\r\n")
body.appendString("--\(boundary)--\r\n")
return body
}
func generateBoundaryString() -> String {
return "Boundary-\(NSUUID().UUIDString)"
}
}
extension NSMutableData {
func appendString(string: String) {
let data = string.dataUsingEncoding(NSUTF8StringEncoding, allowLossyConversion: true)
appendData(data!)
}
}
Learning about objects you have described may mean many things. If you are after their capabilities then the documentation should be enough. But if you are more after what is under the hood and why we need these objects then I could only suggest you to look into some older language like C.
These objects NSData, NSMutableData, NSString are all data containers, buffers. But NSObject is just a base class from which all other objects inherit.
So a bit about NSData and NSMutableData:
In C when creating a raw buffer you use malloc which reserves a chunk in memory which you may use as you please. Once done you need to call free to release that memory or you will have a memory leak.
void *voidPointer = malloc(100); // Reserved 100 bytes of whatever data
int *intPointer = (int *)malloc(sizeof(int)*100); // Reserved enough bytes to fit 100 integers whatever their size may be
intPointer[13] = 1; // You may use these as normal array
free(voidPointer); // Free this memory
free(intPointer); // Free this memory
So NSData is basically a wrapper for that and does all of it for you. You may even access the raw pointer by calling bytes on NSData object.
Then the mutable version NSMutableData is just a subclass which has some additional functionality. You may actually append data. From what is under the hood appending data is not so simple. You need to allocate a new memory chunk, copy old data to it, copy new data and release the previous memory chunk.
void *currentData = malloc(100); // Assume we have some data
void *dataToAppend = malloc(100); // Another chunk of data we want to append
void *combinedData = malloc(200); // We need a larger buffer now
memcpy(combinedData, currentData, 100); // Copy first 100 bytes to new data
memcpy(combinedData+100, dataToAppend, 100); // Copy next 100 bytes to new data
free(currentData); // Free old data
free(dataToAppend); // Free old data
... use combinedData here ...
free(combinedData); // Remember to free combined data once done
These are all really simple methods but they may already be pain to write and it is easy to produce bugs doing so. So NSData or NSMutableData and even Data in Swift are all just data containers that make your developer life easier. And in Objective-C conversion from data to C buffers is as easy as it gets:
NSData *myData = [NSData dataWithBytes:myDataPointer length:myDataLength];
void *myRawPointer = [myData bytes];
The NSString is not really that different. In C we again have character pointer which is used as string so we write something like:
char *myText = "Some text";
These are a bit special, a convenience really. We could as well do:
char *myText = (char *)malloc(sizeof(char)*100);
And then fill the data character by character:
myText[0] = 'S';
myText[1] = 'o';
myText[2] = 'm';
...
myText[9] = 't';
myText[10] = '\0'; // We even need to set a null terminator at the end
and then we needed to free the memory again... But never mind the C strings, NSString is again a wrapper that is responsible to allocate the memory, assign data and do whatever you want with it. It has may methods you can use simply to make your life easier.
As to the code you posted it is a combination of the two. In your case your API accepts images as multipart form data requests which you may understand as a raw image file with a few texts added around it just to explain what the data contains. It is one of a generally used way but not the only one. You might as well just post the raw image data or you might even post a JSON containing a base64 string encoded data. Also as usually these texts are represented as an utf8 encoded data.
In the end it is a set of standards that are generally used so our computers may communicate between each other. Your image is most likely defined by a standard from png or jpg on how to present it with a string of bytes, your strings are defined by utf8 standard and your whole request body is defined by some HTTP standards (not even sure what part of it is that). And the objects you use and want to learn about are just some helpers for achieving your result. Understanding them in most cases is like understanding a screwdriver; you won't need to in most cases, but you do need to know they exist and you need to know when to use them.
The code itself you posted is relatively bad but should do its job. For a beginner it might be a bit confusing even. Probably a more logical pseudocode for this solution would be something like:
let imageData: Data // My image data
let headerString: String // Text I need to put before the image data
let footerString: String // Text I need to put after the image data
var dataToSend: Data = Data() // Generate data object
dataToSend.append(headerString.utf8Data) // Append header
dataToSend.append(imageData) // Append raw data
dataToSend.append(footerString.utf8Data) // Append footer
I hope this clears up a few things.
Related
I'm trying to compress data to improve the space complexity, but I'm not sure if I'm incorrectly compressing data or incorrectly measuring the size.
I tried the following in the Playground.
import Foundation
import Compression
// Example data
struct MyData: Encodable {
let property = "Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum."
}
// I tried using MemoryLayout to measure the size of the uncompressed data
let size = MemoryLayout<MyData>.size
print("myData type size", size) // 16
let myData = MyData()
let myDataSize = MemoryLayout.size(ofValue: myData)
print("myData instance size", myDataSize) // 16
func run() {
// 1. This shows the size of the encoded data
guard let encoded = try? JSONEncoder().encode(myData) else { return }
print("myData encoded size", encoded) // 589 bytes
/// 2. This shows the size after using a first compression method
guard let compressed = try? (encoded as NSData).compressed(using: .lzfse) else { return }
let firstCompression = Data(compressed)
print("firstCompression", firstCompression) // 491 bytes
/// 3. Second compression method (just wanted to try a different compression method)
let secondCompression = compress(encoded)
print("secondCompression", secondCompression) // 491 bytes
/// 4. Wanted to compare the size difference between compressed and uncompressed for a bigger data so here is the array of uncompressed data.
var myDataArray = [MyData]()
for _ in 0 ... 100 {
myDataArray.append(MyData())
}
guard let encodedArray = try? JSONEncoder().encode(myDataArray) else { return }
print("myData encodedArray size", encodedArray) // 59591 bytes
print("memory layout", MemoryLayout.size(ofValue: encodedArray)) // 16
/// 5. Compressed array
var compressedArray = [Data]()
for _ in 0 ... 100 {
guard let compressed = try? (encoded as NSData).compressed(using: .lzfse) else { return }
let data = Data(compressed)
compressedArray.append(data)
}
guard let encodedCompressedArray = try? JSONEncoder().encode(compressedArray) else { return }
print("myData compressed array size", encodedCompressedArray) // 66661 bytes
print("memory layout", MemoryLayout.size(ofValue: encodedCompressedArray)) // 16
/// 6. Compression using lzma
var differentCompressionArray = [Data]()
for _ in 0 ... 100 {
guard let compressed = try? (encoded as NSData).compressed(using: .lzma) else { return }
let data = Data(compressed)
differentCompressionArray.append(data)
}
guard let encodedCompressedArray2 = try? JSONEncoder().encode(differentCompressionArray) else { return }
print("myData compressed array size", encodedCompressedArray2) // 60702 bytes
print("memory layout", MemoryLayout.size(ofValue: encodedCompressedArray2)) // 16
}
run()
// The implementation for the second compression method
func compress(_ sourceData: Data) -> Data {
let pageSize = 128
var compressedData = Data()
do {
let outputFilter = try OutputFilter(.compress, using: .lzfse) { (data: Data?) -> Void in
if let data = data {
compressedData.append(data)
}
}
var index = 0
let bufferSize = sourceData.count
while true {
let rangeLength = min(pageSize, bufferSize - index)
let subdata = sourceData.subdata(in: index ..< index + rangeLength)
index += rangeLength
try outputFilter.write(subdata)
if (rangeLength == 0) {
break
}
}
}catch {
fatalError("Error occurred during encoding: \(error.localizedDescription).")
}
return compressedData
}
The MemoryLayout object doesn't seem to be helpful in measuring the size of encoded arrays whether or not they're compressed. I'm not sure how to measure a struct or an array of struts without encoding them with JSONEncoder which already compresses the data.
The before/after compression for the single instance of MyData (#1, #2, and #3) seems to show that the data is being properly compressed going from 589 bytes to 491 bytes. However, the comparison between an array of uncompressed data and an array of compressed data (#4, #5) seems to show that the size increased from 59591 to 66661 after the compression.
Finally, I tried using a different compression algorithm lzma (#6). It reduced the size to 60702 which is lower than the previous compression, but it still wasn't smaller than the uncompressed data.
To get a bit of confusion out of the way first: MemoryLayout gives you information about the size and structure of the layout of a type at compile time, but can't be used to determine the amount of storage an Array value needs at runtime because the size of the Array structure itself does not depend on how much data it contains.
Highly simplified, the layout of an Array value looks like this:
┌─────────────────────┐
│ Array │
├──────────┬──────────┤ ┌──────────────────┐
│ length │ buffer ─┼───▶│ storage │
└──────────┴──────────┘ └──────────────────┘
1 word / 1 word /
8 bytes 8 bytes
└─────────┬─────────┘
└─▶ MemoryLayout<Array<UInt8>>.size
An Array value stores its length, or count (mixed in with some flags, but we don't need to worry about that) and a pointer to the actual space where the items it contains are stored. Those items aren't stored as part of the Array value itself, but separately in allocated memory which the Array points to. Whether the Array "contains" 10 values or 100000 values, the size of the Array structure remains the same: 1 word (or 8 bytes on a 64-bit system) for the length, and 1 word for the pointer to the actual underlying storage. (The size of the storage buffer, however, is exactly determined by the number of elements it is able to contain, at runtime.)
In practice, Array is significantly more complicated than this for bridging and other reasons, but this is the basic gist; this is why you only ever see MemoryLayout.size(ofValue:) return the same number every time. [And incidentally, the size of String is the same as Array for similar reasons, which is why MemoryLayout<MyData>.size also reports 16.]
In order to know how many bytes an Array or a Data effectively take up, it's sufficient to ask them for their .count: Array<UInt8> and Data are both collections of UInt8 values (bytes), and their .count will reflect the amount of data effectively stored in their underlying storage.
As for the size increase between step (4) and (5), note that
Step 4 takes 100 copies of your MyData and joins them together before converting them to JSON, while
Step 5 takes 100 copies of individually compressed MyData instances, joins those together, and then re-coverts them to JSON
Step 5 has a few issues compared to step 4:
Compression benefits heavily from repetition in data: a bit of data compressed and repeated 100 times won't be nearly as compact as a bit of data repeated 100 times, then compressed, because each round of compression can't benefit from knowing that there's another copy of the data that came before it. As a simple example:
Let's say we wanted to use a form of run-length encoding to compress the string Hello: there isn't a lot we can do, except maybe turn it into Hel{2}o (where {2} indicates a repetition of the last character 2 times)
If we compress Hello and join it 3 times, we get might get Hel{2}oHel{2}oHel{2}o,
But if we first joined Hello 3 times and then compressed, we could get {Hel{2}o}{3}, which is much more compact
Compression also typically needs to insert some information about how the data was compressed in order to be able to recognize and decompress the data later. By compressing MyData 100 times and joining all of those instances, you're repeating that metadata 100 times
Even after compressing your MyData instances, re-representing them as JSON decreases how compressed they are because it can't represent the binary data exactly. Instead, it has to convert each Data blob into a Base64-encoded string, which causes it to grow again
Between these issues, it's not terribly surprising that your data is growing. What you actually want is a modification to step 4, which is compressing the joined data:
guard let encodedArray = try? JSONEncoder().encode(myDataArray) else { fatalError() }
guard let compressedEncodedArray = try? (encodedArray as NSData).compressed(using: .lzma) else { fatalError() }
print(compressedEncodedArray.count) // => 520
This is significantly better than
guard let encodedCompressedArray = try? JSONEncoder().encode(compressedArray) else { fatalError() }
print(encodedCompressedArray.count) // => 66661
As an aside: it seems unlikely that you're actually using JSONEncoder in practice to join data in this way, and this was just for measurement here — but if you actually are, consider other mechanisms for doing this. Converting binary data to JSON in this way is very inefficient storage-wise, and with a bit more information about what you might actually need in practice, we might be able to recommend a more effective way to do this.
If what you're actually doing in practice is encoding an Encodable object tree and then compressing that the one time, that's totally fine.
I've created a Service file in order to handle all of my networking within the Weather Application that I am fine tuning. Within this service file, I use protocols, in order to return the retrieved data from GET requests to the appropriate View Controller.
During my code refactoring, and for the sake of learning, rather than using URLSessions, I decided I wanted to learn how to use Alamofire.
One of my GET requests retrieves an image of either a (sun, cloud, rain cloud, etc.), depending on the weather of a certain city (this is an example of the URL I am submitting my GET request to: http://openweathermap.org/img/wn/03n#2x.png.
Before I imported Alomofire, I would GET the bytes of this image, and render the bytes within UIImage like so:
self.weatherIcon.image = UIImage(data: result)
This worked just fine. But now, when using Alamofire for my request, the challenge I am having is that I'm unable to convert AFDataResponse to type Data, in order to then be rendered to UIImage.
Below you may see my GET Request.
AF.request(myUrl).responseData{response in
debugPrint(reponse)
self.delegate3?.iconServiceDelegateDidFinishWithData(result: response)
}
The response is of type AFDataResponse.
Therefore, when trying to write:
self.weatherIcon.image = UIImage(data: result)
I get an error saying,
Cannot convert value of type 'AFDataResponse (aka 'DataResponse<Data, AFError') to expected argument type 'Data'.
Any help would be much appreciated from the community.
Thanks.
Simple answer:
Create a variable of type Data and assign response.data to this variable.
AF.request(myUrl).responseData{ response in
debugPrint(response)
var imgData : Data //create variable of type data
imgData = Data(response.data!) // access the data through response.data
self.delegate3?.iconServiceDelegateDidFinishWithData(result: imgData)
}
There are many ways to do this. You can access the data directly, as you originally suggested (though I would treat it as an Optional rather than force unwrapping, as it'll crash otherwise).
.responseData { response in
let image = response.data.map(UIImage.init(data:)) // Creates UIImage?
}
You can transform the Result value to maintain any Error you receive.
.responseData { response in
let image = response.result.map(UIImage.init(data:)) // Creates Result<UIImage, AFError>
}
Or you can map the entire DataResponse.
.responseData { response in
let imageResponse = response.map(UIImage.init(data:)) // Creates DataResponse<UIImage, AFError>
}
I thought I'd ask after hours of inconclusive research and tests:
Introduction
I'm trying to send very large arrays of Doubles from an app to a server, naturally, I want to compress this as much as possible.
Specifically, these array contain CMDeviceMotion components (acceleration, x, y, z, gyroscope, etc...), but this question should apply to any large array of numbers (over 100K or a million values)
What I've tried and found by researching options
Say I have a large array of Double (There are many others) :
var CMX = CM.map({$0.userAcceleration.x})
here, CMX is of type [Double] and CM is [CMDeviceMotion]
I've tried making POST requests to my server by sending CMX in different ways, then calculating the total size after I receive it on the server :
First, as a single comma separated string :
{"AX":"-0.0441827848553658,-0.103976868093014,-0.117475733160973,-0.206566318869591,-0.266509801149368,-0.282151937484741,-0.260240525007248,-0.266505032777786,-0.315020948648453,-0.305839896202087,0.0255246963351965,0.0783950537443161,0.0749507397413254,0.0760494321584702,-0.0101579604670405,0.106710642576218,0.131824940443039,0.0630970001220703,0.21177926659584,0.27022996544838,0.222621202468872,0.234281644225121,0.288497060537338,0.176655143499374,0.193904414772987,0.169417425990105,0.150193274021149,0.00871349219232798,-0.0270088445395231,-0.0 ....
Size 153 Kb.
It makes sense that this is larger than sending as binary data, since a single number here is 64 bits (8 bytes), and becomes 17 bytes long (one byte per character) +1 = 18 (added a character for the comma).
With this reasoning, sending the array as binary data should be smaller.
Base 64 encoding
Here, I convert the array to a Data object using NSKeyedArchiver and base 64 encode the data before sending it :
["AX":NSKeyedArchiver.archivedData(withRootObject:CM.map({$0.userAcceleration.x})).base64EncodedString()]
This made the file size 206 Kb
Sending the data as a JSON array
By just sending :
["AX": CM.map({$0.userAcceleration.x})]
It turned out that this array of numbers was practically converted to a comma separated string, the size ended up being the same as in trial 1 (160Kb)
Sending as Data without base 64 encoding
Doing this:
["AX":NSKeyedArchiver.archivedData(withRootObject:CM.map({$0.userAcceleration.x}))
made the application crash at runtime, so I can't send a Data object as a value in a JSON
Question
How can I send these array in a more condensed way in a JSON object ?
Note that I already have downsampling in mind, and using 32 bit floats to reduce the size.
Simple way would be to do this:
let data: Data = CMX.withUnsafeBufferPointer { pointer in
return Data(buffer: pointer)
}
And you have binary buffer with all your Doubles/Floats combined.
But because HTTP is text-based protocol you will have to convert this data to base64 string:
let base64String = data.base64EncodedString()
And this base64String should be passed for AX parameter of your POST(?) HTTP request.
EDIT:
To convert it back you may use code like this:
extension Array {
init?(data: Data) {
// This check should be more complex, but here we just check if total byte count divides to one element size in bytes
guard data.count % MemoryLayout<Element>.size == 0 else { return nil }
let elementCount = data.count / MemoryLayout<Element>.size
let buffer = UnsafeMutableBufferPointer<Element>.allocate(capacity: elementCount)
data.copyBytes(to: buffer)
self = buffer.map({$0})
buffer.deallocate()
}
// Wrapped here code above
var data: Data {
return self.withUnsafeBufferPointer { pointer in
return Data(buffer: pointer)
}
}
}
let converted: [Double]? = Array(data: CMX.data) // converted now should be equal to CMX
If this seems like a dupe, sorry. I'm trying to ask a very specific question, and not sure my searching has really led me to the right place. Anyway, here's the setup. Take a picture on the iPhone camera, turn it into base64 string data, shove it up the wire to a Node API, turn that into a file to shove onto S3. Pretty straight forward.
General disclaimers apply; I'd prefer to use a B64 string in JSON for simplicity and universality, and I'll withhold further comments on the silliness of form-encoded uploads :)
Here's my very simple Swift code to produce B64, turn it back into an image, and display it as a proof that the stuff works - at least in Apple land.
Note: "redButton" is one of the assets in my app. I switched to that for the sake of sending much smaller packets at the API for testing, but the results remain. Thanks.
func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [String : Any]) {
// build data packet
imagePicker.dismiss(animated: true, completion: nil)
//let image:UIImage = (info[UIImagePickerControllerOriginalImage] as? UIImage)!
let image:UIImage = UIImage(named: "redButton")!
showAlert( title: "size", msg: String( describing: image.size ) )
let data = UIImageJPEGRepresentation(image, 0.5)
let b64 = data!.base64EncodedData()//.base64EncodedString()//options: Data.Base64EncodingOptions.lineLength64Characters)
let b64String = b64.base64EncodedString()
debugPrint( "len=" + String( describing: b64String.lengthOfBytes(using: String.Encoding.utf8)))
let newDataString = Data.init(base64Encoded: b64String )
let newData = Data.init(base64Encoded: newDataString! )
let newImage = UIImage.init(data: newData!)
tmpImage.image = newImage
}
That all works. I see the image in the little UIImage.
So at least fully in the Apple camp, the img->b64->img works.
However...
When I copy the actual resulting blob of b64-encoded string data, and manually paste it into the source attribute, marked up with the data stuff, it does NOT display the expected image, and in fact, just shows a broken image in the browser.
ie...
<html>
<body>
<img src="data:image/jpg;base64,LzlqLzRBQVFTa1pKUmdBQkFRQUFTQUJJ (brevity)...">
</body>
</html>
So, am I doing something wrong in my proofing in the HTML page? Am I expecting the wrong results from what Apple calls base64 string data? Am I just plain missing something painfully obvious that my sleep-deprived brain is missing?
It eventually gets sent to the server in an HTTP POST call, per normal means, as a dictionary, turned into json via the json encoding stuff in newer Swift.
var params = ["image": [ "content_type": "image/jpg", "filename":"test.jpg", "file_data": b64String] ]
And for the sake of compeleteness, here's the Node code where I reconstitute this data into a binary bit, and from here I shove it up to the S3 system, and in every case, the file is not recognized as a proper JPG file.
var b64 = req.body.image.file_data;
var base64data = new Buffer(b64, 'base64'); // according to all the new-node version docs
I'm on the home stretch of a crunch-time product that we're shoving at investors next week, and apparently this is a critical feature to show off for that meeting.
Tell me I'm missing something painfully obvious, and that I'm stupid. I welcome it, please. It can't not be just something stupid, right?
Thanks!
Here:
let b64 = data!.base64EncodedData()
let b64String = b64.base64EncodedString()
you encode the given data twice. It should be just
let b64String = data!.base64EncodedString()
Your “in Apple land” test works because
let newDataString = Data.init(base64Encoded: b64String )
let newData = Data.init(base64Encoded: newDataString! )
also decodes the Base64 twice. That would now be just
let newData = Data(base64Encoded: b64String)
I'm using MonoTouch and I have a UIImage (displayed in a UIImageView and it looks good) and I'm trying to convert it to NSData, but AsJPEG and AsPNG returns null. What can be the problem?
My code looks like this:
NSError err;
NSData imageData = CroppedImageView.Image.AsJPEG(); // imageData is null!
if (!imageData.Save ("tmp.png", true, out err)) {
Console.WriteLine("Saving of file failed: " + err.Description);
}
The AsJPEG method calls UIImageJPEGRepresentation and its return value is documented as:
A data object containing the JPEG data, or nil if there was a problem generating the data. This function may return nil if the image has no data or if the underlying CGImageRef contains data in an unsupported bitmap format.
The is similar to many API in iOS (and OSX) where exception are not commonly used (and null is used to report some kind of error).
Anyway you should check your image dimensions and properties - they might give you an hint at something that would not translate into a JPEG bitmap.
Also since the NSData can represent a very large amount of memory you should try to limit it's life, e.g.:
using (NSData imageData = CroppedImageView.Image.AsJPEG ()) {
NSError err;
if (!imageData.Save ("tmp.jpg", true, out err)) {
Console.WriteLine("Saving of file failed: " + err.Description);
}
}
It looks like you are writing to a file in the current directory of the app, this is readonly.
You should use:
var path = System.IO.Path.GetTempFilename();
or
var path = System.IO.Path.Combine(System.IO.Path.GetTempPath(), "tmp.png");
Like you would do on other platforms, and use a file from there.
You can also use Environment.SpecialFolder.MyDocuments.
The AsJPEG returned null because the image size was too big (it was taken with an iPhone 5). After I Scaled it down by 2, it generates the data properly.