5

I have an app where the user has the ability to choose if they would like to take a picture or select one from their library. The code I have gets the image URL for the Xcode device simulator but does not work for my physical iPhone. When I choose an image using my personal device My result prints

Failed: There is a client side validation error for the field [local] with message: The file located at the `local` URL is missing. Make sure the file exists before uploading to storage.

But when I run with the device simulator it runs perfect and the image is stored in my backend. I am unsure as to why this is happening.

I would like to be able to get the URL from my own device.

printed url from xcode device simulator

file:///Users/GBMR/Library/Developer/CoreSimulator/Devices/877A8184-D857-4211-94B1-00A6B724A956/data/Containers/Data/Application/094279FA-37B1-45E6-ABC4-ADAA08B5477B/PicturesB71286E4-1994-4D76-AC14-D40A062BC832.jpeg
Completed: ywassupo

printed url from my physical iPhone

file:///var/mobile/Containers/Data/Application/C0415B3C-F50E-4D20-8D8A-941D29B9C4D1/Pictures9BA481F9-C52A-40FD-8A06-AAA2D6CDA6D7.jpeg
Failed: There is a client side validation error for the field [local] with message: The file located at the `local` URL is missing. Make sure the file exists before uploading to storage.

Code:

  func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) {

        if let selectedImage = info[UIImagePickerController.InfoKey.editedImage] as? UIImage {
            imageView.image = selectedImage
            imageView.contentMode = .scaleAspectFit

            //            detect(image: ciimage)
            navigationItem.title = "Tap next to continue"
            nextButton.isEnabled = true

            if let imgUrl = info[UIImagePickerController.InfoKey.imageURL] as? URL{
                let imgName = imgUrl.lastPathComponent
                let documentDirectory = NSSearchPathForDirectoriesInDomains(.picturesDirectory, .allDomainsMask, true).first
                let localPath = documentDirectory?.appending(imgName)

                //let image = info[UIImagePickerController.InfoKey] as! UIImage
                let data = selectedImage.pngData()! as NSData
                data.write(toFile: localPath!, atomically: true)
                //let imageData = NSData(contentsOfFile: localPath!)!
                let photoURL = URL.init(fileURLWithPath: localPath!)
                let infoTVC = InfoTableViewController()
                infoTVC.imageChosenName = photoURL
                _ = Amplify.Storage.uploadFile(key: "ywassupo", local: photoURL ,resultListener: {(event) in
                    switch event{
                    case .success(let data):
                        print("Completed: \(data)")
                    case .failure(let storageError):
                        print("Failed: \(storageError.errorDescription). \(storageError.recoverySuggestion)")
                    }

                })
                //NSURL(fileURLWithPath: localPath!)
                print(photoURL)

                dismiss(animated:true, completion: nil)
            }
        }
    }

Can anyone steer me into the right direction as to how I can fix this?

GBMR
  • 594
  • 1
  • 7
  • 16
  • Has the app requested permission for access to the Photos library? – Chris Brandow Jun 12 '20 at 22:48
  • Yes I request permission for both in the infoPlist – GBMR Jun 12 '20 at 23:07
  • Print your url it is probably an asset url – Leo Dabus Jun 12 '20 at 23:08
  • If you take a picture with your camera it will probably work – Leo Dabus Jun 12 '20 at 23:09
  • This might help https://stackoverflow.com/a/43944769/2303865 – Leo Dabus Jun 12 '20 at 23:10
  • I need to update the syntax to Swift 5. But the compiler should fix them automatically for you – Leo Dabus Jun 12 '20 at 23:19
  • @LeoDabus Maybe something is wrong with my backend because I tried both solutions and they did not work. – GBMR Jun 13 '20 at 00:56
  • Did you print the URL? I will update my post. The syntax have changed a lot – Leo Dabus Jun 13 '20 at 01:01
  • @LeoDabus I will update my post with print output from physical device and simulator as well – GBMR Jun 13 '20 at 01:04
  • @GBMR don't use PNG. It will loose its orientation and the data will be too big. Better to use JPEG – Leo Dabus Jun 13 '20 at 01:07
  • @GBMR don't use pictures directory use the document directory – Leo Dabus Jun 13 '20 at 01:09
  • don't use `NSData` use only Swift native types `Data` – Leo Dabus Jun 13 '20 at 01:09
  • There is something wrong with your path `Pictures9BA481F9-C52A-40FD-8A06-AAA2D6CDA6D7.jpeg` as you can see it is all together. – Leo Dabus Jun 13 '20 at 01:11
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/215855/discussion-between-gbmr-and-leo-dabus). – GBMR Jun 13 '20 at 01:14
  • @GBMR Just some notes. The imageURL points to a temporary folder. If you don't upload it to your backend or copy/move it to another folder it will be deleted as soon as that method finishes. Again you should use Documents directory (not pictures) – Leo Dabus Jun 13 '20 at 02:09
  • Don't use NSSearchPathForDirectoriesInDomains. Use FileManager.default.urls(for:) method to get the document directory and get used to work with URLs instead of paths – Leo Dabus Jun 13 '20 at 02:20
  • Your main issue there is the malformed ur (it is missing a slash "/" between the directory name and the filename and there is no Pictures directory in iOS). In the simulator it saves it properly but in iOS it points to a location which is write protected. If you create your destination url properly pointing to the documents directory it would work. – Leo Dabus Jun 13 '20 at 02:54
  • Another thing that you should know is that You can NOT save the whole url. The path changes on every run of your App. So you need to save only the file name and construct the URL every time you run the app – Leo Dabus Jun 13 '20 at 02:59

2 Answers2

4

As UIImagePickerController will not give you path for selected image, all you have to do is simply grab the image info[UIImagePickerControllerOriginalImage] as UIImage and save to the locally in your application's storage area by giving directory path.

func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) {
    guard let image = info[.editedImage] as? UIImage else { return }

    let imageName = UUID().uuidString
    let imagePath = getDocumentsDirectory().appendingPathComponent(imageName)

    if let jpegData = image.jpegData(compressionQuality: 0.8) {
        try? jpegData.write(to: imagePath)
    }

    dismiss(animated: true)
}

func getDocumentsDirectory() -> URL {
    let paths = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)
    return paths[0]
}

If you need to save only one image it works great. In case of multiple image you need to write some logic to create unique image name and save it to the same directory.

Don't forget to add permission following keys in info.plist

Camera :

Key       :  Privacy - Camera Usage Description   
Value     :  <your project name> will use camera to capture photo.

Photo :

Key       :  Privacy - Photo Library Usage Description    
Value     :  <your project name> will use photo library to select photo.
bestiosdeveloper
  • 2,339
  • 1
  • 11
  • 28
1

You are not building your path correctly. There are a few issues:

  1. You are just appending the file name to the pictures path string. You’re missing a slash between the folder and the file name. Obviously, you can add this yourself, but for future reference, if you work with URLs, it would have added the necessary slash for you.

  2. This “pictures” directory is not for iOS. I’d suggest a different folder. Perhaps copy this to a folder in the temporary directory, upload from there, and then clean up when you’re done.

  3. If you’re wondering why your code worked on the simulator, but not the device, it’s because the former doesn’t enforce sandbox rules, whereas the latter will.


As an aside, I’d suggest avoiding “round tripping” through a UIImage unless necessary. Above, we’re dealing with the raw asset, but if you create a UIImage, only to then extract the JPG or PNG representation, you’ll lose information about your asset, and likely make the asset bigger than it needs to be. It’s far more efficient to just grab the image URL, copy that file, and upload:

func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) {
    guard let imageUrl = info[.imageURL] as? URL else {
        return
    }

    do {
        let tempDirectory = URL(fileURLWithPath: NSTemporaryDirectory())
            .appendingPathComponent("uploads")

        try FileManager.default.createDirectory(at: tempDirectory, withIntermediateDirectories: true, attributes: nil)

        let fileURL = tempDirectory
            .appendingPathComponent(UUID().uuidString)
            .appendingPathComponent(imageUrl.pathExtension)

        try FileManager.default.copyItem(at: imageUrl, to: fileURL)

        // when done, clean up (presumably in the completion handler of the async upload routine)

        try FileManager.default.removeItem(at: fileURL)
    } catch {
        print(error)
    }

    dismiss(animated: true)
}
Rob
  • 415,655
  • 72
  • 787
  • 1,044