Swift 3 - Save image alternative - ios

I need to found an alternative of this method to save images
let save = UserDefaults.standard
let imageData = UIImageJPEGRepresentation(Photo.image!, 1.0)
save.set(imageData, forKey: "Image")
save.synchronize()
if let imgData = save.object(forKey: "Image"){
let compressedJPGImage = UIImage(data: imgData as! Data)
}
and load images
let imgData = save.object(forKey: "Image")
let compressedJPGImage = UIImage(data: imgData as! Data)
Photo.image = compressedJPGImage
The problem with this method is that i have a lot of another value saved with UserDefaults.standard so it take a lot of time (5-10 minutes) when i synchronize.

It is not advisable to save large files like images to UserDefaults. UserDefaults was intended to save very small data such as a user's preferred theme color of your app. Perhaps a suitable alternative is to save your images in the document directory. Here is a function that will allow you save an image:
func saveImage(image: UIImage) -> String {
let imageData = NSData(data: UIImagePNGRepresentation(image)!)
let paths = NSSearchPathForDirectoriesInDomains(FileManager.SearchPathDirectory.documentDirectory, FileManager.SearchPathDomainMask.userDomainMask, true)
let docs = paths[0] as NSString
let uuid = NSUUID().uuidString + ".png"
let fullPath = docs.appendingPathComponent(uuid)
_ = imageData.write(toFile: fullPath, atomically: true)
return uuid
}
The above function will create the name of the saved image for you. If you prefer to specify the name of the image you are saving then you could do the following (but you will be responsible for ensuring the image names you specify are unique):
func saveImage(image: UIImage, withName name: String) {
let imageData = NSData(data: UIImagePNGRepresentation(image)!)
let paths = NSSearchPathForDirectoriesInDomains(FileManager.SearchPathDirectory.documentDirectory, FileManager.SearchPathDomainMask.userDomainMask, true)
let docs = paths[0] as NSString
let name = name
let fullPath = docs.appendingPathComponent(name)
_ = imageData.write(toFile: fullPath, atomically: true)
}
To retrieve those images, you could pass the image name to this function:
func getImage(imageName: String) -> UIImage? {
var savedImage: UIImage?
if let imagePath = getFilePath(fileName: imageName) {
savedImage = UIImage(contentsOfFile: imagePath)
}
else {
savedImage = nil
}
return savedImage
}
Which relies on this function to work:
func getFilePath(fileName: String) -> String? {
let nsDocumentDirectory = FileManager.SearchPathDirectory.documentDirectory
let nsUserDomainMask = FileManager.SearchPathDomainMask.userDomainMask
var filePath: String?
let paths = NSSearchPathForDirectoriesInDomains(nsDocumentDirectory, nsUserDomainMask, true)
if paths.count > 0 {
let dirPath = paths[0] as NSString
filePath = dirPath.appendingPathComponent(fileName)
}
else {
filePath = nil
}
return filePath
}
Here is an example of how you would now save your images instead of UserDefaults. I am saving an image I will call "Image":
saveImage(image: Photo.image, withName name: "Image")
Here is an example of how I would retrieve the saved image:
if let theSavedImage = getImage(imageName: "Image") {
//I got the image
}

UserDefaults is a place to store a small portion of data like user preferences. UserDefaults has very limited space and can be quite slow. In your case, you mentioned it's 5-10 minutes which I doubt though.
If you want images to be stored across the sessions of the app (persistent storage), you should consider using file system (Application_Folder/Library/Cache/) or Core Data framework. You will get better performance here while accessing the image.
If images are not needed to be persisted and need to be stored for a single session of the app, you should use the imageNamed: API of UIImage class. This API loads the image once in the memory and keeps it in the system cache. For all the successive accesses it refers to the cached image only. This will increase the system cache size and application's memory footprint if you are loading too many images. Another API is imageWithContentsOfFile:. Unlike the first API, this API will always load the new image instance in memory. Memory will be deallocated once image instance is released which is not the case with the first API.

Related

ios FileManager losing stored data when rebuilding app

I'm trying to store images inside the app's document folder, so the user can retrieve them at any later time that they want to. This is my code to store them:
func store(_ image: UIImage) -> String {
let imageName = "\(Date().timeIntervalSince1970)"
let imagePath = "\(documentasPath)/\(imageName).png"
let imageData = UIImagePNGRepresentation(image)
fileManager.createFile(atPath: imagePath, contents: imageData, attributes: nil)
return imagePath
}
And this is my code to retrieve the image from the storage:
func retrieveImage(from path: String) -> UIImage? {
guard fileManager.fileExists(atPath: path) else {
return nil
}
return UIImage(contentsOfFile: path)
}
It seems to work fine, except when I rebuild the app from xcode. Then all of my stored images disappear (although all of the paths I stored that pointed to them are still present and correct).
Is this some behavior of the default file manager? And is there a way to avoid this from happening? I want the images to only be deleted either manually or when I uninstall the app.
Thanks
The problem is that you are storing an absolute path. You can't do that, because your app is sandboxed, which means (in part) that the URL of the Documents folder can change. Store just the document name, and each time you want to save to it or write from it, calculate the path to the Documents folder again and append the document name and use that result as your path.
Change to this
func store(_ image: UIImage) -> String {
let imageName = "\(Date().timeIntervalSince1970)"
let documentsUrl = NSURL(fileURLWithPath: NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true)[0])
var imagePath = documentsUrl.appendingPathComponent("\(imageName).png")
let imageData = UIImagePNGRepresentation(image)
fileManager.createFile(atPath: imagePath, contents: imageData, attributes: nil)
return imagePath
}
func retrieveImage(from path: String) -> UIImage? {
guard fileManager.fileExists(atPath: path) else {
return nil
}
return UIImage(contentsOfFile: path)
}

Issue while image was converted to NSData

When I pick an image from gallery, I convert it to NSData & assign it a variable like so...
func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [String : Any]) {
if let image = info[UIImagePickerControllerOriginalImage] as? UIImage {
UIImageWriteToSavedPhotosAlbum(image, self, #selector(image(_:didFinishSavingWithError:contextInfo:)), nil)
let data = UIImagePNGRepresentation(image) as NSData?
self.appDelegate.mydata1 = data!
}
Now I'm trying to store self.appDelegate.mydata1 to coredata like so..
guard let appDelegate = ... else {
return
}
let managedContext = ...
let entity = ...
let newProdObj = ...
newProdObj.setValue(self.appDelegate.mydata1, forKey: "imageData") //The attribute imageData in xcdatamodel is of type 'Binary Data'
do {
try managedContext.save()
self.newProductDetails.append(newProdObj as! NewProduct)
} catch let error as NSError {}
Later in another viewcontroller I'm fetching it like so...
guard let appDelegate = ...
let managedContext = ...
let fetchRequest = ...
do {
newProdDetails = try managedContext.fetch(fetchRequest as! NSFetchRequest<NSFetchRequestResult>) as! [NewProduct]
for result in newProdDetails {
print(result)
if let imageData = result.value(forKey: "imageData") as? NSData {
print(imageData)
}}
} catch let error as NSError {}
But when I try to print imageData, the control gets stuck and it goes on continuously printing numbers (which is the data) something like so...<123214 434534 345345 ...etc.
Did go through other posts with the same issue but couldn't get much help from them...
Instead of saving image data in coreData, save your images in document directory and name of those images in UserDefaults/Core Data.
And then you can fetch name from UserDefaults/Core Data and you can get the filPath for those images using this :
// Get path for any filename in document directory
func getFilePathInDocuments(fileName:String, completionHandler:#escaping(String, Bool)->()) {
let path = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true)[0] as String
let url = NSURL(fileURLWithPath: path)
let fileManager = FileManager.default
let filePath = url.appendingPathComponent(fileName)?.path
if (fileManager.fileExists(atPath: filePath!)) {
completionHandler(filePath!, true)
}else{
completionHandler("File not found!",false)
}
}
If you want to know how to write your images in document directory see this : how to use writeToFile to save image in document directory?
This is not an issue. You are fetching the NSData which is an image, hence its printing the bytes in the image data which are the numbers you are talking about. Also this code is executing in the main thread, hence your app is unresponsive.
As the other answer pointed out save your image in document directory and save the relative path of the imagename in core data. This way you can fetch the name of the imagepath(relative mind you, as full document directory URL can change in app launch), and load the image from the document directory.
Also when saving the image in document directory, please do so in a background queue, using GCD or NSOperation/Operation, as if this image is large it can again block the main thread, as you are operating this in the main thread.
My suggestion is that convert image data to base64 string, and store it to CoreData. Later when you get back image, you can convert it back to NSData. Please refer this for base64 string conversion:
convert base64 decoded NSData to NSString

Save and Retrieve Image File From Realm

Writing to the file system allows users to write and retrieve string, image, etc., files to and from the application on their devices.
The images I write and retrieve to the app that appear when I run the simulator, for example, are unique to that app bundle, and differ from the images I write and retrieve from my device.
When I research how to write and read image files with Realm I am told not to save the image but the path. Images are too large to store. Got it.
But the file path I am saving naturally returns data native to the specific app bundle-- or returns nil. How might I write and retrieve image files/paths to and from Realm if the file path is unique to the device(s)? Do I need a separate image array?
First View Controller
let planet = planets[indexPath.item]
cell.name.text = planet.name
cell.system.text = planet.system
let imageData = (self.getDirectoryPath() as NSString).appendingPathComponent("image")
if self.fileManager.fileExists(atPath: imageData) {
// Populates every cell with the same image
cell.earthImage.image = UIImage(contentsOfFile: imageData)
// returns nil
cell.earthImage.image = UIImage(contentsOfFile: planet.image)
}
// Retrieve directory path for image
func getDirectoryPath() -> String {
let paths = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true)
let documentsDirectory = paths[0]
return documentsDirectory
}
Second View Controller
let planet = Planet()
self.realm = try! Realm(configuration: config(user: SyncUser.current!))
planet.name = self.name.text!
planet.system = self.system.text!
let imageData = (self.getDirectoryPath() as NSString).appendingPathComponent("person")
if self.fileManager.fileExists(atPath: imageData) {
self.earthImage.image = UIImage(contentsOfFile: imageData)
// Why does this not write to realm?
planet.image = imageData
}
try! self.realm.write {
self.realm.add(planet)
}
Here, I am posting from the secondViewController where I would like to render these values in my collectionView in the firstViewController's numberOfItemsInSection_:) method.
Here's the model:
class Planet: Object {
#objc dynamic var name = String()
#objc dynamic var system = String()
#objc dynamic var image = String()
}

Problems with loading image path from realm in Swift

In my app I am storing an image in local storage and I am saving the path of that image in my Realm database. And now i have problems with load this image from that path?
Thats how I save path to database:
let nsDocumentDirectory = FileManager.SearchPathDirectory.documentDirectory
let nsUserDomainMask = FileManager.SearchPathDomainMask.userDomainMask
let paths = NSSearchPathForDirectoriesInDomains(nsDocumentDirectory,nsUserDomainMask, true)
let dirPath = paths.first
let imageURL1 = URL(fileURLWithPath: dirPath!).appendingPathComponent("refuge1.jpg")
let med1 = Meditation()
med1.name = "Refuge"
med1.count = 0
med1.targetAmount = 111111
med1.malasStep = 108
med1.imagePath = imageURL1.path
med1.id = 1
It's straightforward that am trying to get an image from this meditation.imagePath path. I double-checked the path, image is there still am not able to set the image using this path, is there is something that am missing?
In debug mode I see this:
Meditation {
name = Refuge;
count = 0;
targetAmount = 111111;
malasStep = 108;
imagePath = /Users/macbook/Library/Developer/CoreSimulator/Devices/2E25309F-D6A9-41C3-9EF4-67203142172C/data/Containers/Data/Application/F198640B-3C72-4F9C-8173-FB00D3ABEC15/Documents/refuge1.jpg;
id = 1;}
but my variable image still nil in debug mode
// Configure the cell...
cell.nameOfMeditation.text = meditation.name
cell.countOfMeditation.text = String(meditation.count)
let image = UIImage(contentsOfFile: meditation.imagePath)
cell.imageForMeditation.image = image
return cell
I see name of meditation and sound, bot no omg.
It's not advised to save the absolute file path of a file in an iOS app (ie, everything including /Users/macbook/Library/Developer/...), in Realm or anywhere else.
For security reasons, iOS devices rename the UUID folder name between launches. This means that while the folder path was valid at the time it was saved, it won't be at a later date.
Instead, it's recommended to save just the relative path of the file (eg, its location in relation to just the Documents folder. In this case, it would be just /refuge1.jpg) and to then dynamically build the absolute path by requesting the Documents directory path as you need it.
Try this:
// Use these convenience methods if you do a lot of saving and loading
func getDocumentsURL() -> URL {
let documentsURL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]
return documentsURL
}
func fileInDocumentsDirectory(_ filename: String) -> String {
let fileURL = getDocumentsURL().appendingPathComponent(filename)
return fileURL.path
}
func saveRefugeOne(image: UIImage) {
// Create a file name, and then save the path in your documents with that name
let imageFileName:String = "refuge1.jpg"
let imagePath = fileInDocumentsDirectory(imageFileName!)
saveImage(image, path: imagePath)
}
func loadRefugeOne() -> UIImage? {
// Get the image back
let imageName:String = "refuge1.jpg" // Or whatever name you saved
let imagePath = fileInDocumentsDirectory(imageName)
if let loadedImage = self.loadImageFromPath(imagePath) {
return loadedImage
} else {
print("Couldn't Load: \(imageName)")
return nil
}
}
// This will be your method to save image
func saveImage(_ image: UIImage, path: String ) {
//If you want PNG use this let pngImageData = UIImagePNGRepresentation(image)
// But since you mentioned JPEG:
if let jpgData = UIImageJPEGRepresentation(image, 1.0) {
try? jpgData.write(to: URL(fileURLWithPath: path), options: [.atomic])
}
}
// This will load image from saved path. Make sure to store the path
// somewhere. This makes it easier to save images locally. You can
// save the image in the documents directory, and then just save the
// path in CoreData or something similar.
func loadImageFromPath(_ path: String) -> UIImage? {
let image = UIImage(contentsOfFile: path)
if image == nil {
print("couldn't find image at path: \(path)")
}
return image
}
Hopefully this will help. It's the method I always use, and it works like a charm when I follow my own steps right ;-)

How can I save a photo taken with UIImagePickerController camera to be displayed later in the app?

I am having the user take a photo with UIImagePickerController and I need it to be saved to the app so that it can be displayed when they need to see it later. How can I accomplish this?
I have heard NSUserDefaults would be a mistake. All I need to save is a single image, never more.
Here is the function to save your image to document folder
func saveImage (image: UIImage, path: String ) -> Bool{
let pngImageData = UIImagePNGRepresentation(image)
//let jpgImageData = UIImageJPEGRepresentation(image, 1.0) // if you want to save as JPEG
let result = pngImageData.writeToFile(path, atomically: true)
return result
}
Here is the function to get your document directory path with image name
func fileInDocumentsDirectory(filename: String) -> String {
let documentsFolderPath = NSSearchPathForDirectoriesInDomains(NSSearchPathDirectory.DocumentDirectory, NSSearchPathDomainMask.UserDomainMask, true)[0] as! String
return documentsFolderPath.stringByAppendingPathComponent(filename)
}
Here is an example of how to use your image path
let imagePath = fileInDocumentsDirectory(myImageName)
And here is how you save your image to that folder
saveImage(image, path: imagePath)
Now the last part is to get your image, so use this function to get your image
func loadImageFromPath(path: String) -> UIImage? {
let image = UIImage(contentsOfFile: path)
if image == nil {
println("Image not available at: (path)")
}
return image
}
And here is how you use the above function
image = loadImageFromPath(imagePath)
imageView.image = image
Hope this helps you

Resources