1

I have a modal presented Sheet which should display based on a UserDefault bool. I wrote a UI-Test which dismisses the sheet and I use launch arguments to control the defaults value.

However, when I tried using @AppStorage initially it didn't seem to persist the value, or was secretly not writing it? My test failed as after 'dismissing' the modal pops back up as the value is unchanged.

To work around this I wrote a custom binding. But i'm not sure what the behaviour difference is between the two implementations? The test passes this way.

Does anyone know what i'm not understanding sorry?

Cheers!

Simple Example

1. AppStorage

struct ContentView: View {
  @AppStorage("should.show.sheet") private var binding: Bool = true

  var body: some View {
    Text("Content View")
      .sheet(isPresented: $binding) {
        Text("Sheet")
      }
  }
}

2. Custom Binding:

struct ContentView: View {
  var body: some View {
    let binding = Binding<Bool> {
        UserDefaults.standard.bool(forKey: "should.show.sheet")
    } set: {
        UserDefaults.standard.set($0, forKey: "should.show.sheet")
    }
      
    Text("Content View")
      .sheet(isPresented: binding) {
        Text("Sheet")
      }
  }
}

Test Case:

final class SheetUITests: XCTestCase {
  override func setUpWithError() throws {
      continueAfterFailure = false
  }
  
  func testDismiss() {
      // Given 'true' userdefault value to show sheet on launch
      let app = XCUIApplication()
      app.launchArguments += ["-should.show.sheet", "<true/>"]
      app.launch()
      
      // When the user dismisses the modal view
      app.swipeDown()
      
      // Wait a second for animation (make sure it doesn't pop back)
      sleep(1)
      
      // Then the sheet should not be displayed
      XCTAssertFalse(app.staticTexts["Sheet"].exists)
  }
}
lacking-cypher
  • 483
  • 4
  • 16

1 Answers1

2
  1. It does not work even when running app, because of that "." in key name (looks like this is AppStorage limitation, so use simple notation, like isSheet.

  2. IMO the test-case is not correct, because it overrides defaults by arguments domain, but it is read-only, so writing is tried into persistent domain, but there might be same value there, so no change (read didSet) event is generated, so there no update in UI.

  3. To test this it should be paired events inside app, ie. one gives AppStorage value true, other one gives false

*Note: boolean value is presented in arguments as 0/1 (not XML), lie -isSheet 1

Asperi
  • 228,894
  • 20
  • 464
  • 690
  • I removed the periods from the key and it worked as expected (though wrong approach). Brilliant! Can I ask how you discovered this limitation sorry? Thank you as well for the advice on testing approach. I will update it to be more reliable. Regarding the note that's cool to know. I (very vaguelly) remember being told it would parse like property list data. But yeah `app.launchArguments += ["-shouldShowSheet", "1"]` works great too. I'll need to try and find some documentation on this area again. Cheers again for answering my quesiton and going above the extra help, really appreciated! – lacking-cypher May 14 '22 at 15:00
  • Commenting just to say how important point 2 is. Running UI tests requires a known state for user defaults. Trying to get there via launch arguments only works if your tests never change user defaults (because of the problem explained in point 2). And for what it's worth, currently there doesn't seem to be a way to make user defaults work with UI tests without custom wrappers. – Erik Doernenburg Jan 15 '23 at 19:29