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
    }
}