14

I am trying to setup a SwiftUI .contextMenu with a button that toggle a Bool value. The context menu's button text is supposed to change when the Bool toggles. But the context menu doesn't update. Is there a way to force update the context menu?

Sample code depicting the issue:

import SwiftUI

struct Item: Identifiable {
    let id = UUID()
    let user: String
    var active: Bool
}

struct ContentView: View {
    @State var items: [Item] = [
        Item(user: "Daniel",active: false),
        Item(user: "Jack", active: true),
        Item(user: "John", active: true)
    ]
    
    @State var isOn : Bool = false
    var body: some View {
        List {
            ForEach(items) { item in
                VStack {
                    Text("\(item.user)")
                        
                    HStack {
                        Text("Active ? ")
                        Text(item.active ? "YES": "NO")
                    }
                }.contextMenu{
                    Button(action: {
                        let index = items.firstIndex{$0.user == item.user}
                        
                        items[index!].active.toggle()
                    }) {
                        Text(item.active ? "Set as Active": "Set as Inactive")
                    }
                }
            }
        }
    }
}

2 Answers 2

11

It's a bug in SwiftUI, and it is still broken in the simulator of Xcode 13.2 beta 2.

I managed to work around it by duplicating the list item in both branches of an if item.active statement, like this:

struct ContentView: View {
    @State var items: [Item] = [
        Item(user: "Daniel",active: false),
        Item(user: "Jack", active: true),
        Item(user: "John", active: true)
    ]

    var body: some View {
        List {
            ForEach(items) { item in
                // work around SwiftUI bug where
                // context menu doesn't update
                if item.active { listItem(for: item) }
                else { listItem(for: item) }
            }
        }
    }

    private func listItem(for item: Item) -> some View {
        VStack {
            Text("\(item.user)")

            HStack {
                Text("Active ? ")
                Text(item.active ? "YES": "NO")
            }
        }
        .contextMenu {
            Button(item.active ? "Deactivate" : "Activate") {
                if let index = items.firstIndex(where: { $0.id == item.id }) {
                    items[index].active.toggle()
                }
            }
        }
        .id(item.id)
    }
}
Sign up to request clarification or add additional context in comments.

1 Comment

Continues to be broken in Xcode Version 13.4 (13F17a) — on both simulator and live device.
6

This is a bug with SwiftUI in iOS 15 only. iOS 14 and 16 properly update on state variable changes.

A workaround for this can be done by adding the ContextMenu in a background to the item (Color.clear in this case). This also requires the id to be set to the property that's changed (in this case, it's item.active)

struct Item: Identifiable {
    let id = UUID()
    let user: String
    var active: Bool
}

struct ContentView: View {
    @State var items: [Item] = [
        Item(user: "Daniel", active: false),
        Item(user: "Jack", active: true),
        Item(user: "John", active: true)
    ]

    var body: some View {
        List {
            ForEach(items) { item in
                VStack {
                    Text("\(item.user)")
                        
                    HStack {
                        Text("Active ? ")
                        Text(item.active ? "YES": "NO")
                    }
                }
                .background {
                    Color.clear
                        .contextMenu{
                            Button(action: {
                                if let index = items.firstIndex(where: {$0.user == item.user}) {
                                    items[index].active.toggle()
                                }
                            }) {
                                Text(item.active ? "Set as Inactive": "Set as Active")
                            }
                        }
                        .id(item.active)
                }
            }
        }
    }
}

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.