Reading time: 3 min
This recipe shows how to style and customize your SwiftUI tooltips. It relies on TipKit for managing tooltip appearance and their state.
The end result looks like this:
This code works starting with iOS 17 and macOS 14.
If you haven't, make sure to check the first recipe in the series for the basic intro. The code in this article builds up on the code presented in that recipe.
Styling the title
A great thing about the Tip
protocol methods is that they return a SwiftUI Text
, making all the appearance modifiers available out of the box:
enum Tooltip: Tip, CaseIterable {
case add
var title: Text {
switch self {
case .add:
return Text("Add a new item to the list")
.bold()
.italic()
.font(.title)
.foregroundStyle(.red)
}
}
}
Which results in this:
Adding message and image
The Tip
protocol has a few more optional methods that we can use to further customize our tooltips. For example, we can use message
and image
properties to show additional info:
enum Tooltip: Tip, CaseIterable {
case add
var title: Text {
switch self {
case .add:
return Text("Add a new item to the list")
}
}
var message: Text? {
switch self {
case .add:
return Text("Clicking here will add ")
+ Text("another row to this already large list!").bold()
}
}
var image: Image? {
switch self {
case .add:
return Image(systemName: "exclamationmark.questionmark")
}
}
}
Resulting in:
Styling tooltip image
As you can see, styling the message is simple enough since a good chunk of Text
-styling modifiers return a Text
themselves, but styling the image is a tricker proposition. E.g, if you wanted to change the color of our !?, we'd be in trouble since foregroundColor
and foregroundStyle
both return some View
, while our image
property is required to return an Image
.
As usual, we have to resort to tricks, just like we did in when we were inserting image into Text views, by converting the resulting view into an UIImage
by utilizing `ImageRenderer.
The method from this recipe won't work on iOS < 16, therefore we have to resort to using an ImageRenderer
, which in itself isn't an issue since TipKit is only available since iOS 17.
The first step is to add View-to-Image conversion method as a modifier:
extension View {
@MainActor var asImageWithRenderer: UIImage {
let renderer = ImageRenderer(content: self)
renderer.scale = UIScreen.main.scale
return renderer.uiImage ?? UIImage()
}
}
Then, we can supply it to a these Image
modifiers, allowing us to use the modifiers we need while still returning an Image
:
@MainActor extension Image {
func foregroundStyleAsImage<S>(_ style: S) -> Image
where S : ShapeStyle {
Image(uiImage: self
.foregroundStyle(style)
.asImageWithRenderer)
}
func foregroundStyleAsImage<S1, S2>(_ primary: S1, _ secondary: S2) -> Image
where S1 : ShapeStyle, S2 : ShapeStyle {
Image(uiImage: self
.foregroundStyle(primary, secondary)
.asImageWithRenderer)
}
}
After that, styling the tooltip image becomes trivial:
@MainActor var image: Image? { // notice the @MainActor
switch self {
case .add:
return Image(systemName: "exclamationmark.questionmark")
.foregroundStyleAsImage(.red) // HERE
}
}
The result is here:
Styling TipViews
TipView
has a few modifiers that can affect its appearance. Here's a simple showcase that lists all of them, given their names are pretty self-explanatory:
TipView(Tooltip.add)
.tipBackground(.orange)
.tipCornerRadius(50)
.tipImageSize(CGSize(width: 70, height: 70))
This ends up looking something like this:
While all the modifiers above (tipBackground
, tipCornerRadius
and tipImageSize
) are available with the popoverTip
modifier, they don't work it and don't modify its appearance.