Search code examples
swiftuiuikitcore-graphics

Text lines background color


I'm trying to recreate the HTML span background coloring effect in SwiftUI. Not the whole bounding view should be colored, only the text lines. Can this be done easily in SwiftUI / UIKit / Core Graphics ?

div {
  max-width: 400px;
  line-height: 2;
  font-family: sans-serif;
}

span {
  background-color: blue;
  color: white;
  padding: 0.3em;
}
<div>
  <span>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed varius, nulla eget consequat finibus, tortor erat scelerisque ipsum, nec dictum justo quam in ipsum. Nulla nec eleifend felis. Sed vel semper mauris, a placerat elit.</span>
</div>

struct ContentView: View {
    var body: some View {
        Text("Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed varius, nulla eget consequat finibus, tortor erat scelerisque ipsum, nec dictum justo quam in ipsum. Nulla nec eleifend felis. Sed vel semper mauris, a placerat elit.")
            .foregroundColor(.white)
            .background(Color.blue)
            .frame(maxWidth: 200)
            .lineSpacing(6)
    }
}

SwiftUI Preview: SwiftUI Preview


Solution

  • In iOS 18, you can now create a TextRenderer to do this. Here is an implementation that draws a background behind the text lines with a specified amount of padding.

    struct HighlightedTextRenderer: TextRenderer {
        let padding: CGFloat
        
        func draw(layout: Text.Layout, in ctx: inout GraphicsContext) {
            ctx.translateBy(x: 0, y: padding)
            for line in layout {
                let rect = line.typographicBounds.rect.insetBy(dx: 0, dy: -padding)
                // first draw the rectangle, then draw the text
                ctx.fill(Path(rect), with: .color(.yellow))
                ctx.draw(line)
            }
        }
        
        var displayPadding: EdgeInsets {
            .init(top: padding, leading: 0, bottom: padding, trailing: 0)
        }
        
        func sizeThatFits(proposal: ProposedViewSize, text: TextProxy) -> CGSize {
           let textSize =  if let proposedHeight = proposal.height {
                // if there is a proposed height, propose a smaller height than that to account for padding
                text.sizeThatFits(.init(width: proposal.width, height: proposedHeight - padding * 2))
            } else {
                text.sizeThatFits(proposal)
            }
            return .init(width: textSize.width, height: textSize.height + padding * 2)
        }
    }
    

    Usage:

    Text("Lorem ipsum odor amet, consectetuer adipiscing elit. Vehicula nulla ut porta eros taciti vehicula magna. Nulla ultricies tempor facilisi sem sagittis mollis taciti aliquet cubilia. Litora tortor non semper finibus facilisi elementum litora pellentesque platea. Mauris convallis feugiat luctus donec parturient habitasse torquent pharetra. Lacus maximus massa aliquam facilisi per luctus suscipit in.")
        .foregroundStyle(.black)
        .font(.title)
        .lineSpacing(12)
        .textRenderer(HighlightedTextRenderer(padding: 4))
        .border(.red)
        .padding()
    

    enter image description here