TEAM LICENSES: Save money and learn new skills through a Hacking with Swift+ team license >>

SOLVED: isPresented: on the basis of an observed object not being empty?

Forums > SwiftUI

I have an @Observable that looks like this:

@Observable class BStoreOO {
    var items: [String] = []

    func updateItems(newItems: [String]) {
        self.items = newItems
    }
}

This is passed into the main app as an environment.

Inside my ContentView I want to toggle a popover/sheet on the basis of whether BStoreOO.items is empty or not.

The items is empty by default but gets populated by pressing buttons elsewhere in the app (I can see the value updating in print statements).

At the top of ContentView I have this:

struct ContentView: View {
    @State var bStore = BStoreOO()
    func shouldShowSheet() -> Binding<Bool> {
        return .init(get: {
            return bStore.items.count > 0
        }, set: { _ in })
    }
    var body: some View {
        @Bindable var bStore = bStore
        ...

And then further down, on a ZStack I have this:

.popover(isPresented: !$bStore.items.isEmpty) {
            PopoverContent()
        }

This produces the error Cannot convert value of type 'Bool' to expected argument type 'Binding<Bool>

I have also tried this:

.popover(isPresented: shouldShowSheet()) {
            PopoverContent() // Pass the items from the DataStore
        }

Which doesn't error, but also does not show the PopoverContent() when the value of bStore.items changes.

I have also tried using onChange like this:

.popover(isPresented: $isShowingBS) {
            PopoverContent()
        }
        .onChange(of: bStore.items) {
            self.isShowingBS = self.bStore.items.count > 0
        }

And again, this does not error but the popover is not shown either.

How can I link the display of the popover to the changing count of items in the bStore?

   

Hi @Obelix

I'm new to SwiftUI so certainly been useful to read through your code samples for better ways to structure things.

However, my question is whether, in principle, it is possible to use to present a sheet or popover, purely on the basis of a Observed array changing.

I'm unsure at this point if I'm fundamentally approaching the problem of how to present this popover/sheet in the wrong way?

As I'm not seeing errors, and I can see the array contents changing, I'm unsure if there is some kind of binding I should be doing?

   

Thanks for the additional code. I am unsure if I lack sufficient comprehension of your solution or I have just not explained myself very well. Probably both!

I will attempt to re-iterate my requirements/problem with a simpler shopping list analogy. Sorry you are having to spoon feed me here!

Suppose we have our MainView, that displays a list of possible items available. This view also has the ShoppingBasketView sheet attached that may be needed to popover all other views when it has items in the ShoppingBasketContents array.

However, in order to actually add an item to our ShoppingBasketContents, we need to navigate from the MainView to one of the item ChildViews (different file). The ChildView has a button to append an item to our array of shopping items (ShoppingBasketContent). It also has a button to remove that item from the ShoppingBasketContents array.

As these ChildViews may be any level "deep" in the app, my intention was to use @Environment for ferrying the ShoppingBasketContents around, without needing to do 'prop-drilling' through each view from MainView to ChildView.

So...

  • MainView - shows our list of possible items, and has the ShoppingBasketView, presented as a popover/sheet
  • ShoppingBasketView - displays the contents of the ShoppingBasketContents array, and should not be presented if there are currently no items in the ShoppingBasketContents array.
  • ChildView - displays the detail of an item and has an "Add to Basket" button. This appends an item into the ShoppingItems array, which I had assumed it would be easiest to access via @Environment(basketStore.self) var items. It could also remove this item from the ShoppingBasketContents array, and the ShoppingBasketView should show/hide if this results in a change between no items or some items in the ShoppingBasketContents array.

The crux of my issue is getting the ShoppingBasketView to show/hide reactively to changes to the ShoppingBasketContents that occur from child views.

So suppose the user is on the ChildView, and the ShoppingBasketView is showing at the bottom of the screen as a fraction of the screen, because they have just clicked the 'add this item' button, and the user subsequently clicks on the 'remove this item' button, the ShoppingBasketView hides itself as there is no longer any items in the basket.

Does this explanation alter your proposed solution? Or do I simply lack understanding at this point?

   

At present, I am only able to get the desired outcome by doing this in the @Observable:

@Observable class BStoreOO {
    var items: [Int] = []

    var shouldShowBasket: Bool = false

    public var itemsIsEmpty: Bool { items.isEmpty }

    func updateItems(newItems: [Int]) {
        self.items = newItems
    }
}

This is being passed into MainView as an environment modifier like so:

.environment(BStoreOO())

That recieved into MainView like this:

@Environment(BStoreOO.self) var bStoreEnvOO

Then made bindable in that view's body with this:

@Bindable var bsBindable = bStoreEnvOO

And then the popover is presented like this:

.popover(isPresented: $bsBindable.shouldShowBasket) {
                PopoverContent()
            }

However, the bit that feels a bit skanky is in order to update that shouldShowBasket, in the ChildView I have to do this:

@Environment(BStoreOO.self) var basketItems

And then have the logic of updating the @Observable like this:

Button(action: {
     basketItems.items.append(1)
     if !basketItems.itemsIsEmpty {
          basketItems.shouldShowBasket = true
     }
}) {
    // ...visuals
}

That feels 'off' to me. I feel that it should be possible to toggle the popover merely by asking whether the itemsIsEmpty, and have that be dynamic and react to be true/false and present/hide the popover accordingly.

Having to directly set the Bool from a child view seems weird and I will have a lot of repeated code for every button that could add an item to the bastket??

   

How about adding to items a didSet property observer that sets the value of shouldShowBasket according to items.isEmpty.

You can get rid of itemsIsEmpty.

   

Thanks @bobstern, I did not know about the didSet property, that was exactly the thing I needed!

   

Do other peoples replies get automatically deleted when a solution is ticked? That's pretty annoying as there was plenty of interesting stuff in other peoples replies that would be useful for future travellers.

   

@twostraws  Site AdminHWS+

No replies get deleted when an answer is marked solved. Occasionally I delete replies that are from spam bots, some of whom try to be clever by using ChatGPT to add some "helpful" replies to their account before they wade in with spam.

1      

That explains a lot ;) Thanks

   

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!

Reply to this topic…

You need to create an account or log in to reply.

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.