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:

message_image_styled

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:

title_styling

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:

message_image

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:

message_image_styled

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:

tip_view_styled

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.

Previous Post