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

SOLVED: Child view seems to update before parent view. Can anybody explain why?

Forums > SwiftUI

I have created a simple project to show a problem that I am running into in a larger project that I am working on.

If you run this code, tap the button to load the image, and then tap the button to remove the image, you will get an error saying "Fatal error: Unexpectedly found nil while unwrapping an Optional value"

import SwiftUI

struct ContentView: View {
    @State private var inputImage: UIImage? = nil

    var body: some View {
        if inputImage == nil {
            ImageInputView(inputImage: $inputImage)
        } else {
            DisplayImageView(inputImage: $inputImage)
        }
    }
}

struct ImageInputView: View {
    @Binding var inputImage: UIImage?

    var body: some View {
        Button("Tap to Input Image") {
            inputImage = UIImage(systemName: "star")
        }
    }
}

struct DisplayImageView: View {
    @Binding var inputImage: UIImage?

    var body: some View {
        VStack {
            Image(uiImage: inputImage!)

            Button("Tap to Remove Image") {
                inputImage = nil
            }
        }
    }
}

It seems to happen because DisplayImageView reloads with inputImage set to nil before ContentView can realize that inputImage has changed it's value, and that it should be displaying ImageInputView instead.

However, if you run this code, it works just fine...

import SwiftUI

struct ContentView: View {
    @State private var inputImage: UIImage? = nil

    var body: some View {
        if inputImage == nil {
            Button("Tap to Input Image") {
                inputImage = UIImage(systemName: "star")
            }
        } else {
            VStack {
                Image(uiImage: inputImage!)

                Button("Tap to Remove Image") {
                    inputImage = nil
                }
            }
        }
    }
}

The only difference being that we are no longer separating the if/else closures into their own separate view structs and passing them a binding to the state variable.

Can anybody explain to me why this happens?

3      

So I slightly altered your code so I could better see what was going on:

import SwiftUI

struct ContentView: View {
    @State private var inputImage: UIImage? = nil

    var body: some View {
        print("- ContentView rendered")
        return Group {
            if inputImage == nil {
                ImageInputView(inputImage: $inputImage)
            } else {
                DisplayImageView(inputImage: $inputImage)
            }
        }
        .onAppear {
            print("- ContentView appeared")
        }
        .onDisappear {
            print("- ContentView disappeared")
        }
    }
}

struct ImageInputView: View {
    @Binding var inputImage: UIImage?

    var body: some View {
        print("- ImageInputView rendered")
        return Button("Tap to Input Image") {
            print("- ImageInputView: Button pushed")
            inputImage = UIImage(systemName: "star")
        }
        .onAppear {
            print("- ImageInputView appeared")
        }
        .onDisappear {
            print("- ImageInputView disappeared")
        }
    }
}

struct DisplayImageView: View {
    @Binding var inputImage: UIImage?

    var body: some View {
        print("- DisplayImageView rendered")
        return VStack {
            if inputImage == nil {  //just so it won't crash while I'm logging events
                //note that this image won't actually display once inputImage == nil
                Image(systemName: "person.fill")
            } else {
                Image(uiImage: inputImage!)
            }

            Button("Tap to Remove Image") {
                print("- DisplayImageView: Button pushed")
                inputImage = nil
            }
            .onAppear {
                print("- DisplayImageView appeared")
            }
            .onDisappear {
                print("- DisplayImageView disappeared")
            }
        }
    }
}

Running this, I get the following output in the console (I grouped them for better readability):

- ContentView rendered
- ImageInputView rendered
- ContentView appeared
- ImageInputView appeared

- ImageInputView: Button pushed
- ContentView rendered
- DisplayImageView rendered
- ImageInputView rendered
- DisplayImageView appeared
- ImageInputView disappeared

- DisplayImageView: Button pushed
- ContentView rendered
- DisplayImageView rendered
- ImageInputView rendered
- ImageInputView appeared
- DisplayImageView disappeared

You can see that SwiftUI renders all the currently "alive" Views that have a dependency on inputImage before it swaps in ImageInputView for DisplayImageView after the remove button has been pushed. So since DisplayImageView is currently "alive" it gets updated with the new value of inputImage, which since that's nil causes the crash when it hits the force unwrap.

It's kind of late and it's been nearly two months since I last watched it, so it will have to wait until tomorrow for a rewatch, but I'm betting the WWDC21 session "Demystify SwiftUI" will be informative here. That session has a lot to say about View lifecycles and dependencies and such.

4      

Thanks. That did help me to understand a little better.

3      

Thanks for sharing that technique of debugging views; I hadn't thought of using .onappear and .ondisappear

4      

Hacking with Swift is sponsored by RevenueCat

SPONSORED Take the pain out of configuring and testing your paywalls. RevenueCat's Paywalls allow you to remotely configure your entire paywall view without any code changes or app updates.

Learn more here

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.