UPGRADE YOUR SKILLS: Learn advanced Swift and SwiftUI on Hacking with Swift+! >>

Day 77 image from image picker is nil

Forums > 100 Days of SwiftUI

@Wryte  

Hello all,

I am trying to solve the day 77 challenge of making the person tagging app. I reused the image picker from instafilter but for some reason when I try to display the image after it being picked it is still nil.

Am I missing something obvious here?

After running this code "setting parent image" is printed in the console and "oops" is displayed on the screen

ContentView.swift

import SwiftUI

struct ContentView: View {
    @State private var list = [ImageData]()
    @State private var inputImage: UIImage?
    @State private var isPickerShowing = false
    @State private var isNamingImage = false

    var body: some View {
        NavigationView {
            List(list) { imageData in
                Text(imageData.label)
            }
            .navigationBarTitle(Text("Whojavu"))
            .navigationBarItems(trailing: Button(action: {
                isPickerShowing = true
            }) {
                Image(systemName: "plus")
            })
        }
        .sheet(isPresented: $isPickerShowing, onDismiss: {
            isNamingImage = true
        }) {
            ImagePicker(image: self.$inputImage)
        }
        .sheet(isPresented: $isNamingImage) {
            if inputImage != nil {
                Image(uiImage: inputImage!)
            } else {
                Text("oops")
            }

        }
    }
}

ImagePicker.swift

import SwiftUI

struct ImagePicker: UIViewControllerRepresentable {
    @Binding var image: UIImage?
    @Environment(\.presentationMode) var presentationMode

    func makeCoordinator() -> Coordinator {
        Coordinator(self)
    }

    func makeUIViewController(context: Context) -> UIImagePickerController {
        let picker = UIImagePickerController()

        picker.delegate = context.coordinator

        return picker
    }

    func updateUIViewController(_ uiViewController: UIImagePickerController, context: Context) {

    }

    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 uiImage = info[.originalImage] as? UIImage {
                print("setting parent image")
                parent.image = uiImage
            }

            parent.presentationMode.wrappedValue.dismiss()
        }
    }
}

If you need any more info please let me know. I'm very interested to know what's going on here. Thanks!

2      

Your code works just fine for me in Xcode 12.5.1

2      

Maybe you didn't give permission for the app to access the photo album on your device/simulator?

2      

@Wryte  

Hmm I think you're right. I don't see the app in the simulators device settings app.

When should I be prompted to allow access? I can see the picker show up and I added a message for "Privacy - Photo Library Usage Description" in my plist.

2      

If you completely delete the app from the simulator (Hold down on its icon and delete app just like you would on a real device) and then try to run it in the simulator again does it ask you to give it permission?

2      

@Wryte  

If you completely delete the app from the simulator (Hold down on its icon and delete app just like you would on a real device) and then try to run it in the simulator again does it ask you to give it permission?

I did try this but it didn't seem to do anything.

What is interesting is that if I try to render the Image in the body instead of a sheet it does render properly. Does that make sense? Is there a race condition here that I'm not aware of with having that second sheet immediately appear after the first one?

2      

That may have something to do with it. I haven't completed the 100 days of SwiftUI course myself yet, so I'm not familar enough with the inner workings of the language to know. But I recently completed the day 77 milestone project, and it lead me to a similar question.

At first, I had a bunch of cluttery looking code in an if/else statement in ContentView, so I tried to separate the if and else parts into separate views to make things look more clean. I broke it up into NamedFacesListView and AddNamedFaceView so my ContentView ended up looking like this:

import SwiftUI

struct ContentView: View {
    @State private var inputImage: UIImage? = nil
    @ObservedObject var namedFaces = NamedFaces()

    var body: some View {
        if inputImage == nil {
            NamedFacesListView(namedFaces: namedFaces, inputImage: $inputImage)
        } else {
            AddNamedFaceView(namedFaces: namedFaces, inputImage: $inputImage)
        }
    }
}

Basically, how it worked was the NamedFacesListView shows a list of all NamedFaces in the namedFaces array that have already been added, and has a + button in the top right corner that opens up an ImagePicker in a sheet. Once an image is selected, inputImage is no longer nil so it would move to the AddNamedFaceView which would allow a person to enter a name to be saved along with the picture and push a save button, which would save the NamedFace into the namedFaces array and then set inputImage back to nil.

When I had all of my code in ContentView everything worked just fine. But I ran into a small problem when I moved the code into separate views as shown above.

The problem was that in AddNamedFaceView I was implicitly unwrapping the selected image (inputImage!) because it was impossible to end up in the else part of my code unless an image had already been selected. But with the code in separate views, after I tapped the "Save" button to save a name with a picture it was giving me an error saying that nil was found unexpectedly while unwrapping the optional image.

This was confusing for me because I had thought that as soon as inputImage was set back to nil ContentView would recognize the change, and immediately move back to NamedFacesListView but apparently it doesn't happen quite that fast. The code seems to linger in AddNamedFaceView for a moment before ContentView recognizes the change.

I was able to solve my problem by explicitly unwrapping inputImage in AddNamedFaceView and only trying to show an image if inputImage != nil but there seemed to be some kind of race condition there. When I run the app, the switch back to NamedFacesListView happens so fast after clicking the Save button that you would think it was instantaneous. But apparently it isn't actually instantaneous, or else the implicit unwrapping wouldn't have been necessary.

3      

If you want to see my full code for this project to get a better idea of what I'm talking about, it is available at this link.

But, I guess my point is that it seems like the binding to inputImage in my child view was getting updated before the actual inputImage variable in ContentView was getting updated, which caused my child view to reload with inputImage set to nil before ContentView could realize that it should actually be displaying the other child view at that point instead.

So, I'm not sure if there is a similar issue being caused in your project or not, but it may be possible.

2      

Hacking with Swift is sponsored by Essential Developer

SPONSORED Join a FREE crash course for mid/senior iOS devs who want to achieve an expert level of technical and practical skills – it’s the fast track to being a complete senior developer! Hurry up because it'll be available only until April 28th.

Click to save your free spot now

Sponsor Hacking with Swift and reach the world's largest Swift community!

Archived topic

This topic has been closed due to inactivity, so you can't reply. Please create a new topic if you need to.

All interactions here are governed by our code of conduct.

 
Unknown user

You are not logged in

Log in or create account
 

Link copied to your pasteboard.