LaTeXSwiftUI

April 3, 2026 ยท View on GitHub

โœจ Beautifully rendered LaTeX equations in SwiftUI โ€” powered by MathJax.

Unit Tests

๐Ÿ†• What's New in v2

  • โ™ฟ Accessibility โ€” VoiceOver support via the Speech Rule Engine (SRE)
  • ๐ŸŒ Script Scaling โ€” CJK and non-Latin script support with .script()
  • ๐Ÿ“ Line Spacing โ€” Automatic normalization on iOS 18+ / macOS 15+
  • ๐Ÿ“Š Arrays & Tables โ€” array, matrix, cases, and more with borders and rules
  • ๐Ÿ”ฒ Redacted Placeholder โ€” New redactedOriginal async rendering style
  • ๐Ÿ“ Dynamic Type โ€” Equations scale with system text size settings
  • ๐Ÿ–ผ๏ธ Render to Image โ€” Render LaTeX to UIImage/NSImage without a SwiftUI view
  • ๐Ÿงฉ Generic Environments โ€” \begin{align}, \begin{cases}, etc. work standalone

๐Ÿ“– Contents

โ„น๏ธ About

LaTeXSwiftUI is a package that exposes a LaTeX view capable of parsing and rendering TeX and LaTeX equations containing math-mode macros. It uses the MathJaxSwift package to render equations with MathJax, so the view's capabilities are influenced by MathJax's supported features.

The view renders math-mode equations (inline and block), \text{} within equations, numbered block equations, any \begin{...}...\end{...} environment (including align, gather, cases, array, and more), and provides VoiceOver accessibility via the Speech Rule Engine. It scales equations for non-Latin scripts and normalizes line spacing on iOS 18+ / macOS 15+. You can also render equations directly to UIImage/NSImage without a SwiftUI view. Equations automatically scale with Dynamic Type accessibility settings. It does not render full LaTeX documents or text-mode macros.

Requires Swift 6.0. Supports iOS 15+, macOS 12+, and visionOS 1+. All rendering is performed off the main thread.

๐Ÿ“ฆ Installation

Add the dependency to your package manifest file.

.package(url: "https://github.com/colinc86/LaTeXSwiftUI", from: "2.0.0")

๐Ÿš€ Quick Start

import LaTeXSwiftUI

struct MyView: View {

  var body: some View {
    LaTeX("Hello, $\\LaTeX$!")
  }

}

That's it! The LaTeX view's body is built from Text views, so standard SwiftUI modifiers work out of the box.

LaTeX("Hello, $\\LaTeX$!")
  .fontDesign(.serif)
  .foregroundColor(.blue)

โŒจ๏ธ Usage

๐Ÿ”ค Fonts

The view measures the current font's x-height to correctly size rendered equations. It converts SwiftUI's Font to UIFont/NSFont internally, which currently works with SwiftUI's preferred fonts (largeTitle, title, headline, caption, etc.) and platform font types passed directly.

// SwiftUI preferred fonts
LaTeX("Hello, $\\LaTeX$!")
  .font(.title)

// UIFont/NSFont passed directly
LaTeX("Hello, $\\LaTeX$!")
  .font(UIFont.systemFont(ofSize: 30))

LaTeX("Hello, $\\LaTeX$!")
  .font(UIFont(name: "Avenir", size: 25)!)

โš ๏ธ Custom SwiftUI fonts (.custom(name:size:), .system(size:)) and UIFont/NSFont wrapped in Font() will not size equations correctly. Use preferred fonts or pass platform font types directly.

๐Ÿ”ง Parsing & Input

Parsing Mode

The view can search for top-level equations delimited by the following terminators, or render the entire input as math.

Terminators
$...$
$$...$$
\(...\)
\[...\]
\begin{equation}...\end{equation}
\begin{equation*}...\end{equation*}
\begin{...}...\end{...}
Generic Environments

Any \begin{name}...\end{name} environment is automatically recognized as a block equation โ€” including align, gather, cases, array, matrix, pmatrix, and more. There's no need to wrap them in $$...$$.

LaTeX("The function is \\begin{cases} x & \\text{if } x \\geq 0 \\\\ -x & \\text{if } x < 0 \\end{cases}")
// Only parse equations (default)
LaTeX("Euler's identity is $e^{i\\pi}+1=0$.")
  .parsingMode(.onlyEquations)

// Parse the entire input
LaTeX("\\text{Euler's identity is } e^{i\\pi}+1=0\\text{.}")
  .parsingMode(.all)

Unencode HTML

Input may contain HTML entities such as &lt; which LaTeX won't parse. Use the unencoded modifier to decode them.

LaTeX("$x^2&lt;1$")
  .errorMode(.error)

// Replace "&lt;" with "<"
LaTeX("$x^2&lt;1$")
  .unencoded()

String Formatting

The view renders the following markdown syntax by default.

SyntaxDescription
*...*Italic
**...**Bold
***...***Bold & Italic
~~...~~Strikethrough
`...`Monospaced
[...](...)Links

The reserved LaTeX characters &, %, $, #, _, {, }, ~, ^, and \ are also unescaped when preceded by a backslash. Use ignoreStringFormatting() to disable both markdown rendering and escape replacement.

LaTeX(input)
  .ignoreStringFormatting()

Use processEscapes() to allow \$ for literal dollar signs and \\ for literal backslashes within your input.

๐ŸŽจ Visual & Layout

Image Rendering Mode

Equations can match the surrounding text style or display the original MathJax-rendered colors.

// Match surrounding text (default)
LaTeX("Hello, $\\color{red}\\LaTeX$!")
  .imageRenderingMode(.template)

// Display original rendered colors
LaTeX("Hello, ${\\color{red} \\LaTeX}$!")
  .imageRenderingMode(.original)

Error Mode

Control how the view handles rendering errors.

When rendered mode is used, MathJax loads the noerrors and noundefined packages. In the other modes, errors are either displayed or replaced with the original text.

LaTeX("$\\asdf$")
  .errorMode(.original)  // Show original text

LaTeX("$\\asdf$")
  .errorMode(.error)     // Show error message

LaTeX("$\\asdf$")
  .errorMode(.rendered)  // Show rendered image if available

Block Rendering Mode

Block equations can be rendered centered on their own line (blockViews, the default), forced inline (alwaysInline), or as text with newlines (blockText). Block equations are placed in horizontal scroll views when they exceed the view width.

LaTeX("The quadratic formula is $$x=\\frac{-b\\pm\\sqrt{b^2-4ac}}{2a}$$ and it has zeros at the roots of $f(x)=ax^2+bx+c$.")
  .blockMode(.blockViews)

Divider()

LaTeX("The quadratic formula is $$x=\\frac{-b\\pm\\sqrt{b^2-4ac}}{2a}$$ and it has zeros at the roots of $f(x)=ax^2+bx+c$.")
  .blockMode(.alwaysInline)

Divider()

LaTeX("The quadratic formula is $$x=\\frac{-b\\pm\\sqrt{b^2-4ac}}{2a}$$ and it has zeros at the roots of $f(x)=ax^2+bx+c$.")
  .blockMode(.blockText)

Numbered Block Equations

The view supports simple numbering of block equations when using blockViews mode.

ModifierDescription
.equationNumberMode(_:)Position: .left, .right, or .none (default)
.equationNumberStart(_:)Starting number (default: 1)
.equationNumberOffset(_:)Left or right offset in points
.formatEquationNumber(_:)Custom formatting closure (Int) -> String
LaTeX("$$E = mc^2$$")
  .equationNumberMode(.right)
  .equationNumberOffset(10)
  .padding([.bottom])

LaTeX("$$E = mc^2$$ $$E = mc^2$$")
  .equationNumberMode(.right)
  .equationNumberOffset(10)
  .equationNumberStart(2)
  .formatEquationNumber { n in
    return "~[\(n)]~"
  }

Rendering Style

All rendering (MathJax conversion and SVG rasterization) is performed off the main thread. Choose a rendering style to control loading behavior.

StyleAsyncDescription
emptyYesThe view remains empty until rendering completes.
originalYesThe view displays the input text until rendering completes.
redactedOriginalYesThe view displays a redacted placeholder until rendering completes. ๐Ÿ†•
progressYesThe view displays a progress indicator until rendering completes.
waitNo(default) The view blocks until rendering completes.

When using an asynchronous style, use renderingAnimation to animate the transition.

LaTeX(input)
  .renderingStyle(.original)
  .renderingAnimation(.easeIn)

Note: The LaTeX view automatically re-renders when its input string changes, so you can bind it to @State variables without needing .id():

@State var text: String = ""

var body: some View {
    VStack {
        TextField("LaTeX", text: $text)
        LaTeX("$\(text)$")
    }
}

Script Mode

๐Ÿ†• When displaying equations inline with non-Latin scripts such as Korean, Japanese, or Chinese, equations may appear undersized or misaligned. The script modifier adjusts equation scaling to match the surrounding text.

// Korean
LaTeX("๋ฐฉ์ •์‹ $x^2 + y^2 = z^2$ ์€ ์ž˜ ์•Œ๋ ค์ ธ ์žˆ์Šต๋‹ˆ๋‹ค.")
  .script(.cjk)

// Japanese
LaTeX("ๆ–น็จ‹ๅผ $E = mc^2$ ใฏๆœ‰ๅใงใ™ใ€‚")
  .script(.cjk)

// Custom scale factor
LaTeX("Scaled equation: $\\int_$0^{1}$ x^2 dx$")
  .script(.custom(1.3))
ScriptDescription
.latin(default) Uses the font's x-height. Suitable for Latin, Cyrillic, and similar scripts.
.cjkUses the font's cap-height. Suitable for Korean, Japanese, and Chinese.
.custom(CGFloat)Multiplies the font's x-height by the given factor.

Line Spacing

๐Ÿ†• On iOS 18+ / macOS 15+, the view automatically normalizes line spacing when inline equations cause uneven line gaps. This uses a custom TextRenderer and requires no configuration.

Arrays & Tables

๐Ÿ†• The view renders LaTeX array and table environments including array, matrix, pmatrix, vmatrix, and cases. Horizontal rules (\hline) and vertical column borders are supported.

LaTeX("$$\\begin{pmatrix} a & b \\\\ c & d \\end{pmatrix}$$")

LaTeX("$$\\begin{cases} x & \\text{if } x \\geq 0 \\\\ -x & \\text{if } x < 0 \\end{cases}$$")

โ™ฟ Accessibility

๐Ÿ†• Rendered equations are images that need accessibility labels for VoiceOver. By default, LaTeXSwiftUI uses MathJax's Speech Rule Engine (SRE) to generate natural language descriptions automatically.

// Default (.sre) โ€” VoiceOver reads "x squared plus y squared equals z squared"
LaTeX("$x^2 + y^2 = z^2$")

// Use the raw TeX input as the label
LaTeX("$x^2 + y^2 = z^2$")
  .imageAccessibility(.input)

// No accessibility label
LaTeX("$x^2 + y^2 = z^2$")
  .imageAccessibility(.none)

// Custom label
LaTeX("$E = mc^2$")
  .imageAccessibility(.custom("Einstein's mass-energy equivalence"))
ModeDescription
.sre(default) Uses the Speech Rule Engine to generate natural language. Falls back to raw TeX on failure.
.inputUses the raw TeX input as the accessibility label.
.noneNo accessibility label (default SwiftUI behavior).
.custom(String)Uses a custom string as the accessibility label.

๐Ÿ–ผ๏ธ Rendering to Images

๐Ÿ†• You can render LaTeX equations directly to UIImage (iOS/visionOS) or NSImage (macOS) without using the LaTeX SwiftUI view. This is useful for UIKit integration, image export, or custom rendering pipelines.

// Render all equations to images
let images = LaTeX.renderToImages("$x^2 + y^2 = z^2$")

// With custom options
let images = LaTeX.renderToImages(
  "Euler's identity: $e^{i\\pi}+1=0$ and $\\int_$0^{1}$ x\\,dx$",
  displayScale: 3.0,
  processEscapes: true
)

// Each equation produces one image
for image in images {
  imageView.image = image
}

โšก Performance & Caching

All rendering is performed off the main thread. The package caches both SVG data from MathJax and the rasterized images. You can control the caches directly.

// Clear the SVG data cache
LaTeX.dataCache.removeAllObjects()

// Clear the rendered image cache
LaTeX.imageCache.removeAllObjects()

Preloading

SVGs and images are rendered on demand, but you can preload them to minimize lag when the view appears. Call preload last in the modifier chain.

VStack {
  ForEach(expressions, id: \.self) { expression in
    LaTeX(expression)
      .font(.caption2)
      .foregroundColor(.green)
      .unencoded()
      .errorMode(.error)
      .processEscapes()
      .preload()
  }
}