GO FURTHER, FASTER: Try the Swift Career Accelerator today! >>

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      

Go further, faster with the Swift Career Accelerator.

GO FURTHER, FASTER Unleash your full potential as a Swift developer with the all-new Swift Career Accelerator: the most comprehensive, career-transforming learning resource ever created for iOS development. Whether you’re just starting out, looking to land your first job, or aiming to become a lead developer, this program offers everything you need to level up – from mastering Swift’s latest features to conquering interview questions and building robust portfolios.

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.