Context Menu with Preview in SwiftUI
--
SwiftUI already provides many native components and .contextMenu
is one of them. However, unfortunately, it doesn’t have as much customization as we have in UIKit. ContextMenu
only supports showing menu items with a 3D preview of the applied view but does not support showing a custom 3D preview or having a destination.
For this reason, I have created a context menu with preview component that can mimic UIKit features. As this is only a workaround for now, I took advantage of UIViewRepresentable
. I hope and believe that a native preview/destination support will be available in SwiftUI 3.0 this fall.
Part 1: Setting Up the View Extension
I aimed to solve three problems:
- Displaying a custom preview with no destination
- Displaying a custom preview with the same destination
- Displaying a custom preview with a different destination
Hence, I created three functions in a View extension:
Using the above View
extension, we can now achieve the tasks above.
In total, we have five variables we can use to customize our experience:
preview
: This is the 3D preview that we want to display.destination
: This is the destination view that we want to navigate if the user taps on the preview.preferredContentSize
: This is a size we can set if we want to that will force the view to have that size. If we do not set a size here, the view will take the all available space which is usually what we want.presentAsSheet
: This specifies whether we want the destination view be presented as a sheet or in a navigation stack.actions
: These are the actions/buttons that will show up under the 3D preview.
Part 2: Setting Up the Modifier
As you can see in the code above, we initialize PreviewContextViewModifier
on each modifier
. PreviewContextViewModifier
will be the core of this component.
In this modifier we have three initializers as well. Most important part of this view is in the body
.
Depending on the presentAsSheet
variable, we either create a NavigationLink
or add .sheet
modifier to the view. For sheet modifier, I used a custom if block that you can also use in your code.
I set the overlay opacity as 0.05
as there will be a short amount of time that the preview appears after the preview is dismissed that this opacity modifier will fix.
On top of the content view, we add an overlay of PreviewContextView
that I will explain next.
Part 3: Setting Up the View
PreviewContextView
will be a wrapper of the UIKit component. It will handle the UIContextMenuInteractionDelegate
using its Coordinator
.
Here, we basically provide the preview and activate the destination when the preview is tapped.
Part 4: Setting Up the Actions
How do we handle the actions for our preview context menu then? Normally, in UIKit
we use UIAction
s to add actions. In my wrapper component though, I wanted to create a custom action struct that will make creating buttons/actions easier.
Using this action struct, we can add actions using title, image, system image, and attributes. Good thing about this action is that we can now pass .destructive
attribute that would make the button red (this is unfortunately is not possible in SwiftUI) yet.
I have also created a
ButtonBuilder
using the@_functionBuilder
modifier. Don’t worry, it is not a private API. It just has not gone through Swift Evolution yet and not recommended to use in a production code, however, as I created this component as a workaround I decided to use it. This way we can pass the actions without using an array. You can always remove the function builder and pass an array instead.
Finally: Preview Context Menu in Action
I will demo some different cases we can achieve:
1. Displaying a custom preview with no destination
2. Displaying a custom preview with destination (same view for both)
3. Displaying a custom preview with a custom destination
4. Displaying a custom preview with custom size
5. Displaying the destination as a sheet
I hope you enjoyed this tutorial. Please let me know If I have missed anything or if you want me to add/explain any specific point.
I look forward to hearing your feedback.