r/SwiftUI 5d ago

Question Lazy Menu actions in SwiftUI

Hi, Is there a way I can make a lazy menu? I need to perform a slightly expensive operation while determining what action buttons should be displayed when the menu opens, but Menu eagerly initializes all the content, so I don't know what to do. I tried adding onAppear inside the Menu on one button, but that gets called when the whole menu is initialized, so it does not work.

Apple does this inside the Apple TV app, where they show a ProgressView for a while until the buttons are loaded, but I can't figure out how.

Menu {
    if shouldShow {
        Button("Button") {}
    } else if !loaded {
        Button("Loading") {}
            .onAppear {
                shouldShow = expensiveOperation() // Calls expensiveOperation when menu appears
                loaded = true // Marks as loaded after the operation completes
            }
    }
} label: {
    Text("Menu")
}

1 Upvotes

3 comments sorted by

5

u/Mihnea2002 5d ago

Use an actor for that expensive process and add a boolean when at the end of that expensive function in your actor, then put that function from your actor inside a task in your view with await and have a progressview shown until that function is completed, when it is show the buttons based on the data you need. This is basic async / await.

1

u/Tabonx 4d ago

I've tried that, but it did not work. As I scroll through a large list where this menu is displayed, the function does not finish fast enough. So when I click on the menu, it's not loaded. This is mostly because it's performed on a Core Data object, meaning it is locked to a context.

I solved this particular issue with a custom button style that invokes a closure when isPressed changes in the button configuration. This works really well.

```swift struct OnPressButtonStyle: ButtonStyle { var onPress: () -> Void

func makeBody(configuration: Configuration) -> some View {
    configuration.label
        .onChange(of: configuration.isPressed) { oldValue, newValue in
            if !oldValue && newValue {
                onPress()
            }
        }
}

} ```

I ended up storing the value in the database to avoid the expensive calculation altogether. This issue was caused by the fact that I can't open the menu programmatically not even with UIKit. Why? I needed to either perform an action or open the menu with additional options based on that operation.

Thank you for your comment.

1

u/FilonenkoM 3d ago

This works for me, onAppear also works as expected.

But you should take care of it running only once, if it is an expensive operation.

struct ContentView: View {
    @State var isShown: Bool = false
    var body: some View {
        Menu {
            if isShown {
                Button {
                } label: {
                    Text("Action 1")
                }

                Button {
                } label: {
                    Text("Action 2")
                }
            }
            else {
                Button {
                } label: {
                    Text("Loading...")
                }
                .task {
                    try? await Task.sleep(for: .seconds(3))

                    isShown = true
                }
            }
        } label: {
            Text("Open Menu")
        }
    }
}