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


    Instead of hardcoding the background color in the text renderer, you can put it in a TextAttribute.

    struct TextBackgroundAttribute: TextAttribute {
        let background: Color
    }
    
    extension Text {
        // convenient modifier for applying the attribute
        func textBackground(_ color: Color) -> Text {
            customAttribute(TextBackgroundAttribute(background: color))
        }
    }
    
    // modify the draw method to read the attribute:
    func draw(layout: Text.Layout, in ctx: inout GraphicsContext) {
        ctx.translateBy(x: 0, y: padding)
        for line in layout {
            for run in line {
                if let background = run[TextBackgroundAttribute.self]?.background {
                    let rect = run.typographicBounds.rect.insetBy(dx: 0, dy: -padding)
                    ctx.fill(Path(rect), with: .color(background))
                }
                ctx.draw(run)
            }
        }
    }