The aim for this post to build a very simple List
view. When you long-press a row you will see a classic iOS context menu. It will look a little like this:
Why Context Menus?
Context menus give users an easy way to discover secondary actions right where they’re looking. They:
- Keep the primary UI clean.
- Surface actions only when they’re relevant.
- Feel familiar thanks to the long-press gesture we already use across iOS.
What We’ll Build
We’ll create a simple list of fruit. A long-press on any row reveals three actions:
- “Favorite” – toggles a star
- “Delete” – removes the item
- “Share” – brings up a share sheet
That’s enough functionality to highlight the most common context-menu patterns.
Project Setup
- Create a new iOS 16+ SwiftUI project.
- Delete the template
ContentView.swift
; we’ll replace it with our own.
The Model
struct Fruit: Identifiable {
let id = UUID()
var name: String
var isFavorite: Bool = false
}
@MainActor
final class FruitStore: ObservableObject {
@Published var fruits: [Fruit] = [
.init(name: "🍎 Apple"),
.init(name: "🍌 Banana"),
.init(name: "🍑 Peach"),
.init(name: "🍍 Pineapple")
]
func delete(_ fruit: Fruit) {
fruits.removeAll { $0.id == fruit.id }
}
}
Nothing fancy—just an ObservableObject
to hold an array of Fruit
. The function delete
removes a Fruit
from the array.
Building the List
struct FruitList: View {
@StateObject private var store = FruitStore()
@State private var shareItem: Fruit?
var body: some View {
NavigationStack {
List {
ForEach(store.fruits) { fruit in
FruitRow(fruit: fruit)
// Here we add the context menu
.contextMenu {
contextMenu(for: fruit)
}
}
}
.navigationTitle("Fruit Basket")
.sheet(item: $shareItem) { fruit in
ActivityView(items: [fruit.name])
}
}
}
}
FruitRow
is our reusable cell view (see below).contextMenu
attaches a menu to every row. This means any allFruitRow
can be long-pressed to see a context menu.- We keep a
@State
property (shareItem
) to drive a share sheet. This is only here as one of our context menu items is a share button.
The Row View
struct FruitRow: View {
var fruit: Fruit
var body: some View {
HStack {
Text(fruit.name)
.font(.headline)
Spacer()
if fruit.isFavorite {
Image(systemName: "star.fill")
.foregroundColor(.yellow)
.transition(.scale)
}
}
.animation(.default, value: fruit.isFavorite)
}
}
The tiny star uses a scale transition so it animates in nicely.
Crafting the Context Menu
extension FruitList {
@ViewBuilder
func contextMenu(for fruit: Fruit) -> some View {
Button {
toggleFavorite(fruit)
} label: {
Label("Favorite", systemImage: fruit.isFavorite ? "star.slash" : "star")
}
Button(role: .destructive) {
store.delete(fruit)
} label: {
Label("Delete", systemImage: "trash")
}
Button {
shareItem = fruit
} label: {
Label("Share", systemImage: "square.and.arrow.up")
}
}
private func toggleFavorite(_ fruit: Fruit) {
if let index = store.fruits.firstIndex(where: { $0.id == fruit.id }) {
store.fruits[index].isFavorite.toggle()
}
}
}
Here we use a ViewBuilder
so that we can extract the logic that builds the context menu. This makes the earlier call site easier to read, and separates the context menu logic.
ButtonRole
.destructive
role automatically tints the “Delete” button red..cancel
is another available option
Adding a Share Sheet
A share button in a context menu is fairly common. When tapped it presents a system share sheet.
UIKit still owns the system share sheet, so we wrap it in UIViewControllerRepresentable
.
struct ActivityView: UIViewControllerRepresentable {
let items: [Any]
func makeUIViewController(context: Context) -> UIActivityViewController {
UIActivityViewController(activityItems: items, applicationActivities: nil)
}
func updateUIViewController(_ uiViewController: UIActivityViewController, context: Context) { }
}
Bonus: Context Menus on Grid Items
The API is view-agnostic. Swap List
for LazyVGrid
, attach the same contextMenu
, and you get identical behavior.
Conclusion
Context menus hide complexity until the user asks for it. In SwiftUI they’re a one-liner (.contextMenu
) but, as we’ve seen, you can layer in:
- Role-based styling (
.destructive
) - Animated state changes
- System integrations like the share sheet
Context menus… easy to implement and a delight to discover.
Happy coding! 🍏