SwiftUI: Understanding .sheet / .fullScreenCover lifecycle when using constant vs @Binding initializers

Viewed 669

I'm trying to understand how and when the .sheet and .fullScreenCover initializers are called. Below is a minimal reproducible example, where the first screen has 3 colored rectangles and the SecondView (shown via .fullScreenCover) has 1 rectangle that changes color based on the selected color from the first screen.

  • When the app first loads, the color is set to .gray.
  • If I tap on the green rectangle, SecondView presents with a gray rectangle. (ie. the color DIDN'T change correctly).
  • If I then dismiss the SecondView and tap on the red rectangle, the SecondView presents with a red rectangle. (ie. the color DID change correctly.)

So, I'm wondering why this set up does NOT work on the initial load, but does work on the 2nd/3rd try?

Note: I understand this can be solved by changing the 'let selectedColor' to a @Binding variable, that's not what I'm asking.

Code:

import SwiftUI

    struct SegueTest: View {
        
        @State var showSheet: Bool = false
        @State var color: Color = .gray
        
        var body: some View {
            HStack {
                RoundedRectangle(cornerRadius: 25)
                    .fill(Color.red)
                    .frame(width: 100, height: 100)
                    .onTapGesture {
                        color = .red
                        showSheet.toggle()
                    }
                
                RoundedRectangle(cornerRadius: 25)
                    .fill(Color.green)
                    .frame(width: 100, height: 100)
                    .onTapGesture {
                        color = .green
                        showSheet.toggle()
                    }
    
                RoundedRectangle(cornerRadius: 25)
                    .fill(Color.orange)
                    .frame(width: 100, height: 100)
                    .onTapGesture {
                        color = .orange
                        showSheet.toggle()
                    }
    
            }
            .fullScreenCover(isPresented: $showSheet, content: {
                SecondView(selectedColor: color)
            })
        }
    }
    
    struct SecondView: View {
        
        @Environment(\.presentationMode) var presentationMode
        let selectedColor: Color // Should change to @Binding
        
        var body: some View {
            ZStack {
                Color.black.edgesIgnoringSafeArea(.all)
                
                RoundedRectangle(cornerRadius: 25)
                    .fill(selectedColor)
                    .frame(width: 300, height: 300)
            }
            .onTapGesture {
                presentationMode.wrappedValue.dismiss()
            }
        }
    
    }
    
    struct SegueTest_Previews: PreviewProvider {
        static var previews: some View {
            SegueTest()
        }
    }
2 Answers

See comments and print statements. Especially the red

import SwiftUI

struct SegueTest: View {
    
    @State var showSheet: Bool = false{
        didSet{
            print("showSheet :: didSet")
        }
        willSet{
            print("showSheet :: willSet")
        }
    }
    @State var color: Color = .gray{
        didSet{
            print("color :: didSet :: \(color.description)")
        }
        willSet{
            print("color :: willSet :: \(color.description)")
        }
    }
    @State var refresh: Bool = false
    init(){
        print("SegueTest " + #function)
    }
    var body: some View {
        print(#function)
        return HStack {
            //Just to see what happens when you recreate the View
            //Text(refresh.description)
            Text(color.description)
            RoundedRectangle(cornerRadius: 25)
                .fill(Color.red)
                .frame(width: 100, height: 100)
                .onTapGesture {
                    print("SegueTest :: onTapGesture :: red")
                    //Changing the color
                    color = .red
                    //Refreshed SegueTest reloads function
                    //refresh.toggle()
                    showSheet.toggle()
                }
            
            RoundedRectangle(cornerRadius: 25)
                .fill(Color.green)
                .frame(width: 100, height: 100)
                .onTapGesture {
                    print("SegueTest :: onTapGesture :: green")
                    //Changing the color
                    color = .green
                    showSheet.toggle()
                }
            
            RoundedRectangle(cornerRadius: 25)
                .fill(Color.orange)
                .frame(width: 100, height: 100)
                .onTapGesture {
                    print("SegueTest :: onTapGesture :: orange")
                    //Changing the color
                    color = .orange
                    showSheet.toggle()
                }
            
        }
        //This part is likely created when SegueTest is created and since a struct is immutable it keeps the original value
        .fullScreenCover(isPresented: $showSheet, content: {
            SecondView(selectedColor: color)
        })
    }
}

struct SecondView: View {
    
    @Environment(\.presentationMode) var presentationMode
    //struct is immutable
    let selectedColor: Color // Should change to @Binding
    init(selectedColor: Color){
        print("SecondView " + #function)
        self.selectedColor = selectedColor
        print("SecondView :: struct :: selectedColor = \(self.selectedColor.description)" )
        print("SecondView :: parameter :: selectedColor = \(selectedColor.description)" )
    }
    var body: some View {
        ZStack {
            Color.black.edgesIgnoringSafeArea(.all)
            
            RoundedRectangle(cornerRadius: 25)
                .fill(selectedColor)
                .frame(width: 300, height: 300)
        }
        .onTapGesture {
            presentationMode.wrappedValue.dismiss()
        }
    }
    
}

struct SegueTest_Previews: PreviewProvider {
    static var previews: some View {
        SegueTest()
    }
}

The problem is that you are not using your @State color inside SegueView, which will not reload your view on State change. Just include your @State somewhere in the code, which will force it to rerender at then update the sheet, with the correct color.

RoundedRectangle(cornerRadius: 25)
    .fill(color == .red ? Color.red : Color.red) //<< just use dump color variable here

If you do not include your color state somewhere in the code, it won't re render your SegueView, hence you still pass the old color gray to your SecondView.

Or even pass your colors as Binding your your Second View..

SecondView(selectedColor: $color)

@Binding var selectedColor: Color 
 
Related