iosswiftswiftuigeometryreaderappstorage

SwiftUI - AppStorage doesn't work with GeometryReader


Here is a simple example. You can create new SwiftUI iOS project and copy it to ContentView file.

import SwiftUI

struct Settings {
    static let onOff = "onOff"
}

struct ContentView: View {
    @AppStorage(wrappedValue: false, Settings.onOff) var onOff
    
    var body: some View {
        NavigationView {
            GeometryReader { reader in // < Comment out this line
                List {
                    Section (header:
                                VStack {
                                    HStack {
                                        Spacer()
                                        VStack {
                                            Text("THIS SHOULD BE FULL-WIDTH")
                                            Text("It is thanks to GeometryReader")
                                        }
                                        Spacer()
                                    }
                                    .padding()
                                    .background(Color.yellow)
                                    
                                    HStack {
                                        Text("This should update from AppStorage: ")
                                        Spacer()
                                        Text(onOff == true ? "ON" : "OFF")
                                    }
                                    .padding()
                                }
                                .frame(width: reader.size.width) // < Comment out this line
                                .textCase(nil)
                                .font(.body)
                    ) {
                        Toggle(isOn: $onOff) {
                            Text("ON / OFF")
                        }
                    }
                    
                }
                .listStyle(GroupedListStyle())
            } // < Comment out this line
            
        }
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

I have 3 elements:

  1. Text with yellow background - I need it full-width and I use GeometryReader to do it.
  2. Text. Last word should switch ON/OFF based on toggle value. This is just for testing purposes to check if AppStorage works correctly.
  3. Toggle - switches onOff variable and saves it to AppStorage (UserDefaults).

AppStorage works perfectly only without GeometryReader. Please comment out 3 tagged lines to check it out.

Is it a bug? Or something is wrong with my AppStorage code? Or maybe GeometryReader part is wrong? If I could set yellow part full-width, I could drop GeometryReader completely.


Solution

  • One solution that works in my testing is to factor out the GeometryReader's content, including the @AppStorage:

    struct ContentView: View {
        var body: some View {
            NavigationView {
                GeometryReader { proxy in
                    _ContentView(width: proxy.size.width)
                }
            }
        }
    }
    
    struct _ContentView: View {
        var width: CGFloat
        @AppStorage(wrappedValue: false, Settings.onOff) var onOff
    
        var body: some View {
            List {
                Section(header: header) {
                    Toggle(isOn: $onOff) {
                        Text("ON / OFF")
                    }
                }
    
            }
            .listStyle(GroupedListStyle())
        }
    
        var header: some View {
            VStack {
                VStack {
                    Text("THIS SHOULD BE FULL-WIDTH")
                    Text("It is thanks to GeometryReader")
                }
                .padding()
                .frame(width: width)
                .background(Color.yellow)
    
                HStack {
                    Text("This should update from AppStorage: ")
                    Spacer()
                    Text(onOff == true ? "ON" : "OFF")
                }
                .padding()
            }
            .textCase(nil)
            .font(.body)
        }
    }