SwiftMath Documentation

repository·main·Indexed 18 days ago

https://github.com/mgriebling/swiftmath

A high-performance, native Swift library for rendering LaTeX mathematical equations in iOS 11+ and macOS 12+ applications. It provides a native alternative to web-based engines like MathJax and KaTeX, utilizing the MTMathUILabel class to typeset formulae without requiring UIWebView or JavaScript. The library supports standard LaTeX delimiters, automatic line wrapping via interatom line breaking, and various matrix and alignment environments.

Tokens
6.7K
Snippets
25
Records
39
Agent score
63%

What's inside SwiftMath

  1. What is SwiftMath

    main
    SwiftMath is a native Swift implementation of iosMath designed for displaying beautifully rendered math equations in iOS and macOS applications. It typesets LaTeX formulae into a UILabel equivalent class, following the same typesetting rules as LaTeX. Unlike web-based solutions like MathJax or KaTeX, SwiftMath does not require a UIWebView or JavaScript, making it significantly faster and more efficient for native applications.
  2. Complex atom inline behavior

    main

    SwiftMath supports intelligent inline layout for complex mathematical atoms. Rather than forcing a line break immediately when a complex atom is encountered, the typesetter uses a shouldBreakBeforeDisplay() helper to check if the atom can fit within the remaining maxWidth.

    Supported Complex Atoms

    The following atom types are designed to stay inline whenever possible:

    • Fractions: .fraction cases check width before breaking.
    • Radicals: .radical cases check width before breaking.
    • Large Operators: Checks both height and width.
    • Delimiters: Handles nested and multiple delimiters.
    • Colored Expressions: Propagates maxWidth through .color, .textcolor, and .colorBox cases.
    • Matrices: Small matrices are kept inline; they only break when they exceed the width constraint.
    • Scripted Atoms: Exponents and subscripts are calculated using estimateAtomWidthWithScripts() to ensure they participate in interatom breaking decisions without losing correct positioning.
  3. Performance optimization: Early Exit

    main

    To improve rendering speed, especially for short expressions that typically fit on a single line, SwiftMath uses an Early Exit Optimization.

    When the typesetter determines that the remaining content is highly likely to fit within the maxWidth, it sets a remainingContentFits flag. Once this flag is active, all subsequent expensive width-checking and line-breaking calculations are skipped (the "fast path").

    Optimization Heuristics

    The flag is set based on the following conservative estimates:

    • If current usage is < 60% of maxWidth and there are ≤ 5 atoms remaining.
    • If current usage is < 75% of maxWidth, the system uses estimateRemainingAtomsWidth() (a heuristic based on character count × average width with a 1.5× safety margin) to project the total width.

    The flag is reset immediately if a line break is actually performed.

  4. Understand how SwiftMath handles automatic line breaking

    main

    SwiftMath uses a two-tier breaking system to manage how mathematical expressions wrap within a given width. The system is designed to be aesthetically pleasing by using a Break Quality Scoring mechanism. Instead of breaking at arbitrary points, the typesetter calculates a penalty for potential break points and chooses the one with the lowest penalty.

    Break Penalty Scoring

    When the current line width is slightly exceeded (between 100% and 120% of maxWidth), the typesetter looks ahead up to 3 atoms to find the best break point based on these scores:

    PenaltyBreak Point TypeExample
    0 (Best)Binary operators, relations, or punctuation+, -, ×, ÷, =, <, >, ,
    10 (Good)Ordinary atomsVariables, numbers
    100 (Bad)BracketsAfter an open bracket ( or before a close bracket )
    150 (Worst)Unary or large operatorsUnary minus, large integral signs

    This look-ahead logic ensures that expressions break at natural, readable points rather than in the middle of a term.

  5. Configure LaTeX math delimiters

    main

    SwiftMath automatically detects and handles standard LaTeX delimiters to switch between Inline (compact) and Display (large) math modes.

    Inline Math (Text Style)

    Use these for math within a sentence. They render compactly.

    • Dollar signs: $E = mc^2$
    • Parentheses: \(\sum_{i=1}^{n} x_i\)
    • Cases environment: \(\begin{cases} x + y = 5 \\ 2x - y = 1 \end{cases}\)

    Display Math (Display Style)

    Use these for standalone equations. They render with larger operators and limits.

    • Double dollar signs: $$ \int_{0}^{\infty} e^{-x^2} dx = \frac{\sqrt{\pi}}{2} $$
    • Square brackets: \[\sum_{k=1}^{n} k^2 = \frac{n(n+1)(2n+1)}{6}\]
    • Equation environment: \begin{equation} x^2 + y^2 = z^2 \end{equation}
    • Cases environment: \begin{cases} x + y = 5 \\ 2x - y = 1 \end{cases}

    Note: Equations without explicit delimiters default to display mode.

    // Example of programmatic style detection
    let (mathList, style) = MTMathListBuilder.buildWithStyle(fromString: "\\[x^2 + y^2 = z^2\\]")
    // style will be .display for \[...\] or $$...$$
    // style will be .text for \(...\) or $...$
  6. Understand SwiftMath automatic line breaking

    main

    SwiftMath supports automatic line breaking (multiline display) for mathematical equations using a two-tier system. This allows complex expressions to wrap naturally within a specified maxWidth while preserving mathematical structure and semantic meaning.

    The Two-Tier Breaking System

    1. Interatom Line Breaking (Primary): This is the main mechanism that checks before adding an atom to a line. It calculates if the currentLineWidth + atomWidth + interElementSpacing exceeds maxWidth. If it does, it flushes the current line and starts a new one. This applies to:

      • .ordinary (variables, text, symbols)
      • .binaryOperator (+, -, ×, ÷)
      • .relation (=, <, >, , )
      • .open and .close brackets
      • .placeholder and .punctuation
    2. Universal Line Breaking (Fallback): A fallback mechanism used for very long single text atoms or atoms without scripts. It uses Core Text's CTTypesetterSuggestLineBreak for Unicode-aware breaking and protects numbers (like 3.14) from being split.

    Supported Mathematical Structures

    SwiftMath intelligently handles wrapping for various complex elements:

    • Fractions & Radicals: Stay inline if they fit within the width constraint.
    • Large Operators: Operators like , , , and lim stay inline, with intelligent height checking (breaks if height > fontSize * 2.5).
    • Delimited Expressions: \left(...\right) structures stay inline, and the maxWidth is propagated to the inner content for proper wrapping.
    • Colored Expressions: Sections using .color, .textcolor, or .colorBox wrap their inner content correctly.
    • Matrices/Tables: Small matrices stay inline if they fit.
    • Atoms with Scripts: Superscripts and subscripts participate in width-based breaking.
  7. Understand the limitations of SwiftMath's LaTeX implementation

    main

    SwiftMath is not a complete implementation of LaTeX math mode. When using it, be aware that certain commands are currently unsupported.

    Currently missing features include:

    • \middle delimiters (used between \left and \right).
    • Certain fine spacing commands: \: , \;, and \!.

    Note: The \, spacing command is supported and works as expected.

    For a comprehensive list of supported and unsupported features, refer to the MISSING_FEATURES.md file in the repository.

  8. Capabilities of SwiftMath multiline line breaking

    main

    SwiftMath's multiline implementation provides intelligent line breaking for complex mathematical expressions. It uses a two-tier system to ensure that expressions flow naturally within width constraints while maintaining aesthetic quality (e.g., breaking after operators and avoiding awkward breaks).

    Supported Expression Types

    SwiftMath supports intelligent inline layout and width-based breaking for:

    • Simple equations: Expressions using standard operators.
    • Mixed content: Combining text and math seamlessly.
    • Long sequences: Variables, numbers, and long mathematical sequences.
    • Fractions: Inline fractions that respect width constraints.
    • Radicals: Square roots and other radicals.
    • Large operators: Summation ($\sum$), integrals ($\int$), and other large operators.
    • Delimited expressions: Content inside parentheses or brackets (e.g., (a+b)).
    • Colored expressions: Color-coded sections that respect width constraints.
    • Matrices and tables: Small matrices/tables that stay inline with surrounding content.
    • Scripted atoms: Superscripts and subscripts (e.g., a^{2}).
    • Mixed complex expressions: Combinations of the above types.

    Layout Behavior

    • Width Constraint Propagation: Width constraints are propagated down to nested content to ensure deep expressions break correctly.
    • Dynamic Line Height: The system calculates line height based on actual content; for example, tall fractions automatically receive more vertical space, while regular content remains compact.
  9. How dynamic line height works in SwiftMath

    main

    To prevent overlapping or excessive gaps, SwiftMath implements Dynamic Line Height. Instead of using a fixed multiplier of the font size, the typesetter calculates the height of each line based on its specific content.

    Calculation Logic

    For every line, the typesetter:

    1. Tracks the currentLineStartIndex to identify all displays belonging to that line.
    2. Iterates through the displays to find the maximum ascent and maximum descent.
    3. Calculates the total height using the formula: total height = maxAscent + maxDescent + minimumLineSpacing

    Spacing Rules

    • minimumLineSpacing: Set to 20% of the fontSize to provide breathing room.
    • Minimum Threshold: The system ensures at least fontSize × 1.2 spacing is used to maintain readability.

    This approach ensures that tall elements like fractions, radicals, or large operators receive appropriate vertical space, while standard lines of text remain compact.

  10. Use SwiftMath in SwiftUI

    main

    To use MTMathUILabel in SwiftUI, you must wrap it in a UIViewRepresentable (for iOS/visionOS) or NSViewRepresentable (for macOS).

    Below is a standard implementation for iOS/visionOS that handles font management, text alignment, and automatic line wrapping via sizeThatFits.

    import SwiftUI
    import SwiftMath
    
    struct MathView: UIViewRepresentable {
        var equation: String
        var font: MathFont = .latinModernFont
        var textAlignment: MTTextAlignment = .center
        var fontSize: CGFloat = 30
        var labelMode: MTMathUILabelMode = .text
        var insets: MTEdgeInsets = MTEdgeInsets()
    
        func makeUIView(context: Context) -> MTMathUILabel {
            let view = MTMathUILabel()
            view.setContentHuggingPriority(.required, for: .vertical)
            view.setContentCompressionResistancePriority(.required, for: .vertical)
            return view
        }
    
        func updateUIView(_ view: MTMathUILabel, context: Context) {
            view.latex = equation
            let font = MTFontManager().font(withName: font.rawValue, size: fontSize)
            font?.fallbackFont = UIFont.systemFont(ofSize: fontSize)
            view.font = font
            view.textAlignment = textAlignment
            view.labelMode = labelMode
            view.textColor = MTColor(Color.primary)
            view.contentInsets = insets
            view.invalidateIntrinsicContentSize()
        }
    
        func sizeThatFits(_ proposal: ProposedViewSize, uiView: MTMathUILabel, context: Context) -> CGSize? {
            if let width = proposal.width, width.isFinite, width > 0 {
                uiView.preferredMaxLayoutWidth = width
                let size = uiView.sizeThatFits(CGSize(width: width, height: .greatestFiniteMagnitude))
                return size
            }
            return nil
        }
    }
  11. Set a fallback font for Unicode text

    main

    By default, math fonts only support a limited set of characters (Latin, Greek, etc.). To display characters like Chinese, Japanese, Korean, or Emojis within \text{} commands, you must configure a fallbackFont on your MTFont instance.

    let mathFont = MTFontManager().font(withName: MathFont.latinModernFont.rawValue, size: 30)
    
    #if os(iOS) || os(visionOS)
    let systemFont = UIFont.systemFont(ofSize: 30)
    mathFont?.fallbackFont = CTFontCreateWithName(systemFont.fontName as CFString, 30, nil)
    #elseif os(macOS)
    let systemFont = NSFont.systemFont(ofSize: 30)
    mathFont?.fallbackFont = CTFontCreateWithName(systemFont.fontName as CFString, 30, nil)
    #endif
    
    label.font = mathFont
    label.latex = "\\text{Hello 世界 🌍}"
    let mathFont = MTFontManager().font(withName: MathFont.latinModernFont.rawValue, size: 30)
    
    #if os(iOS) || os(visionOS)
    let systemFont = UIFont.systemFont(ofSize: 30)
    mathFont?.fallbackFont = CTFontCreateWithName(systemFont.fontName as CFString, 30, nil)
    #elseif os(macOS)
    let systemFont = NSFont.systemFont(ofSize: 30)
    mathFont?.fallbackFont = CTFontCreateWithName(systemFont.fontName as CFString, 30, nil)
    #endif
    
    label.font = mathFont
    label.latex = "\\text{Hello 世界 🌍}"
  12. Split long equations

    main

    For long equations that exceed the line width, use line breaks (\\) and manual spacing/indentation (like \quad) to maintain readability.

    ```latex\n\frak Q(\lambda,\hat{\lambda}) =
    -\frac{1}{2} \mathbb P(O \mid \lambda ) \sum_s \sum_m \sum_t \gamma_m^{(s)} (t) +\\
    \quad \left( \log(2 \pi ) + \log \left| \cal C_m^{(s)} \right| + 
    \left( o_t - \hat{\mu}_m^{(s)} \right) ^T \cal C_m^{(s)-1} \right) \n```