Swift: NSCoding Implementation For An Object Without Instance Variables - ios

Case in point: Beginner here please bear with me. I just learnt how to persist objects by encoding/decoding & archiving/unarchiving. Problem is now I want to persist a single UIImage. What is the recommended way to do that?
My current implementation that works but looks really weird:
class Photo: NSObject, NSCoding {
static let documentsDirectory = FileManager.default.urls(for: .documentDirectory,in: .userDomainMask).first!
static let archiveURL = documentsDirectory.appendingPathComponent("photo")
static func saveToDisk(selectedPhoto: UIImage) {
NSKeyedArchiver.archiveRootObject(selectedPhoto, toFile: archiveURL.path)
}
static func loadFromDisk() -> UIImage? {
guard let unarchivedPhoto = NSKeyedUnarchiver.unarchiveObject(withFile: archiveURL.path) as? UIImage
else {return nil}
return unarchivedPhoto
}
func encode(with aCoder: NSCoder) {
}
convenience required init?(coder aDecoder: NSCoder) {
self.init()
}
}
Is there a better way to do it? Many thanks.

Hope this helps
Please add correct file url in place of "filename"
func convertImageToDataAndWrite()
{
let img:UIImage = UIImage(named: "img1.png")!
let data:NSData? = UIImageJPEGRepresentation(img, 0.7)
if data != nil
{
var check:Bool = NSKeyedArchiver.archiveRootObject(data!, toFile: "filename")
}
}
func readImagefromFile()
{
let data:NSData = NSKeyedUnarchiver.unarchiveObjectWithFile("filename") as! NSData!
var img:UIImage = UIImage(data: data)!
}
Thanks.

I completely misunderstood the use of NSCoding and Archive. "Archives provide a means to convert objects and values into an architecture-independent stream of bytes that preserves the identity of and the relationships between the objects and values."
So to save/load a UIImage simply:
// Define path
let documentsDirectory = FileManager.default.urls(for: .documentDirectory,in: .userDomainMask).first!
let photoURL = documentsDirectory.appendingPathComponent("photo.jpg")
// Convert selectedPhoto to Data and write to path
if let data = UIImageJPEGRepresentation(selectedPhoto, 1) {
try? data.write(to: photoURL)
}
// Load Data and init UIImage
UIImage(contentsOfFile: photoURL.path)

Related

Saving a reference to an image in camera roll to recall later in app?

I'm trying to let the user take or select an image in an ImagePickerController, and I want to save a reference (as efficiently as possible) in my app to recall when the app loads again. Is saving the image's file URL the best approach for this?
import UIKit
import SwiftUI
struct ImagePicker: UIViewControllerRepresentable {
var sourceType: UIImagePickerController.SourceType = .photoLibrary
#Binding var selectedImage: UIImage
#Environment(\.presentationMode) private var presentationMode
func makeUIViewController(context: UIViewControllerRepresentableContext<ImagePicker>) -> UIImagePickerController {
let imagePicker = UIImagePickerController()
imagePicker.allowsEditing = false
imagePicker.sourceType = sourceType
imagePicker.delegate = context.coordinator
return imagePicker
}
func updateUIViewController(_ uiViewController: UIImagePickerController, context: UIViewControllerRepresentableContext<ImagePicker>) {
}
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
final class Coordinator: NSObject, UIImagePickerControllerDelegate, UINavigationControllerDelegate {
var parent: ImagePicker
init(_ parent: ImagePicker) {
self.parent = parent
}
func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) {
if let image = info[UIImagePickerController.InfoKey.originalImage] as? UIImage {
parent.selectedImage = image
}
if let imgUrl = info[UIImagePickerController.InfoKey.imageURL] as? URL{
let imgName = imgUrl.lastPathComponent
let documentDirectory = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true).first
let localPath = documentDirectory?.appending(imgName)
let image = info[UIImagePickerController.InfoKey.originalImage] as! UIImage
let data = image.pngData()! as NSData
data.write(toFile: localPath!, atomically: true)
//let imageData = NSData(contentsOfFile: localPath!)!
let photoURL = URL.init(fileURLWithPath: localPath!)//NSURL(fileURLWithPath: localPath!)
print(photoURL)
//TODO save this url in my app as a reference to look up
}
parent.presentationMode.wrappedValue.dismiss()
}
}
}
URL of image of camera roll may change , better way is to save your image to filesystem in the app sandbox and the you can save given name or something to retrieve it when u needed back
//MARK: save and retrive Images
extension UIImage {
func saveImage(imageName: String) {
guard let documentsDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first else { return }
let fileName = imageName
let fileURL = documentsDirectory.appendingPathComponent(fileName)
guard let data = self.jpegData(compressionQuality: 1) else { return }
//Checks if file exists, removes it if so.
if FileManager.default.fileExists(atPath: fileURL.path) {
do {
try FileManager.default.removeItem(atPath: fileURL.path)
print("Removed old image")
} catch let removeError {
print("couldn't remove file at path", removeError)
}
}
do {
try data.write(to: fileURL)
} catch let error {
print("error saving file with error", error)
}
}
static func loadImageFromDiskWith(fileName: String) -> UIImage? {
let documentDirectory = FileManager.SearchPathDirectory.documentDirectory
let userDomainMask = FileManager.SearchPathDomainMask.userDomainMask
let paths = NSSearchPathForDirectoriesInDomains(documentDirectory, userDomainMask, true)
if let dirPath = paths.first {
let imageUrl = URL(fileURLWithPath: dirPath).appendingPathComponent(fileName)
let image = UIImage(contentsOfFile: imageUrl.path)
return image
}
return nil
}
static func removeImage(fileName: String){
guard let documentsDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first else { return }
let fileURL = documentsDirectory.appendingPathComponent(fileName)
if FileManager.default.fileExists(atPath: fileURL.path) {
do {
try FileManager.default.removeItem(atPath: fileURL.path)
print("Removed image")
} catch let removeError {
print("couldn't remove file at path", removeError)
}
}
}
}
Usage
yourImage.saveImage(imageName: "imageNameToSave")//<-save
UIImage.loadImageFromDiskWith(fileName: "ImageNameToRetrive")//<-retrive
UIImage.removeImage(fileName: "ImageNameToRemove")//<-remove
Edit:
You can definitely do FileManager as well, it truly depends on how many files you will be saving, and to where you want to save them. If it is a measly 1 file, that doesn't need to be secured and is public to the app, UserDefaults is the way to go. If you want to add a bit more control of that file, FileManager would be the way to go.
UserDefaults is your way to go to store locally.
Store Image Data
func locallyStoreImgData(image: UIImage, key:String) {
if let pngRepresentation = image.pngData() {
UserDefaults.standard.set(pngRepresentation, forKey: key)
}
else {
//Was unable to create png representation
}
}
Retrieve Image Data
func obtainImg(key:String) -> UIImage? {
if let imageData = UserDefaults.standard.object(forKey: key) as? Data,
let image = UIImage(data: imageData) {
return image
}
return nil
}
Use Case
locallyStoreImgData(image: myImage, key: "myImageKey")
if let image = obtainImg(key: "myImageKey") {
//Do something with image
}
else {
//Was unable to recreate image
}

swift: Load multiple files from documents directory

I have folder with many files in documents directory. In my folder I have files with different extensions (jpg, png, mp3, zip). In my code I have stringArray. I want to load all files in someArray and after that add files with .png extension from someArray in stringArray How to do it?
Is it possible to do this? Or I should find another way to load multiple files from documents directory?
I find answer for Load multiple images from the folder or directory. - Swift 4
I tried to use this code from answer:
func loadImagesFromAlbum(folderName:String) -> [String]{
let nsDocumentDirectory = FileManager.SearchPathDirectory.documentDirectory
let nsUserDomainMask = FileManager.SearchPathDomainMask.userDomainMask
let paths = NSSearchPathForDirectoriesInDomains(nsDocumentDirectory, nsUserDomainMask, true)
var theItems = [String]()
if let dirPath = paths.first
{
let imageURL = URL(fileURLWithPath: dirPath).appendingPathComponent(folderName)
do {
theItems = try FileManager.default.contentsOfDirectory(atPath: imageURL.path)
return theItems
} catch let error as NSError {
print(error.localizedDescription)
return theItems
}
}
return theItems
}
But I can't get files from theItems. Because theItems it a string array. What I do wrong? How to get files from loadImagesFromAlbum?
I tried to use this code:
images = loadImagesFromAlbum(folderName: "/folder1")
But it is not help me. I get only names. But I need to get files.
Update
I use this code to load image from documents directory:
let nsDocumentDirectory = FileManager.SearchPathDirectory.documentDirectory
let nsUserDomainMask = FileManager.SearchPathDomainMask.userDomainMask
let paths = NSSearchPathForDirectoriesInDomains(nsDocumentDirectory, nsUserDomainMask, true)
if let dirPath = paths.first
{
let imageURL = URL(fileURLWithPath: dirPath).appendingPathComponent("/folder1/1.png")
myImageView.image = UIImage(contentsOfFile: imageURL.path)
}
But I need to load 100 images from /folder1 in var images = [String](). Because I use var images = [String]() to show images: contentVC.imageName = images[index] // and etc...
And the code I use above is not very convenient if I need add 40-100 images.
Update 1
I have this code to load images from my project and show it:
class PageViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
for page in 1...pageNumber[indexPage] {
images.append("page\(page).png")
}
}
func getContentViewController(withIndex index: Int) -> ContentViewController? {
if index < images.count{
let contentVC = self.storyboard?.instantiateViewController(withIdentifier: "ContentViewController") as! ContentViewController
contentVC.itemIndex = index
contentVC.imageName = images[index]
return contentVC
}
return nil
}
}
import UIKit
class ContentViewController: UIViewController {
#IBOutlet weak var imageView: UIImageView!
var itemIndex: Int = 0
var imageName: String?
override func viewDidLoad() {
super.viewDidLoad()
if let currentImage = imageName{
imageView.image = UIImage(named: currentImage)
}
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
}
But I need to load images from documents directory and show it. How to do it?
If I understand your question correctly you want to load an image for a specific name in a specific folder in the Documents folder.
Please try this, the method takes two parameters, the file name (with extension!) and the folder name and returns an UIImage or nil if the image cannot be found or created.
func loadImage(withName name : String, from folderName: String) -> UIImage? {
do {
let documentsFolderURL = try FileManager.default.url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: false)
let imageURL = documentsFolderURL.appendingPathComponent(folderName).appendingPathComponent(name)
let data = try Data(contentsOf: imageURL)
return UIImage(data: data)
} catch {
return nil
}
}
There is no benefit to get all file paths with contentsOfDirectory because you have to load the image data one by one anyway.

Save and read arrays Image into UserDefaults swift

I want to save images in an array using UserDefaults in Swift 3
var arrayImage = [UIImage]()
arrayImage.append(UIImage(data: dataImage as! Data)!) //add image to array
globalD.set(arrayImage, forKey: "FavoritesContactStandardImage")
I'm doing it that way, but it does not save, how can I save it and then read it?
Maybe you could try these. You just use saveImages & loadImages functions.
But you need to implement delete function by yourself.
let key = "FavoritesContactStandardImage"
func getImageKey(_ index:Int) -> String {
return "\(key)\(index)"
}
func saveImages(_ images:[UIImage]) {
var list = UserDefaults.standard.array(forKey: key) as? [String] ?? [String]()
var index = list.count
for image in images {
let imgKey = getImageKey(index)
saveImage(imgKey, image)
list.append(imgKey)
UserDefaults.standard.set(list, forKey: key)
UserDefaults.standard.synchronize()
index += 1
}
}
func saveImage(_ imageName:String, _ image:UIImage) {
let path = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true)[0] as NSString
let imgPath = URL(fileURLWithPath: path.appendingPathComponent(imageName))
do {
try UIImagePNGRepresentation(image)?.write(to: imgPath, options: .atomic)
} catch let error {
print(error.localizedDescription)
}
}
func loadImages() -> [UIImage] {
let path = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true)[0] as NSString
let list = UserDefaults.standard.array(forKey: key) as? [String] ?? [String]()
var imageList = [UIImage]()
for (index, _) in list.enumerated() {
let imageName = getImageKey(index)
let imgPath = URL(fileURLWithPath: path.appendingPathComponent(imageName))
if let image = UIImage(contentsOfFile: imgPath.path) {
imageList.append(image)
}
}
return imageList
}
Hope that's helpful.
That's a really bad idea to save image data on UserDefault , maybe better if you save the image name (if you put your image on assets catalog) / image path (if your put your image on custom directory on your project ). And if you want to save the downloaded image from services maybe you can save your downloaded image to a directory before you get the paath and save to UserDefault 😉

Cannot retrieve correctly saved NSKeyArchived Object in Swift3

I cannot retrieve an object saved as a NSkeyed archive in Swift 3, and am scracthcing my head. The object is successfully saved as a plist, but returned as nil when loading back in.
Here is the code I use:
The class itself to be saved as an object is fairly easy:
import Foundation
class ItemList:NSObject, NSCoding {
var name: String = "" //Name of the Item list
var contents: [Int] = [] //Ints referencing the CoreData PackItems
init (listname:String, ContentItems:[Int]) {
self.name=listname
self.contents=ContentItems
}
//MARK: NSCoding
public convenience required init?(coder aDecoder: NSCoder) {
let thename = aDecoder.decodeObject(forKey: "name") as! String
let thecontents = aDecoder.decodeObject(forKey: "contents") as! [Int]
self.init(listname: thename,ContentItems: thecontents)
}
func encode(with aCoder: NSCoder) {
aCoder.encode(self.name,forKey:"name")
aCoder.encode(self.contents, forKey: "contents")
}
}
The code to load and save the object:
class FileHandler: NSObject {
class func getDocumentsDirectory() -> URL {
let filemgr = FileManager.default
let urls = filemgr.urls(for: .documentDirectory, in: .userDomainMask)
let result:URL = urls.first!
return result
}
///This returns the contents of the handed file inside the Documents directory as the object it was saved as.
class func getFileAsObject(filename:String) -> AnyObject? {
let path = getDocumentsDirectory().appendingPathComponent(filename)
if let result = NSKeyedUnarchiver.unarchiveObject(withFile: path.absoluteString) {
//Success
print("Loaded file '"+filename+"' from storage")
return result as AnyObject?
} else {
print("Error: Couldn't find requested object '"+filename+"' in storage at "+path.absoluteString)
return nil
}
}
///This saves the handed object under the given filename in the App's Documents directory.
class func saveObjectAsFile(filename:String, Object:AnyObject) {
let data = NSKeyedArchiver.archivedData(withRootObject: Object)
let fullPath = getDocumentsDirectory().appendingPathComponent(filename)
do {
try data.write(to: fullPath)
print("Wrote file '"+filename+"' to storage at "+fullPath.absoluteString)
} catch {
print("Error: Couldn't write file '"+filename+"' to storage")
}
}
}
...and finally, this is what I do to call it all up:
let testobject:ItemList = ItemList.init(listname: "testlist", ContentItems: [0,0,1,2])
FileHandler.saveObjectAsFile(filename:"Test.plist",Object:testobject)
let tobi = FileHandler.getFileAsObject(filename:"Test.plist") as! ItemList
Alas, I get this as output:
Wrote file 'Test.plist' to storage at file:///…/data/Containers/Data/Application/6747B038-B0F7-4B77-85A8-9EA02BC574FE/Documents/Test.plist
Error: Couldn't find requested object 'Test.plist' in storage at file:///…/data/Containers/Data/Application/6747B038-B0F7-4B77-85A8-9EA02BC574FE/Documents/Test.plist
Note that this is my own output -- so I do (and have checked) that the file was created correctly. But it just won't load. Can anyone tell me what I am doing wrong?
The problem is with the path you pass to unarchiveObject(withFile:).
Change:
if let result = NSKeyedUnarchiver.unarchiveObject(withFile: path.absoluteString) {
to:
if let result = NSKeyedUnarchiver.unarchiveObject(withFile: path.path) {
On a side note, you should use symmetric APIs for your writing and reading logic. When you write the data you archive a root object to a Data object and then write the Data object to a file. But when reading, you directly unarchive the object tree given a file path.
Either change your writing code to use archiveRootObject(_:toFile:) or change the reading code to load the Data from a file and then unarchive the data. Your current code works (once you fix the path issue) but it's not consistent.

How to save data using NSCoding

I got Score.swift and ScoreManager.swift.
My Score.swift looks like this:
class Score: NSObject, NSCoding {
let score:Int;
let dateOfScore:NSDate;
init(score:Int, dateOfScore:NSDate) {
self.score = score;
self.dateOfScore = dateOfScore;
}
required init(coder: NSCoder) {
self.score = coder.decodeObjectForKey("score") as! Int;
self.dateOfScore = coder.decodeObjectForKey("dateOfScore") as! NSDate;
super.init()
}
func encodeWithCoder(coder: NSCoder) {
coder.encodeObject(self.score, forKey: "score")
coder.encodeObject(self.dateOfScore, forKey: "dateOfScore")
}
}
My ScoreManager.swift looks like this:
class ScoreManager {
var scores:Array<Score> = [];
init() {
// load existing high scores or set up an empty array
let documentsPath = NSSearchPathForDirectoriesInDomains(.DocumentDirectory, .UserDomainMask, true)[0] as NSString
let path = documentsPath.stringByAppendingPathComponent("Scores.plist")
let fileManager = NSFileManager.defaultManager()
// check if file exists
if !fileManager.fileExistsAtPath(path) {
// create an empty file if it doesn't exist
if let bundle = NSBundle.mainBundle().pathForResource("Scores", ofType: "plist") {
do {
try fileManager.copyItemAtPath(bundle, toPath: path)
} catch {
}
}
}
if let rawData = NSData(contentsOfFile: path) {
// do we get serialized data back from the attempted path?
// if so, unarchive it into an AnyObject, and then convert to an array of Scores, if possible
let scoreArray: AnyObject? = NSKeyedUnarchiver.unarchiveObjectWithData(rawData);
self.scores = scoreArray as? [Score] ?? [];
}
}
func save() {
// find the save directory our app has permission to use, and save the serialized version of self.scores - the Scores array.
let saveData = NSKeyedArchiver.archivedDataWithRootObject(self.scores);
let paths = NSSearchPathForDirectoriesInDomains(.DocumentDirectory, .UserDomainMask, true) as NSArray;
let documentsDirectory = paths.objectAtIndex(0) as! NSString;
let path = documentsDirectory.stringByAppendingPathComponent("Scores.plist");
saveData.writeToFile(path, atomically: true);
}
// a simple function to add a new high score, to be called from your game logic
// note that this doesn't sort or filter the scores in any way
func addNewScore(newScore:Int) {
let newScore = Score(score: newScore, dateOfScore: NSDate());
self.scores.append(newScore);
self.save();
}
}
My question is this:
How do I call these NSCoding stuff to save data from the actual gameView scene?
I strongly urged you to read a book or at least a tutorial on iOS programming. But here's the low down of it.
class ViewController: UIViewController {
let scoreManager = ScoreManager()
// Save the score to file. Hook it up to a button or label in your view
#IBAction func save (sender: AnyObject) {
scoreManager.save()
}
}
If you don't know how to connect a button/label to an #IBAction, please find a tutorial on Google.

Resources