Context Menus are (typically) small menus attached to specific elements of our user interface and that provide context specific functionality. We probably won’t want too many of these but, properly placed, they can greatly enhance our UI.

For the purposes of this article, I will use the same demo project that I used in the article on menus with a few modifications.

The Data

To illustrate the context menu, we will need some data to display in a simple UI. The app will display a list of names.

Name List

To achieve this, we're going to need a little sample data and a couple of enums. We'll define these in a single file called UserData.swift.

struct User: Identifiable, Hashable {
    public var id = UUID()
    public var firstName: String
    public var lastName: String
    
    public func description(orderedBy: OrderBy) -> String {
        switch orderedBy {
        case .firstName:
            return "\(firstName) \(lastName)"
        case .lastName:
            return "\(lastName), \(firstName)"
        }
    }
}

Our user is simple and consists of a first and last name. I'm going to want to display it in a list and will want to be able to select an item, so it implements Identifiable and Hashable. I want the format of a user to change depending on how the list has been sorted; hence the description function.

enum OrderBy: Identifiable, CaseIterable, CustomStringConvertible {
    case firstName
    case lastName
    
    var id: String {
        self.description
    }
    
    var description: String {
        switch self {
        case .firstName:
            return "First Name"
        case .lastName:
            return "Last Name"
        }
    }
}

I'm going to want to sort the list, so have defined an OrderBy enum. I'll want to use this to construct a Picker for use as a sub-menu, so the Enum needs to conform to Identifiable and CaseIterable. I am also conforming to CustomStringConvertible which requires that I implement a description computed variable.

@Observable
class UserData {
    
    public var users: [User]
    
    init() {
        users = [
            User(firstName: "Conni", lastName: "Sangwine"),
            User(firstName: "Meara", lastName: "Kohlerman"),
            User(firstName: "Ragnar", lastName: "Defraine"),
            User(firstName: "Taylor", lastName: "Bool"),
            User(firstName: "Robinet", lastName: "Nicklin"),
            User(firstName: "Jacquelin", lastName: "Kacheler"),
            User(firstName: "Grace", lastName: "Gilmore"),
            User(firstName: "Juditha", lastName: "Stoddard"),
            User(firstName: "Ginger", lastName: "Ludron"),
            User(firstName: "Minne", lastName: "Snazle"),
            User(firstName: "Roderich", lastName: "Pittem")
        ]
    }
    
    func userList(orderedBy: OrderBy) -> [User] {
        switch orderedBy {
        case .firstName:
            users.sorted(by: {$0.firstName < $1.firstName})
        case .lastName:
            users.sorted(by: {$0.lastName < $1.lastName})
        }
    }
}

Finally, we have our data source which defines a published array of users and a helper function to return the array in sorted order.

That's the data dealt with. We now need a way to display it.

Initial Name List Display

Our first pass at the display code will not include the context menu yet. The first thing we need to achieve is the list containing the names.

struct ContentView: View {
    
    @State var names = UserData()
    @State private var selection: User? = nil
    @State private var orderBy: OrderBy = .firstName
    
    var nameList: [User] { names.userList(orderedBy: orderBy) }
    
    var body: some View {
        VStack {
            List(nameList, id: \.self, selection: $selection) { name in
                Text(name.description(orderedBy: orderBy))
            }
        }
        .padding()
    }
}

Firstly, we create an instance of the UserData data source. Next a variable to bind to the list to keep track of the selected user and lastly the current sort order. This is the entire state for our list.

We have a helper variable called nameList that we connect the List to. This deals with calling the userList function of our data source to retrieve the data in the correct order.

Our view consists of a simple List, based on the list of users in sorted order.

Name List

I want three context menu items;

  1. An option to duplicate the currently selected item.
  2. An option to delete the currently selected item.
  3. An option to sort the list by first name or last name.

We can attach our context menu to two points in the code. Either the list itself or the individual rows in the list. Attaching to the rows gives us the option to have different menu items depending on which name is being displayed. That's unnecessary in this simple example, so we will add our context menu to the list itself.

var body: some View {
    VStack {
        List(nameList, id: \.self, selection: $selection) { name in
            Text(name.description(orderedBy: orderBy))
        }
        .contextMenu {
            Button(action: { duplicateItem() },
                   label: { Text("Duplicate Me")})
                .disabled(selection == nil)
            
            Picker(selection: $orderBy, content: {
                ForEach(OrderBy.allCases) { order in
                    Text(order.description).tag(order)
                }
            }, label: { Text("Order By") })
            
            Divider()
            Button(action: { deleteItem() },
                   label: { Text("Delete Me")})
                .disabled(selection == nil)
        }
    }
    .padding()
}

func duplicateItem() {
    guard let name = selection else { return }
    print("Duplicate \(name.description(orderedBy: orderBy))")
}

func deleteItem() {
    guard let name = selection else { return }
    print("Delete \(name.description(orderedBy: orderBy))")
}

The context menu is defined using the .contextMenu modifier. Within that, we define the content of the menu that we want. Here we have defined a Button to duplicate, a Picker to select the sort order and a second Button to delete the current item. There are helper functions to deal with the logic of duplication and deletion.

Running this, selecting an item in the list and right clicking gives us the context menu.

Context Menu

Moving down to the Order By menu expands the menu out to show the ordering options.

Context Menu Ordering

Selecting order by last name reorders our list using the last name and changes the way names are displayed to show last name, first name.

Ordered By Lastname

Our context menu can be displayed by right clicking anywhere on the list, even in blank space. If we wanted to ensure that the menu only appears when we right click on a name, it would have to be attached to the Text view within the List.

We have also coded the context menu to disable options that are not appropriate if nothing is selected, stopping you from duplicating or deleting items if there is no current selection.

Get The Code
Testimonials
Testimonials

Am I really any good?

Don't take my word for my abilities, take a look at other peoples opinions about me.