I saved a UIImage to UserDefaults via .data with this code, where the key equals "petPhoto1":
#IBAction func addPhotoButton(_ sender: Any) {
let picker = UIImagePickerController()
picker.allowsEditing = false
picker.delegate = self
picker.mediaTypes = ["public.image"]
present(picker, animated: true)
}
func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey: Any]) {
if let image = info[UIImagePickerController.InfoKey.originalImage] as? UIImage {
image.storeInUserDefaults(for: "petPhoto\(activePet)")
UserDefaults.standard.set("yes", forKey: "doesImageExist\(activePet)")
}
dismiss(animated: true, completion: nil)
}
(unrelated stuff in between)
extension UIImage {
func storeInUserDefaults(with compressionQuality: CGFloat = 0.8, for key: String) {
guard let data = self.jpegData(compressionQuality: compressionQuality) else { return }
let encodedImage = try! PropertyListEncoder().encode(data)
UserDefaults.standard.set(encodedImage, forKey: key)
}
}
Now when I erase it like this:
UserDefaults.standard.set(nil, forKey: "petPhoto1")
I can still see that "Documents & Data" for my app under Settings is still full with the same size as the original image, indicating that it didn't actually delete it, even though it no longer displays when it gets loaded back from UserDefaults.
Can anyone figure out a way to fix this? Thanks!
By the way, in case it helps, here is other code related to this issue:
The code I use in the ImageViewController that I display the image after saving it:
#IBOutlet weak var imageView: UIImageView!
override func viewDidLoad() {
super.viewDidLoad()
activePet = UserDefaults.standard.string(forKey: "activePet")! // activePet = 1 (confirmed with debugging with other, unrelated code
imageView.image = try? UIImage.loadFromUserDefaults(with: "petPhoto\(activePet)")
}
extension UIImage {
static func loadFromUserDefaults(with key: String) throws -> UIImage? {
let activePet = UserDefaults.standard.string(forKey: "activePet")!
guard let data = UserDefaults.standard.data(forKey: "petPhoto\(activePet)") else {
return nil
}
do {
let decodedImageData = try PropertyListDecoder().decode(Data.self, from: data)
return UIImage(data: decodedImageData)
} catch let error {
throw error
}
}
}
When you do this:
UserDefaults.standard.set(nil, forKey: "petPhoto1")
The link between the key and the file saved will be removed synchronously. that means if you try to access the value for this key, it gives nil.
But this image needs to be cleared from storage too, that will be happening asynchronously [we don't have completion handler API support from apple to get this information].
Apple Documentation for reference:
At runtime, you use UserDefaults objects to read the defaults that
your app uses from a user’s defaults database. UserDefaults caches the
information to avoid having to open the user’s defaults database each
time you need a default value. When you set a default value, it’s
changed synchronously within your process, and asynchronously to
persistent storage and other processes.
What you can try:
First approch
Give some time for the file delete operation to get completed by OS. then try to access the image in the disk.
Second approch
Try observing to the changes in the directory using GCD. Refer: https://stackoverflow.com/a/26878163/5215474
Related
I'm newbie in iOS development, so some things which I will show and ask here can be stupid and please don't be angry :) So, I need to add support of picking files from local storage in my app. This feature will be used for picking file -> encoding to Base64 and then sending to remote server. Right now I have some problems with adding this functionality to my app. I had found this tutorial and did everything what was mentioned here:
added import - import MobileCoreServices
added implementation - UIDocumentPickerDelegate
added this code scope for showing picker:
let documentPicker = UIDocumentPickerViewController(documentTypes: [String(kUTTypeText),String(kUTTypeContent),String(kUTTypeItem),String(kUTTypeData)], in: .import)
documentPicker.delegate = self
self.present(documentPicker, animated: true)
and also added handler of selected file:
func documentPicker(_ controller: UIDocumentPickerViewController, didPickDocumentsAt urls: [URL]) {
print(urls)
}
In general file chooser appears on simulator screen, but I see warning in XCode:
'init(documentTypes:in:)' was deprecated in iOS 14.0
I visited the official guideline and here also found similar info about deprecation some method. So, how I can solve my problem with file choosing by the way which will be fully compatible with the latest iOS version. And another question - how I can then encode selected file? Right now I have an ability of file choosing and printing its location, but I need to get its data like name, content for encoding and some others. Maybe someone faced with similar problems and knows a solution? I need to add it in ordinary viewcontroller, so when I tried to add this implementation:
UIDocumentPickerViewController
I saw such error message:
Multiple inheritance from classes 'UIViewController' and 'UIDocumentPickerViewController'
I will be so pleased for any info: tutorials or advice :)
I decided to post my own solution of my problem. As I am new in ios development my answer can contain some logical problems :) Firstly I added some dialogue for choosing file type after pressing Attach button:
#IBAction func attachFile(_ sender: UIBarButtonItem) {
let attachSheet = UIAlertController(title: nil, message: "File attaching", preferredStyle: .actionSheet)
attachSheet.addAction(UIAlertAction(title: "File", style: .default,handler: { (action) in
let supportedTypes: [UTType] = [UTType.png,UTType.jpeg]
let documentPicker = UIDocumentPickerViewController(forOpeningContentTypes: supportedTypes)
documentPicker.delegate = self
documentPicker.allowsMultipleSelection = false
documentPicker.shouldShowFileExtensions = true
self.present(documentPicker, animated: true, completion: nil)
}))
attachSheet.addAction(UIAlertAction(title: "Photo/Video", style: .default,handler: { (action) in
self.chooseImage()
}))
attachSheet.addAction(UIAlertAction(title: "Cancel", style: .cancel))
self.present(attachSheet, animated: true, completion: nil)
}
then when a user will choose File he will be moved to ordinary directory where I handle his selection:
func documentPicker(_ controller: UIDocumentPickerViewController, didPickDocumentsAt urls: [URL]) {
var selectedFileData = [String:String]()
let file = urls[0]
do{
let fileData = try Data.init(contentsOf: file.absoluteURL)
selectedFileData["filename"] = file.lastPathComponent
selectedFileData["data"] = fileData.base64EncodedString(options: .lineLength64Characters)
}catch{
print("contents could not be loaded")
}
}
as you can see in scope above I formed special dicionary for storing data before sending it to a server. Here you can also see encoding to Base64.
When the user will press Photo/Video item in alert dialogue he will be moved to gallery for picture selecting:
func chooseImage() {
imagePicker.allowsEditing = false
imagePicker.sourceType = .photoLibrary
present(imagePicker, animated: true, completion: nil)
}
func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) {
var selectedImageData = [String:String]()
guard let fileUrl = info[UIImagePickerController.InfoKey.imageURL] as? URL else { return }
print(fileUrl.lastPathComponent)
if let pickedImage = info[UIImagePickerController.InfoKey.originalImage] as? UIImage {
selectedImageData["filename"] = fileUrl.lastPathComponent
selectedImageData["data"] = pickedImage.pngData()?.base64EncodedString(options: .lineLength64Characters)
}
dismiss(animated: true, completion: nil)
}
func imagePickerControllerDidCancel(_ picker: UIImagePickerController) {
dismiss(animated: true, completion: nil)
}
via my method all file content will be encoded to base64 string.
P.S. Also I'm so pleased to #MaticOblak because he showed me the initial point for my research and final solution. His solution also good, but I have managed to solve my problem in way which is more convenient for my project :)
As soon as you have file URL you can use that URL to retrieve the data it contains. When you have the data you can convert it to Base64 and send it to server. You gave no information about how you will send it to server but the rest may look something like this:
func sendFileWithURL(_ url: URL, completion: #escaping ((_ error: Error?) -> Void)) {
func finish(_ error: Error?) {
DispatchQueue.main.async {
completion(error)
}
}
DispatchQueue(label: "DownloadingFileData." + UUID().uuidString).async {
do {
let data: Data = try Data(contentsOf: url)
let base64String = data.base64EncodedString()
// TODO: send string to server and call the completion
finish(nil)
} catch {
finish(error)
}
}
}
and you would use it as
func documentPicker(_ controller: UIDocumentPickerViewController, didPickDocumentsAt urls: [URL]) {
urls.forEach { sendFileWithURL($0) { <#Your code here#> } }
}
To break it down:
To get file data you can use Data(contentsOf: url). This method even works on remote files so you could for instance use an URL of an image link anywhere on internet you have access to. It is important to know that this method will pause your thread which is usually not what you want.
To avoid breaking the current thread we create a new queue using DispatchQueue(label: "DownloadingFileData." + UUID().uuidString). The name of the queue is not very important but can be useful when debugging.
When data is received we convert it to Base64 string using data.base64EncodedString() and this data can then be sent to server. You just need to fill in the TODO: part.
Retrieving your file data can have some errors. Maybe access restriction or file no longer there or no internet connection... This is handled by throwing. If the statement with try fails for any reason then the catch parts executes and you receive an error.
Since all of this is done on background thread it usually makes sense to go back to main thread. This is what the finish function does. If you do not require that you can simply remove it and have:
func sendFileWithURL(_ url: URL, completion: #escaping ((_ error: Error?) -> Void)) {
DispatchQueue(label: "DownloadingFileData." + UUID().uuidString).async {
do {
let data: Data = try Data(contentsOf: url)
let base64String = data.base64EncodedString()
// TODO: send string to server and call the completion
completion(nil)
} catch {
completion(error)
}
}
}
There are other things to consider in this approach. For instance you can see if user selects multiple files then each of them will open its own queue and start the process. That means that if user selects multiple files it is possible that at some point many or all of them will be loaded in memory. That may take too much memory and crash your application. It is for you to decide if this approach is fine for you or you wish to serialize the process. The serialization should be very simple with queues. All you need is to have a single one:
private lazy var fileProcessingQueue: DispatchQueue = DispatchQueue(label: "DownloadingFileData.main")
func sendFileWithURL(_ url: URL, completion: #escaping ((_ error: Error?) -> Void)) {
func finish(_ error: Error?) {
DispatchQueue.main.async {
completion(error)
}
}
fileProcessingQueue.async {
do {
let data: Data = try Data(contentsOf: url)
let base64String = data.base64EncodedString()
// TODO: send string to server and call the completion
finish(nil)
} catch {
finish(error)
}
}
}
Now one operation will finish before another one starts. But that may only apply for getting file data and conversion to base64 string. If uploading is then done on another thread (Which usually is) then you may still have multiple ongoing requests which may contain all of the data needed to upload.
I am building a simple app where I get an image from UIImagePickerController (from Camera or Photo Library, depends on user selection) and send it to a server.
The latter seems to be saving images on the App internal folder, because when I take any photo, in the Disk Debug Menu in Xcode, I can see every time 1 - 1,5 MB writing on Disk. The same happens for images from Photo Library, they too occupy about 1 MB.
This is the code, very basic:
(VisualRecognition is the IBM API to use one of their AI services)
let imagePicker = UIImagePickerController()
var imageShot = UIImage() //To send later the image to the server
func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [String : Any]) {
let userPickedImage = info[UIImagePickerControllerOriginalImage] as! UIImage
imageShot = userPickedImage
picker.dismiss(animated: true) {
self.elaborateImage(self.imageShot). //The sending function
}
}
func elaborateImage(_ image: UIImage) {
animateIn()
let group = DispatchGroup()
group.enter()
DispatchQueue.main.async {
let visualRecognition = VisualRecognition(version: self.version, apiKey: self.apiKey)
visualRecognition.classify(image: image, acceptLanguage: self.language, failure: { (error) in
print("There was an error")
}, success: { (classifiedImage) in
let classes = classifiedImage.images.first!.classifiers.first!.classes
var classificationResults = [ScanResult]()
for index in 0..<classes.count {
let scan = ScanResult.init(score: Int(classes[index].score! * 100), name: classes[index].className)
classificationResults.append(scan)
}
self.sortedClassificationResults = classificationResults.sorted(by: {$0.score > $1.score})
group.leave()
})
}
group.notify(queue: .main) {
self.performSegue(withIdentifier: "goToResults", sender: self)
}
}
After sending the images to the server I don't need anymore them, but they continue to grow the app size.
How Can I stop UIImagePickerController from saving images inside the App Bundle?
Thanks
I am new to Swift/iOS development, so this might be a stupid question, but I can't seem to find how to do this correctly.
I am following a youtube guide on how to programmatically code (no storyboard) a login screen in Swift and register a user into a Firebase database. The basic outline of the design are as follows
Once the user clicks the icon of the cat with the crown, the image picker comes up and they can select a profile picture and then register like normal. If the user does not select an image from the picker, the cat with the crown icon gets loaded as the profile picture by default.
What I have been trying to do is make it so a different picture (not the cat, and not shown to the user on the login page) named "nedstark" is set as the default profile picture that is stored in the database.
The program uses LoginController.swift and LoginController + handlers.swift files to make this happen. I tried to add a conditional to the picker where if nothing is selected, the default profile pic is set to a specific default user pic (different than the cat), but it doesn't work.
This is the code that stores the photo and other User info into the Database
let imageName = NSUUID().uuidString
let storageRef = Storage.storage().reference().child("profile_images").child("\(imageName).jpg")
if let profileImage = self.profileImageView.image, let uploadData = UIImageJPEGRepresentation(profileImage, 0.1) {
storageRef.putData(uploadData, metadata: nil, completion: { (metadata, error) in
if let error = error {
print(error)
return
}
if let profileImageUrl = metadata?.downloadURL()?.absoluteString {
let values = ["name": name, "email": email, "profileImageUrl": profileImageUrl]
self.registerUserIntoDatabaseWithUID(uid, values: values as [String : AnyObject])
}
})
}
This is the code where I am trying to set the default profile pic
func handleSelectProfileImageView() {
let picker = UIImagePickerController()
picker.delegate = self
picker.allowsEditing = true
present(picker, animated: true, completion: nil)
}
func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [String : Any]) {
var selectedImageFromPicker: UIImage?
if let editedImage = info["UIImagePickerControllerEditedImage"] as? UIImage {
selectedImageFromPicker = editedImage
} else if let originalImage = info["UIImagePickerControllerOriginalImage"] as? UIImage {
selectedImageFromPicker = originalImage
}
else { //Testing if default profile pic set
selectedImageFromPicker = UIImage(named: "nedstark")
}
if let selectedImage = selectedImageFromPicker {
profileImageView.image = selectedImage
}
else { //Testing if default profile pic set
profileImageView.image = UIImage(named: "nedstark")
}
dismiss(animated: true, completion: nil)
}
I
I know that this is a total noob question, but any help, tips, or pointers is greatly appreciated. Thanks!
You need to handle delegate method of cancel event. UIImagepickercontrolledelegate has another method
imagePickerControllerDidCancel()
func imagePickerControllerDidCancel(_ picker: UIImagePickerController) {
imageView.image = UIImage(named: "nedstark")
dismiss(animated: true, completion: nil)
}
When you click on cancel button in pickerviewcontroller then didfinishPickingMedia function won't call. That's way your default image is not set when you tap on cancel button.
Besides browsing the Photolibrary I've enabled the Camera to add a photo to my
#IBAction func startCameraButtonTapped(sender: AnyObject) {
let startCameraController = UIImagePickerController()
startCameraController.delegate = self
startCameraController.sourceType = UIImagePickerControllerSourceType.Camera
startCameraController.allowsEditing = true
self.presentViewController(startCameraController, animated: true, completion: nil)
//invoiceImageHolder.image!.scaleAndRotateImage2(280)
//self.invoiceImageHolder.transform = CGAffineTransformMakeRotation((180.0 * CGFloat(M_PI)) / 180.0)
}
Every user input can be deleted, but now I'd like to know where the corresponding photo is. When I browse the photo library I can't see the photos taken with my app? Where are those photos stored?
[edit]
This is my saveImage process
func imagePickerController(picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [String : AnyObject]) {
let selectedImage = info[UIImagePickerControllerOriginalImage] as! UIImage
saveImage(selectedImage, path: fileInDocumentsDirectory("\(uniqueImageName)"))
// set the unique image reference in textfield
// Set photoImageView to display the selected image
self.invoiceImageHolder.image = selectedImage
// Dismiss the picker
dismissViewControllerAnimated(true, completion: nil)
}
func saveImage(image: UIImage, path: String) -> Bool{
let pngImageData = UIImagePNGRepresentation(image)
// let jpgImageData = UIImageJPEGRepresentation(image, 1.0)
let result = pngImageData?.writeToFile(path, atomically: true)
//print("Save result \(result)")
return result!
}
EDIT hmm I think I see where the photos are stored, but now how do I browse to them for deletions?
I think it's here:
/var/mobile/Containers/Data/Application/CBFE9E9D-A657-4011-8B9F-EF0C9BB2C603/Documents/
BUT everytime i restart the app the part between Application and /Documents is different??
Still not a working solution, but for those newbies as me this is a valuable readup: http://www.techotopia.com/index.php/Working_with_Directories_in_Swift_on_iOS_8
[EDIT 2]
with this code i am able to see all whiles stored with my app. I was a bit surprise to see all the files
// Files stored in my app filemanager
let filemgr = NSFileManager.defaultManager()
do {
print("Start filemanager")
let filelist = try filemgr.contentsOfDirectoryAtPath(documentsDirectory())
for filename in filelist {
print(filename)
}
} catch {
print("No directory path \(error)")
}
// end filemanager
UIImagePickerController() doesn't automatically save your photos into the library. See its UIImagePickerControllerDelegate protocol and you'll see that it has a method returning your UIImage after making a photo. Then you can save it:
func imagePickerController(picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [String : AnyObject]) {
if let image = info[UIImagePickerControllerOriginalImage] as? UIImage {
UIImageWriteToSavedPhotosAlbum(image, nil, nil, nil)
}
}
you save them - not in the photo library - but as 'simply' files inside your app's documents folder. There is no standard UI for deletion then.
You need to write your own ViewController to show & handle deletions then.
I'm trying to understand CloudKit, but I'm having an issue figuring out a real-world issue if I eventually turned it into an app.
I've got a tableviewcontroller that's populated by CloudKit, which works fine. I've got a detailviewcontroller that is pushed when you tap one of the entries in the tableview controller. The detailviewcontroller pulls additional data from CloudKit that wasn't in the initial tableviewcontroller. All of this works perfectly fine.
Now, the detailviewcontroller allows changing of an image and saving it back to a public database. That's working fine as well.
The scenario I'm dealing with is thinking about this core function being used in an app by multiple people. If one person uploads a photo (overwriting an existing photo for one of the records), what happens if another user currently has the app in the background? If they open the app, they'll see the last photo they saw on that page, and not the new photo. So I want to reload the data from CloudKit when the app comes back to the foreground, but I must be doing something wrong, because it's not working. Using the simulator and my actual phone, I can change a photo on one device and the other device (simulator or phone) doesn't update the data when it comes into the foreground.
Here's the code I'm using: First in ViewController.swift:
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
println("viewDidLoad")
NSNotificationCenter.defaultCenter().addObserver(self, selector: "activeAgain", name: UIApplicationWillEnterForegroundNotification, object: nil)
and then the activeAgain function:
func activeAgain() {
println("Active again")
fetchItems()
self.tblItems.reloadData()
println("Table reloaded")
}
and then the fetchItems function:
func fetchItems() {
let container = CKContainer.defaultContainer()
let publicDatabase = container.publicCloudDatabase
let predicate = NSPredicate(value: true)
let query = CKQuery(recordType: "Items", predicate: predicate)
publicDatabase.performQuery(query, inZoneWithID: nil) { (results, error) -> Void in
if error != nil {
println(error)
}
else {
println(results)
for result in results {
self.items.append(result as! CKRecord)
}
NSOperationQueue.mainQueue().addOperationWithBlock({ () -> Void in
self.tblItems.reloadData()
self.tblItems.hidden = false
})
}
}
}
I tried adding a reload function to viewWillAppear, but that didn't really help:
override func viewWillAppear(animated: Bool) {
tblItems.reloadData()
}
Now here's the code in DetailViewController.swift:
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
NSNotificationCenter.defaultCenter().addObserver(self, selector: "activeAgain", name: UIApplicationWillEnterForegroundNotification, object: nil)
self.imageScrollView.contentSize = CGSizeMake(1000, 1000)
self.imageScrollView.frame = CGRectMake(0, 0, 1000, 1000)
self.imageScrollView.minimumZoomScale = 1.0
self.pinBoardScrollView.maximumZoomScale = 8.0
showImage()
and then the showImage function:
func showImage() {
imageView.hidden = true
btnRemoveImage.hidden = true
viewWait.hidden = true
if let editedImage = editedImageRecord {
if let imageAsset: CKAsset = editedImage.valueForKey("image") as? CKAsset {
imageView.image = UIImage(contentsOfFile: imageAsset.fileURL.path!)
imageURL = imageAsset.fileURL
self.title = "Image"
imageView.hidden = false
btnRemoveImage.hidden = false
btnSelectPhoto.hidden = true
}
}
}
and the activeAgain function:
func activeAgain() {
showImage()
println("Active again")
}
Neither of these ViewControllers reload the data from CloudKit when they return. It's like it's cached the table data for the first ViewController and the image for the DetailViewController and it's using that instead of downloading the actual CloudKit data. I'm stumped, so I figure I better try this forum (my first post!). Hopefully I included enough required info to be useful.
Your showImage function is using a editedImage CKRecord. Did you also reload that record? Otherwise the Image field in that record would then still contain the old CKAsset.
Besides just reloading on a moment you feel right, you could also let the changes be pushed by creating a CloudKit subscription. Then your data will even change when it's active and changed by someone else.