SwiftUI : How do you display a tooltip / hint on hover?
When the overlay isn't good enough, e.g. you want the tooltip on a control that accepts mouse events (and an overlay would not allow clicks through), such as Toggle, a solution may be to use a Host view that internally includes a NSHostingView itself - that supports a tooltip being an AppKit view - eventually loading further SwiftUI content inside:
struct Tooltip<Content: View>: NSViewRepresentable {
typealias NSViewType = NSHostingView<Content>
init(_ text: String?, @ViewBuilder content: () -> Content) {
self.text = text
self.content = content()
}
let text: String?
let content: Content
func makeNSView(context: NSViewRepresentableContext<Tooltip<Content>>) -> NSViewType {
NSViewType(rootView: content)
}
func updateNSView(_ nsView: NSViewType, context: NSViewRepresentableContext<Tooltip<Content>>) {
nsView.rootView = content
nsView.toolTip = text
}
}
This does have some caveats regarding sizing when used with certain SwiftUI content (and then you may hopefully use fixedSize() or a frame(width:height:) to get it working as you need), but it's otherwise easy to use:
Tooltip("A description") {
Toggle("...", isOn: $isOn)
}
Thanks to both Andrew and Sorin for the solution direction. The presented solutions mostly worked but when I used them they totally messed up the layout. It turns out that the Tooltip has its own size, frame etc. which isn't automatically matching the content.
In theory I could address those problems by using fixed frames etc. but that did not seem the right direction to me.
I have come up with the following (slightly more complex) but easy to use solution which doesn't have these drawbacks.
extension View {
func tooltip(_ tip: String) -> some View {
background(GeometryReader { childGeometry in
TooltipView(tip, geometry: childGeometry) {
self
}
})
}
}
private struct TooltipView<Content>: View where Content: View {
let content: () -> Content
let tip: String
let geometry: GeometryProxy
init(_ tip: String, geometry: GeometryProxy, @ViewBuilder content: @escaping () -> Content) {
self.content = content
self.tip = tip
self.geometry = geometry
}
var body: some View {
Tooltip(tip, content: content)
.frame(width: geometry.size.width, height: geometry.size.height)
}
}
private struct Tooltip<Content: View>: NSViewRepresentable {
typealias NSViewType = NSHostingView<Content>
init(_ text: String?, @ViewBuilder content: () -> Content) {
self.text = text
self.content = content()
}
let text: String?
let content: Content
func makeNSView(context _: Context) -> NSHostingView<Content> {
NSViewType(rootView: content)
}
func updateNSView(_ nsView: NSHostingView<Content>, context _: Context) {
nsView.rootView = content
nsView.toolTip = text
}
}
I have added a GeometryReader
to the content of the tooltip and then constrain the size of the Tooltip to the match the size of the content.
To use it:
Toggle("...", isOn: $isOn)
.tooltip("This is my tip")
SwiftUI 2.0
As simple as
Button("Action") { }
.help("Just do something")
Button("Action") { }
.help(Text("Just do something"))
2020 | SwiftUI 1 and 2 both
In swiftUI 2:
Toggle("...", isOn: $isOn)
.help("this is tooltip")
In swiftUI 1 there is really no native way to create a tooltip. But here is a solution also for this:
import Foundation
import SwiftUI
public extension View {
/// Overlays this view with a view that provides a Help Tag.
func toolTip(_ toolTip: String) -> some View {
self.overlay(TooltipView(toolTip).allowsHitTesting(false))
}
}
private struct TooltipView: NSViewRepresentable {
let toolTip: String
init(_ toolTip: String?) {
if let toolTip = toolTip {
self.toolTip = toolTip
}
else
{
self.toolTip = ""
}
}
func makeNSView(context: NSViewRepresentableContext<TooltipView>) -> NSView {
NSView()
}
func updateNSView(_ nsView: NSView, context: NSViewRepresentableContext<TooltipView>) {
nsView.toolTip = self.toolTip
}
}