Clean slate
9
.editorconfig
Normal file
@@ -0,0 +1,9 @@
|
||||
# editorconfig.org
|
||||
|
||||
root = true
|
||||
|
||||
[*]
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
trim_trailing_whitespace = true
|
||||
insert_final_newline = true
|
||||
48
Makefile
Normal file
@@ -0,0 +1,48 @@
|
||||
IOS_VERSION = 26.0
|
||||
TVOS_VERSION = 26.0
|
||||
WATCHOS_VERSION = 26.0
|
||||
VISIONOS_VERSION = 26.0
|
||||
|
||||
PLATFORM_IOS = iOS Simulator,id=$(call udid_for,iOS $(IOS_VERSION),iPhone \d\+ Pro [^M])
|
||||
PLATFORM_MACOS = macOS
|
||||
PLATFORM_TVOS = tvOS Simulator,id=$(call udid_for,tvOS $(TVOS_VERSION),TV)
|
||||
PLATFORM_WATCHOS = watchOS Simulator,id=$(call udid_for,watchOS $(WATCHOS_VERSION),Watch)
|
||||
PLATFORM_VISIONOS = visionOS Simulator,id=$(call udid_for,visionOS $(VISIONOS_VERSION),Vision)
|
||||
|
||||
default: test
|
||||
|
||||
test: test-macos test-ios test-tvos test-watchos test-visionos
|
||||
|
||||
test-macos:
|
||||
@echo "Testing macOS..."
|
||||
xcodebuild test -scheme swiftui-math -destination platform="$(PLATFORM_MACOS)"
|
||||
|
||||
test-ios:
|
||||
@echo "Testing iOS $(IOS_VERSION)..."
|
||||
xcodebuild test -scheme swiftui-math -destination platform="$(PLATFORM_IOS)"
|
||||
|
||||
test-tvos:
|
||||
@echo "Testing tvOS $(TVOS_VERSION)..."
|
||||
xcodebuild test -scheme swiftui-math -destination platform="$(PLATFORM_TVOS)"
|
||||
|
||||
test-watchos:
|
||||
@echo "Testing watchOS $(WATCHOS_VERSION)..."
|
||||
xcodebuild test -scheme swiftui-math -destination platform="$(PLATFORM_WATCHOS)"
|
||||
|
||||
test-visionos:
|
||||
@echo "Testing visionOS $(PLATFORM_VISIONOS)..."
|
||||
xcodebuild test -scheme swiftui-math -destination platform="$(PLATFORM_VISIONOS)"
|
||||
|
||||
format:
|
||||
swift format \
|
||||
--ignore-unparsable-files \
|
||||
--in-place \
|
||||
--parallel \
|
||||
--recursive \
|
||||
./Package.swift ./Sources ./Tests
|
||||
|
||||
.PHONY: format test
|
||||
|
||||
define udid_for
|
||||
$(shell xcrun simctl list devices available '$(1)' | grep '$(2)' | sort -r | head -1 | awk -F '[()]' '{ print $$(NF-3) }')
|
||||
endef
|
||||
@@ -1,27 +1,29 @@
|
||||
// swift-tools-version: 5.7
|
||||
// swift-tools-version: 6.0
|
||||
|
||||
import PackageDescription
|
||||
|
||||
let package = Package(
|
||||
name: "swiftui-math",
|
||||
defaultLocalization: "en",
|
||||
platforms: [.iOS("11.0"), .macOS("12.0")],
|
||||
products: [
|
||||
.library(
|
||||
name: "SwiftUIMath",
|
||||
targets: ["SwiftUIMath"]),
|
||||
],
|
||||
dependencies: [
|
||||
],
|
||||
targets: [
|
||||
.target(
|
||||
name: "SwiftUIMath",
|
||||
dependencies: [],
|
||||
resources: [
|
||||
.copy("mathFonts.bundle")
|
||||
]),
|
||||
.testTarget(
|
||||
name: "SwiftUIMathTests",
|
||||
dependencies: ["SwiftUIMath"]),
|
||||
]
|
||||
name: "swiftui-math",
|
||||
platforms: [
|
||||
.macOS(.v12),
|
||||
.iOS(.v15),
|
||||
.tvOS(.v15),
|
||||
.watchOS(.v8),
|
||||
.visionOS(.v1),
|
||||
],
|
||||
products: [
|
||||
.library(name: "SwiftUIMath", targets: ["SwiftUIMath"])
|
||||
],
|
||||
dependencies: [],
|
||||
targets: [
|
||||
.target(
|
||||
name: "SwiftUIMath",
|
||||
dependencies: [],
|
||||
resources: [.copy("mathFonts.bundle")]
|
||||
),
|
||||
.testTarget(
|
||||
name: "SwiftUIMathTests",
|
||||
dependencies: ["SwiftUIMath"]
|
||||
),
|
||||
]
|
||||
)
|
||||
|
||||
682
README.md
@@ -1,681 +1 @@
|
||||
# SwiftMath
|
||||
|
||||
`SwiftMath` provides a full Swift implementation of [iosMath](https://travis-ci.org/kostub/iosMath)
|
||||
for displaying beautifully rendered math equations in iOS and MacOS applications. It typesets formulae written
|
||||
using LaTeX in a `UILabel` equivalent class. It uses the same typesetting rules as LaTeX and
|
||||
so the equations are rendered exactly as LaTeX would render them.
|
||||
|
||||
Please also check out [SwiftMathDemo](https://github.com/mgriebling/SwiftMathDemo.git) for examples of how to use `SwiftMath`
|
||||
from SwiftUI.
|
||||
|
||||
`SwiftMath` is similar to [MathJax](https://www.mathjax.org) or
|
||||
[KaTeX](https://github.com/Khan/KaTeX) for the web but for native iOS or MacOS
|
||||
applications without having to use a `UIWebView` and Javascript. More
|
||||
importantly, it is significantly faster than using a `UIWebView`.
|
||||
|
||||
`SwiftMath` is a Swift translation of the latest `iosMath` v0.9.5 release but includes bug fixes
|
||||
and enhancements like a new \lbar (lambda bar) character and cyrillic alphabet support.
|
||||
The original `iosMath` test suites have also been translated to Swift and run without errors.
|
||||
Note: Error test conditions are ignored to avoid tagging everything with silly `throw`s.
|
||||
Please let me know of any bugs or bug fixes that you find.
|
||||
|
||||
`SwiftMath` prepackages everything needed for direct access via the Swift Package Manager.
|
||||
|
||||
## Examples
|
||||
Here are screenshots of some formulae that were rendered with this library:
|
||||
|
||||
```LaTeX
|
||||
x = \frac{-b \pm \sqrt{b^2-4ac}}{2a}
|
||||
```
|
||||
|
||||

|
||||

|
||||
|
||||
```LaTeX
|
||||
f(x) = \int\limits_{-\infty}^\infty\!\hat f(\xi)\,e^{2 \pi i \xi x}\,\mathrm{d}\xi
|
||||
```
|
||||
|
||||

|
||||

|
||||
|
||||
```LaTeX
|
||||
\frac{1}{n}\sum_{i=1}^{n}x_i \geq \sqrt[n]{\prod_{i=1}^{n}x_i}
|
||||
```
|
||||
|
||||

|
||||

|
||||
|
||||
```LaTex
|
||||
\frac{1}{\left(\sqrt{\phi \sqrt{5}}-\phi\\right) e^{\frac25 \pi}}
|
||||
= 1+\frac{e^{-2\pi}} {1 +\frac{e^{-4\pi}} {1+\frac{e^{-6\pi}} {1+\frac{e^{-8\pi}} {1+\cdots} } } }
|
||||
```
|
||||
|
||||

|
||||

|
||||
|
||||
More examples are included in [EXAMPLES](EXAMPLES.md)
|
||||
|
||||
## Fonts
|
||||
Here are previews of the included fonts:
|
||||
|
||||

|
||||

|
||||
|
||||
## Requirements
|
||||
`SwiftMath` works on iOS 11+ or MacOS 12+. It depends
|
||||
on the following Apple frameworks:
|
||||
|
||||
* Foundation.framework
|
||||
* CoreGraphics.framework
|
||||
* QuartzCore.framework
|
||||
* CoreText.framework
|
||||
|
||||
Additionally for iOS it requires:
|
||||
* UIKit.framework
|
||||
|
||||
Additionally for MacOS it requires:
|
||||
* AppKit.framework
|
||||
|
||||
## Installation
|
||||
|
||||
### Swift Package
|
||||
|
||||
`SwiftMath` is available from [SwiftMath](https://github.com/mgriebling/SwiftMath.git).
|
||||
To use it in your code, just add the https://github.com/mgriebling/SwiftMath.git path to
|
||||
XCode's package manager.
|
||||
|
||||
## Usage
|
||||
|
||||
The library provides a class `MTMathUILabel` which is a `UIView` that
|
||||
supports rendering math equations. To display an equation simply create
|
||||
an `MTMathUILabel` as follows:
|
||||
|
||||
```swift
|
||||
|
||||
import SwiftMath
|
||||
|
||||
let label = MTMathUILabel()
|
||||
label.latex = "x = \\frac{-b \\pm \\sqrt{b^2-4ac}}{2a}"
|
||||
|
||||
```
|
||||
Adding `MTMathUILabel` as a sub-view of your `UIView` will render the
|
||||
quadratic formula example shown above.
|
||||
|
||||
The following code creates a SwiftUI component called `MathView` encapsulating the MTMathUILabel:
|
||||
|
||||
```swift
|
||||
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? {
|
||||
// Enable line wrapping by passing proposed width to the label
|
||||
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
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
For code that works with SwiftUI running natively under MacOS use the following:
|
||||
|
||||
```swift
|
||||
import SwiftUI
|
||||
import SwiftMath
|
||||
|
||||
struct MathView: NSViewRepresentable {
|
||||
var equation: String
|
||||
var font: MathFont = .latinModernFont
|
||||
var textAlignment: MTTextAlignment = .center
|
||||
var fontSize: CGFloat = 30
|
||||
var labelMode: MTMathUILabelMode = .text
|
||||
var insets: MTEdgeInsets = MTEdgeInsets()
|
||||
|
||||
func makeNSView(context: Context) -> MTMathUILabel {
|
||||
let view = MTMathUILabel()
|
||||
view.setContentHuggingPriority(.required, for: .vertical)
|
||||
view.setContentCompressionResistancePriority(.required, for: .vertical)
|
||||
return view
|
||||
}
|
||||
|
||||
func updateNSView(_ view: MTMathUILabel, context: Context) {
|
||||
view.latex = equation
|
||||
let font = MTFontManager().font(withName: font.rawValue, size: fontSize)
|
||||
font?.fallbackFont = NSFont.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, nsView: MTMathUILabel, context: Context) -> CGSize? {
|
||||
// Enable line wrapping by passing proposed width to the label
|
||||
if let width = proposal.width, width.isFinite, width > 0 {
|
||||
nsView.preferredMaxLayoutWidth = width
|
||||
let size = nsView.sizeThatFits(CGSize(width: width, height: .greatestFiniteMagnitude))
|
||||
return size
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Automatic Line Wrapping
|
||||
|
||||
`SwiftMath` supports automatic line wrapping (multiline display) for mathematical content. The implementation uses **interatom line breaking** which breaks equations at atom boundaries (between mathematical elements) rather than within them, preserving the semantic structure of the mathematics.
|
||||
|
||||
#### Using Line Wrapping with UIKit/AppKit
|
||||
|
||||
For direct `MTMathUILabel` usage, set the `preferredMaxLayoutWidth` property:
|
||||
|
||||
```swift
|
||||
let label = MTMathUILabel()
|
||||
label.latex = "\\text{Calculer le discriminant }\\Delta=b^{2}-4ac\\text{ avec }a=1\\text{, }b=-1\\text{, }c=-5"
|
||||
label.font = MTFontManager.fontManager.defaultFont
|
||||
|
||||
// Enable line wrapping by setting a maximum width
|
||||
label.preferredMaxLayoutWidth = 235
|
||||
```
|
||||
|
||||
You can also use `sizeThatFits` to calculate the size with a width constraint:
|
||||
|
||||
```swift
|
||||
let constrainedSize = label.sizeThatFits(CGSize(width: 235, height: .greatestFiniteMagnitude))
|
||||
```
|
||||
|
||||
#### Using Line Wrapping with SwiftUI
|
||||
|
||||
The `MathView` examples above include `sizeThatFits()` which automatically enables line wrapping when SwiftUI proposes a width constraint. No additional configuration is needed:
|
||||
|
||||
```swift
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
MathView(
|
||||
equation: "\\text{Calculer le discriminant }\\Delta=b^{2}-4ac\\text{ avec }a=1\\text{, }b=-1\\text{, }c=-5",
|
||||
fontSize: 17,
|
||||
labelMode: .text
|
||||
)
|
||||
}
|
||||
.frame(maxWidth: 235) // The equation will break across multiple lines
|
||||
```
|
||||
|
||||
#### Line Wrapping Behavior and Capabilities
|
||||
|
||||
SwiftMath implements **two complementary line breaking mechanisms**:
|
||||
|
||||
##### 1. Interatom Line Breaking (Primary)
|
||||
Breaks equations **between atoms** (mathematical elements) when content exceeds the width constraint. This is the preferred method as it maintains semantic integrity.
|
||||
|
||||
##### 2. Universal Line Breaking (Fallback)
|
||||
For very long text within single atoms, breaks at Unicode word boundaries using Core Text with number protection (prevents splitting numbers like "3.14").
|
||||
|
||||
#### Fully Supported Cases
|
||||
|
||||
These atom types work perfectly with interatom line breaking:
|
||||
|
||||
**✅ Variables and ordinary text:**
|
||||
```swift
|
||||
label.latex = "a b c d e f g h i j k l m n o p"
|
||||
label.preferredMaxLayoutWidth = 150
|
||||
// Breaks between individual variables at natural boundaries
|
||||
```
|
||||
|
||||
**✅ Binary operators (+, -, ×, ÷):**
|
||||
```swift
|
||||
label.latex = "a+b+c+d+e+f+g+h"
|
||||
label.preferredMaxLayoutWidth = 100
|
||||
// Breaks cleanly: "a+b+c+d+"
|
||||
// "e+f+g+h"
|
||||
```
|
||||
|
||||
**✅ Relations (=, <, >, ≤, ≥, etc.):**
|
||||
```swift
|
||||
label.latex = "a=1, b=2, c=3, d=4, e=5"
|
||||
label.preferredMaxLayoutWidth = 120
|
||||
// Breaks after commas and operators
|
||||
```
|
||||
|
||||
**✅ Mixed text and simple math:**
|
||||
```swift
|
||||
label.latex = "\\text{Calculer }\\Delta=b^{2}-4ac\\text{ avec }a=1\\text{, }b=-1"
|
||||
label.preferredMaxLayoutWidth = 200
|
||||
// Breaks between text and math atoms naturally
|
||||
```
|
||||
|
||||
**✅ Punctuation (commas, periods):**
|
||||
```swift
|
||||
label.latex = "\\text{First, second, third, fourth, fifth}"
|
||||
label.preferredMaxLayoutWidth = 150
|
||||
// Breaks at commas and spaces
|
||||
```
|
||||
|
||||
**✅ Brackets and parentheses (simple):**
|
||||
```swift
|
||||
label.latex = "(a+b)+(c+d)+(e+f)"
|
||||
label.preferredMaxLayoutWidth = 120
|
||||
// Breaks between parenthesized groups
|
||||
```
|
||||
|
||||
**✅ Greek letters and symbols:**
|
||||
```swift
|
||||
label.latex = "\\alpha+\\beta+\\gamma+\\delta+\\epsilon+\\zeta"
|
||||
label.preferredMaxLayoutWidth = 150
|
||||
// Breaks between Greek letters
|
||||
```
|
||||
|
||||
**✅ Fractions (NEW!):**
|
||||
```swift
|
||||
label.latex = "a+\\frac{1}{2}+b+\\frac{3}{4}+c"
|
||||
label.preferredMaxLayoutWidth = 150
|
||||
// Fractions stay inline if they fit, break to new line only when needed
|
||||
// Example: "a + ½ + b" stays on one line if it fits
|
||||
```
|
||||
|
||||
**✅ Radicals/Square roots (NEW!):**
|
||||
```swift
|
||||
label.latex = "x+\\sqrt{2}+y+\\sqrt{3}+z"
|
||||
label.preferredMaxLayoutWidth = 150
|
||||
// Radicals stay inline if they fit, break to new line only when needed
|
||||
// Example: "x + √2 + y" stays on one line if it fits
|
||||
```
|
||||
|
||||
**✅ Mixed fractions and radicals (NEW!):**
|
||||
```swift
|
||||
label.latex = "a+\\frac{1}{2}+\\sqrt{3}+b"
|
||||
label.preferredMaxLayoutWidth = 200
|
||||
// Intelligently breaks between complex mathematical elements
|
||||
```
|
||||
|
||||
#### Limited Support Cases
|
||||
|
||||
These cases work but with some constraints:
|
||||
|
||||
**⚠️ Atoms with superscripts/subscripts:**
|
||||
```swift
|
||||
label.latex = "a^{2}+b^{2}+c^{2}+d^{2}+e^{2}"
|
||||
label.preferredMaxLayoutWidth = 150
|
||||
// Works, but uses fallback breaking mechanism
|
||||
// May not break at the most optimal positions
|
||||
```
|
||||
**Note**: Scripted atoms (with superscripts/subscripts) trigger the universal breaking mechanism which breaks within accumulated text rather than at atom boundaries. This still works but may not be as clean as pure interatom breaking.
|
||||
|
||||
**⚠️ Very long single text atoms:**
|
||||
```swift
|
||||
label.latex = "\\text{This is an extremely long piece of text within a single text command}"
|
||||
label.preferredMaxLayoutWidth = 200
|
||||
// Uses Unicode word boundary breaking with Core Text
|
||||
// Protects numbers from being split (e.g., "3.14" stays together)
|
||||
```
|
||||
|
||||
#### Remaining Unsupported Cases
|
||||
|
||||
These atom types still force line breaks (not yet optimized):
|
||||
|
||||
**⚠️ Large operators (∑, ∫, ∏, lim):**
|
||||
```swift
|
||||
label.latex = "\\sum_{i=1}^{n} x_i + \\int_{0}^{1} f(x)dx"
|
||||
// Each operator forces a new line
|
||||
```
|
||||
|
||||
**⚠️ Matrices and tables:**
|
||||
```swift
|
||||
label.latex = "A = \\begin{pmatrix} 1 & 2 \\\\ 3 & 4 \\end{pmatrix}"
|
||||
// Matrix always on own line
|
||||
```
|
||||
|
||||
**⚠️ Delimited expressions (\left...\right):**
|
||||
```swift
|
||||
label.latex = "\\left(\\frac{a}{b}\\right) + c"
|
||||
// The parenthesized group forces line breaks
|
||||
```
|
||||
|
||||
**⚠️ Colored expressions:**
|
||||
```swift
|
||||
label.latex = "a + \\color{red}{b} + c"
|
||||
// Colored portion causes line break
|
||||
```
|
||||
|
||||
**⚠️ Math accents:**
|
||||
```swift
|
||||
label.latex = "\\hat{x} + \\tilde{y} + \\bar{z}"
|
||||
// Accents may cause line breaks
|
||||
```
|
||||
|
||||
#### Best Practices
|
||||
|
||||
**DO:**
|
||||
- Use interatom breaking for simple equations with operators and relations
|
||||
- Use for mixed text and math where you want natural breaks
|
||||
- Use for long sequences of variables, numbers, and operators
|
||||
- Set appropriate `preferredMaxLayoutWidth` based on your layout needs
|
||||
|
||||
**DON'T:**
|
||||
- Expect natural breaking in expressions with large operators (∑, ∫, etc. - not yet optimized)
|
||||
- Expect natural breaking in expressions with \left...\right delimiters (not yet optimized)
|
||||
- Use extremely narrow widths (less than ~80pt) which may cause poor breaks
|
||||
|
||||
#### Examples
|
||||
|
||||
**Excellent use case (discriminant formula):**
|
||||
```swift
|
||||
label.latex = "\\text{Calculer le discriminant }\\Delta=b^{2}-4ac\\text{ avec }a=1\\text{, }b=-1\\text{, }c=-5"
|
||||
label.preferredMaxLayoutWidth = 235
|
||||
// ✅ Breaks naturally at good points between atoms
|
||||
```
|
||||
|
||||
**Good use case (simple arithmetic):**
|
||||
```swift
|
||||
label.latex = "5+10+15+20+25+30+35+40+45+50"
|
||||
label.preferredMaxLayoutWidth = 150
|
||||
// ✅ Breaks between operators cleanly
|
||||
```
|
||||
|
||||
**Excellent use case (fractions inline - NEW!):**
|
||||
```swift
|
||||
label.latex = "a+\\frac{1}{2}+b+\\frac{3}{4}+c"
|
||||
label.preferredMaxLayoutWidth = 200
|
||||
// ✅ Fractions stay inline when they fit!
|
||||
// Breaks intelligently: "a + ½ + b" on line 1, "+ ¾ + c" on line 2
|
||||
```
|
||||
|
||||
**Excellent use case (radicals inline - NEW!):**
|
||||
```swift
|
||||
label.latex = "x+\\sqrt{2}+y+\\sqrt{3}+z"
|
||||
label.preferredMaxLayoutWidth = 150
|
||||
// ✅ Radicals stay inline when they fit!
|
||||
// Example: "x + √2 + y" on line 1, "+ √3 + z" on line 2
|
||||
```
|
||||
|
||||
**Alternative for complex expressions:**
|
||||
```swift
|
||||
// Instead of trying to break this:
|
||||
label.latex = "x = \\frac{-b \\pm \\sqrt{b^2-4ac}}{2a}"
|
||||
// Consider it as a single display equation without width constraint
|
||||
label.preferredMaxLayoutWidth = 0 // No breaking
|
||||
```
|
||||
|
||||
#### Technical Details
|
||||
|
||||
- **Line spacing**: New lines are positioned at `fontSize × 1.5` below the previous line
|
||||
- **Breaking algorithm**: Greedy - breaks immediately when projected width exceeds constraint
|
||||
- **Width calculation**: Includes inter-element spacing according to TeX spacing rules
|
||||
- **Number protection**: Numbers in patterns like "3.14", "1,000", etc. are kept intact
|
||||
- **Supports locales**: English, French, Swiss number formats
|
||||
|
||||
### Included Features
|
||||
This is a list of formula types that the library currently supports:
|
||||
|
||||
* Simple algebraic equations
|
||||
* Fractions and continued fractions (including `\frac`, `\dfrac`, `\tfrac`, `\cfrac`)
|
||||
* Exponents and subscripts
|
||||
* Trigonometric formulae
|
||||
* Square roots and n-th roots
|
||||
* Calculus symbols - limits, derivatives, integrals (including `\iint`, `\iiint`, `\iiiint`)
|
||||
* Big operators (e.g. product, sum)
|
||||
* Big delimiters (using `\left` and `\right`)
|
||||
* Greek alphabet
|
||||
* Combinatorics (`\binom`, `\choose` etc.)
|
||||
* Geometry symbols (e.g. angle, congruence etc.)
|
||||
* Ratios, proportions, percentages
|
||||
* Math spacing
|
||||
* Overline and underline
|
||||
* Math accents
|
||||
* Matrices (including `\smallmatrix` and starred variants like `pmatrix*` with alignment)
|
||||
* Multi-line subscripts and limits (`\substack`)
|
||||
* Equation alignment
|
||||
* Change bold, roman, caligraphic and other font styles (`\bf`, `\text`, etc.)
|
||||
* Style commands (`\displaystyle`, `\textstyle`)
|
||||
* Most commonly used math symbols
|
||||
* Colors for both text and background
|
||||
* **Inline and display math mode delimiters** (see below)
|
||||
|
||||
### LaTeX Math Delimiters
|
||||
|
||||
`SwiftMath` now supports all standard LaTeX math delimiters for both inline and display modes. The parser automatically detects and handles these delimiters:
|
||||
|
||||
#### Inline Math (Text Style)
|
||||
Use these delimiters for inline math within text, which renders more compactly:
|
||||
|
||||
```swift
|
||||
// Dollar signs (TeX style)
|
||||
label.latex = "$E = mc^2$"
|
||||
|
||||
// Parentheses (LaTeX style)
|
||||
label.latex = "\\(\\sum_{i=1}^{n} x_i\\)"
|
||||
|
||||
// Cases environment in inline mode
|
||||
label.latex = "\\(\\begin{cases} x + y = 5 \\\\ 2x - y = 1 \\end{cases}\\)"
|
||||
```
|
||||
|
||||
#### Display Math (Display Style)
|
||||
Use these delimiters for standalone equations with larger operators and limits:
|
||||
|
||||
```swift
|
||||
// Double dollar signs (TeX style)
|
||||
label.latex = "$$\\int_{0}^{\\infty} e^{-x^2} dx = \\frac{\\sqrt{\\pi}}{2}$$"
|
||||
|
||||
// Square brackets (LaTeX style)
|
||||
label.latex = "\\[\\sum_{k=1}^{n} k^2 = \\frac{n(n+1)(2n+1)}{6}\\]"
|
||||
|
||||
// Equation environment
|
||||
label.latex = "\\begin{equation} x^2 + y^2 = z^2 \\end{equation}"
|
||||
|
||||
// Cases environment in display mode
|
||||
label.latex = "\\begin{cases} x + y = 5 \\\\ 2x - y = 1 \\end{cases}"
|
||||
```
|
||||
|
||||
**Note:** The difference between inline and display modes:
|
||||
- **Inline mode** (`$...$` or `\(...\)`) renders compactly, suitable for math within text
|
||||
- **Display mode** (`$$...$$`, `\[...\]`, or environments) renders with larger operators and limits positioned above/below
|
||||
|
||||
All delimiters are automatically stripped during parsing, and the math mode is set appropriately. No additional configuration is needed!
|
||||
|
||||
#### Backward Compatibility
|
||||
Equations without explicit delimiters continue to work as before, defaulting to display mode:
|
||||
|
||||
```swift
|
||||
label.latex = "x = \\frac{-b \\pm \\sqrt{b^2-4ac}}{2a}" // Works as always
|
||||
```
|
||||
|
||||
Note: SwiftMath only supports the commands in LaTeX's math mode. There is
|
||||
also no language support for other than west European langugages and some
|
||||
Cyrillic characters. There would be two ways to support more languages:
|
||||
|
||||
1) Find a math font compatible with `SwiftMath` that contains all the glyphs
|
||||
for that language.
|
||||
2) Add support to `SwiftMath` for standard Unicode fonts that contain all
|
||||
langauge glyphs.
|
||||
|
||||
Of these two, the first is much easier. However, if you want a challenge,
|
||||
try to tackle the second option.
|
||||
|
||||
### Example
|
||||
|
||||
The [SwiftMathDemo](https://github.com/mgriebling/SwiftMathDemo) is a SwiftUI version
|
||||
of the Objective-C demo included in `iosMath` that uses `SwiftMath` as a Swift package dependency.
|
||||
|
||||
### Advanced configuration
|
||||
|
||||
`MTMathUILabel` supports some advanced configuration options:
|
||||
|
||||
##### Math mode
|
||||
|
||||
You can change the mode of the `MTMathUILabel` between Display Mode
|
||||
(equivalent to `$$` or `\[` in LaTeX) and Text Mode (equivalent to `$`
|
||||
or `\(` in LaTeX). The default style is Display. To switch to Text
|
||||
simply:
|
||||
|
||||
```swift
|
||||
label.labelMode = .text
|
||||
```
|
||||
|
||||
##### Text Alignment
|
||||
The default alignment of the equations is left. This can be changed to
|
||||
center or right as follows:
|
||||
|
||||
```swift
|
||||
label.textAlignment = .center
|
||||
```
|
||||
|
||||
##### Font size
|
||||
The default font-size is 30pt. You can change it as follows:
|
||||
|
||||
```swift
|
||||
label.fontSize = 25
|
||||
```
|
||||
##### Font
|
||||
The default font is *Latin Modern Math*. This can be changed as:
|
||||
|
||||
```swift
|
||||
label.font = MTFontManager.fontmanager.termesFont(withSize:20)
|
||||
```
|
||||
|
||||
This project has 12 fonts bundled with it, but you can use any OTF math
|
||||
font. A python script is included that generates the `.plist` files
|
||||
required for an `.otf` font to work with `SwiftMath`. If you generate
|
||||
(and test) any other fonts please contribute them back to this project for
|
||||
others to benefit.
|
||||
|
||||
|
||||
|
||||
Note: The `KpMath-Light`, `KpMath-Sans`, `Asana` fonts currently incorrectly
|
||||
render very large radicals. It appears that the font files do
|
||||
not properly define the offsets required to typeset these glyphs. If
|
||||
anyone can fix this, it would be greatly appreciated.
|
||||
|
||||
##### Text Color
|
||||
The default color of the rendered equation is black. You can change
|
||||
it to any other color as follows:
|
||||
|
||||
```swift
|
||||
label.textColor = .red
|
||||
```
|
||||
|
||||
It is also possible to set different colors for different parts of the
|
||||
equation. Just access the `displayList` field and set the `textColor`
|
||||
of the underlying displays of which you want to change the color.
|
||||
|
||||
##### Fallback Font for Unicode Text
|
||||
By default, math fonts only support a limited set of characters (Latin, Greek, common math symbols).
|
||||
To display other Unicode characters like Chinese, Japanese, Korean, emoji, or other scripts in `\text{}`
|
||||
commands, you can configure a fallback font:
|
||||
|
||||
```swift
|
||||
let mathFont = MTFontManager().font(withName: MathFont.latinModernFont.rawValue, size: 30)
|
||||
|
||||
// Set a fallback font for unsupported characters (defaults to nil)
|
||||
#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 世界 🌍}" // English, Chinese, and emoji
|
||||
```
|
||||
|
||||
When the main math font doesn't contain a glyph for a character, the fallback font will be used automatically.
|
||||
This is particularly useful for:
|
||||
- Chinese text: `\text{中文}`
|
||||
- Japanese text: `\text{日本語}`
|
||||
- Korean text: `\text{한국어}`
|
||||
- Emoji: `\text{Math is fun! 🎉📐}`
|
||||
- Mixed scripts: `\text{Equation: 方程式}`
|
||||
|
||||
**Note**: The fallback font only applies to characters within `\text{}` commands, not regular math mode.
|
||||
|
||||
##### Custom Commands
|
||||
You can define your own commands that are not already predefined. This is
|
||||
similar to macros is LaTeX. To define your own command use:
|
||||
|
||||
```swift
|
||||
MTMathAtomFactory.addLatexSymbol("lcm", value: MTMathAtomFactory.operator(withName: "lcm", limits: false))
|
||||
```
|
||||
|
||||
This creates an `\lcm` command that can be used in the LaTeX.
|
||||
|
||||
##### Content Insets
|
||||
The `MTMathUILabel` has `contentInsets` for finer control of placement of the
|
||||
equation in relation to the view.
|
||||
|
||||
If you need to set it you can do as follows:
|
||||
|
||||
```swift
|
||||
label.contentInsets = UIEdgeInsets(top: 0, left: 10, bottom: 0, right: 20)
|
||||
```
|
||||
|
||||
##### Error handling
|
||||
|
||||
If the LaTeX text given to `MTMathUILabel` is
|
||||
invalid or if it contains commands that aren't currently supported then
|
||||
an error message will be displayed instead of the label.
|
||||
|
||||
This error can be programmatically retrieved as `label.error`. If you
|
||||
prefer not to display anything then set:
|
||||
|
||||
```swift
|
||||
label.displayErrorInline = true
|
||||
```
|
||||
|
||||
## Future Enhancements
|
||||
|
||||
Note this is not a complete implementation of LaTeX math mode. There are
|
||||
some important pieces that are missing and will be included in future
|
||||
updates. This includes:
|
||||
|
||||
* Support for explicit big delimiters (`\big`, `\Big`, `\bigg`, `\Bigg`, etc.)
|
||||
* `\middle` delimiter for use between `\left` and `\right`
|
||||
* Fine spacing commands (`\,`, `\:`, `\;`, `\!`)
|
||||
* Bold symbol command (`\boldsymbol`)
|
||||
* Addition of missing plain TeX commands
|
||||
|
||||
For a complete list of missing features and their implementation status, see [MISSING_FEATURES.md](MISSING_FEATURES.md).
|
||||
|
||||
## License
|
||||
|
||||
`SwiftMath` is available under the MIT license. See the [LICENSE](./LICENSE)
|
||||
file for more info.
|
||||
|
||||
### Fonts
|
||||
This distribution contains the following fonts. These fonts are
|
||||
licensed as follows:
|
||||
* Latin Modern Math:
|
||||
[GUST Font License](GUST-FONT-LICENSE.txt)
|
||||
* Tex Gyre Termes:
|
||||
[GUST Font License](GUST-FONT-LICENSE.txt)
|
||||
* [XITS Math](https://github.com/khaledhosny/xits-math):
|
||||
[Open Font License](OFL.txt)
|
||||
* [KpMath Light/KpMath Sans](http://scripts.sil.org/OFL):
|
||||
[SIL Open Font License](OFL.txt)
|
||||
# SwiftUIMath
|
||||
|
||||
6
Sources/SwiftUIMath/Untitled.swift
Normal file
@@ -0,0 +1,6 @@
|
||||
//
|
||||
// Untitled.swift
|
||||
// swiftui-math
|
||||
//
|
||||
// Created by Guille Gonzalez on 31/12/25.
|
||||
//
|
||||
681
SwiftMath/README.md
Normal file
@@ -0,0 +1,681 @@
|
||||
# SwiftMath
|
||||
|
||||
`SwiftMath` provides a full Swift implementation of [iosMath](https://travis-ci.org/kostub/iosMath)
|
||||
for displaying beautifully rendered math equations in iOS and MacOS applications. It typesets formulae written
|
||||
using LaTeX in a `UILabel` equivalent class. It uses the same typesetting rules as LaTeX and
|
||||
so the equations are rendered exactly as LaTeX would render them.
|
||||
|
||||
Please also check out [SwiftMathDemo](https://github.com/mgriebling/SwiftMathDemo.git) for examples of how to use `SwiftMath`
|
||||
from SwiftUI.
|
||||
|
||||
`SwiftMath` is similar to [MathJax](https://www.mathjax.org) or
|
||||
[KaTeX](https://github.com/Khan/KaTeX) for the web but for native iOS or MacOS
|
||||
applications without having to use a `UIWebView` and Javascript. More
|
||||
importantly, it is significantly faster than using a `UIWebView`.
|
||||
|
||||
`SwiftMath` is a Swift translation of the latest `iosMath` v0.9.5 release but includes bug fixes
|
||||
and enhancements like a new \lbar (lambda bar) character and cyrillic alphabet support.
|
||||
The original `iosMath` test suites have also been translated to Swift and run without errors.
|
||||
Note: Error test conditions are ignored to avoid tagging everything with silly `throw`s.
|
||||
Please let me know of any bugs or bug fixes that you find.
|
||||
|
||||
`SwiftMath` prepackages everything needed for direct access via the Swift Package Manager.
|
||||
|
||||
## Examples
|
||||
Here are screenshots of some formulae that were rendered with this library:
|
||||
|
||||
```LaTeX
|
||||
x = \frac{-b \pm \sqrt{b^2-4ac}}{2a}
|
||||
```
|
||||
|
||||

|
||||

|
||||
|
||||
```LaTeX
|
||||
f(x) = \int\limits_{-\infty}^\infty\!\hat f(\xi)\,e^{2 \pi i \xi x}\,\mathrm{d}\xi
|
||||
```
|
||||
|
||||

|
||||

|
||||
|
||||
```LaTeX
|
||||
\frac{1}{n}\sum_{i=1}^{n}x_i \geq \sqrt[n]{\prod_{i=1}^{n}x_i}
|
||||
```
|
||||
|
||||

|
||||

|
||||
|
||||
```LaTex
|
||||
\frac{1}{\left(\sqrt{\phi \sqrt{5}}-\phi\\right) e^{\frac25 \pi}}
|
||||
= 1+\frac{e^{-2\pi}} {1 +\frac{e^{-4\pi}} {1+\frac{e^{-6\pi}} {1+\frac{e^{-8\pi}} {1+\cdots} } } }
|
||||
```
|
||||
|
||||

|
||||

|
||||
|
||||
More examples are included in [EXAMPLES](EXAMPLES.md)
|
||||
|
||||
## Fonts
|
||||
Here are previews of the included fonts:
|
||||
|
||||

|
||||

|
||||
|
||||
## Requirements
|
||||
`SwiftMath` works on iOS 11+ or MacOS 12+. It depends
|
||||
on the following Apple frameworks:
|
||||
|
||||
* Foundation.framework
|
||||
* CoreGraphics.framework
|
||||
* QuartzCore.framework
|
||||
* CoreText.framework
|
||||
|
||||
Additionally for iOS it requires:
|
||||
* UIKit.framework
|
||||
|
||||
Additionally for MacOS it requires:
|
||||
* AppKit.framework
|
||||
|
||||
## Installation
|
||||
|
||||
### Swift Package
|
||||
|
||||
`SwiftMath` is available from [SwiftMath](https://github.com/mgriebling/SwiftMath.git).
|
||||
To use it in your code, just add the https://github.com/mgriebling/SwiftMath.git path to
|
||||
XCode's package manager.
|
||||
|
||||
## Usage
|
||||
|
||||
The library provides a class `MTMathUILabel` which is a `UIView` that
|
||||
supports rendering math equations. To display an equation simply create
|
||||
an `MTMathUILabel` as follows:
|
||||
|
||||
```swift
|
||||
|
||||
import SwiftMath
|
||||
|
||||
let label = MTMathUILabel()
|
||||
label.latex = "x = \\frac{-b \\pm \\sqrt{b^2-4ac}}{2a}"
|
||||
|
||||
```
|
||||
Adding `MTMathUILabel` as a sub-view of your `UIView` will render the
|
||||
quadratic formula example shown above.
|
||||
|
||||
The following code creates a SwiftUI component called `MathView` encapsulating the MTMathUILabel:
|
||||
|
||||
```swift
|
||||
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? {
|
||||
// Enable line wrapping by passing proposed width to the label
|
||||
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
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
For code that works with SwiftUI running natively under MacOS use the following:
|
||||
|
||||
```swift
|
||||
import SwiftUI
|
||||
import SwiftMath
|
||||
|
||||
struct MathView: NSViewRepresentable {
|
||||
var equation: String
|
||||
var font: MathFont = .latinModernFont
|
||||
var textAlignment: MTTextAlignment = .center
|
||||
var fontSize: CGFloat = 30
|
||||
var labelMode: MTMathUILabelMode = .text
|
||||
var insets: MTEdgeInsets = MTEdgeInsets()
|
||||
|
||||
func makeNSView(context: Context) -> MTMathUILabel {
|
||||
let view = MTMathUILabel()
|
||||
view.setContentHuggingPriority(.required, for: .vertical)
|
||||
view.setContentCompressionResistancePriority(.required, for: .vertical)
|
||||
return view
|
||||
}
|
||||
|
||||
func updateNSView(_ view: MTMathUILabel, context: Context) {
|
||||
view.latex = equation
|
||||
let font = MTFontManager().font(withName: font.rawValue, size: fontSize)
|
||||
font?.fallbackFont = NSFont.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, nsView: MTMathUILabel, context: Context) -> CGSize? {
|
||||
// Enable line wrapping by passing proposed width to the label
|
||||
if let width = proposal.width, width.isFinite, width > 0 {
|
||||
nsView.preferredMaxLayoutWidth = width
|
||||
let size = nsView.sizeThatFits(CGSize(width: width, height: .greatestFiniteMagnitude))
|
||||
return size
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Automatic Line Wrapping
|
||||
|
||||
`SwiftMath` supports automatic line wrapping (multiline display) for mathematical content. The implementation uses **interatom line breaking** which breaks equations at atom boundaries (between mathematical elements) rather than within them, preserving the semantic structure of the mathematics.
|
||||
|
||||
#### Using Line Wrapping with UIKit/AppKit
|
||||
|
||||
For direct `MTMathUILabel` usage, set the `preferredMaxLayoutWidth` property:
|
||||
|
||||
```swift
|
||||
let label = MTMathUILabel()
|
||||
label.latex = "\\text{Calculer le discriminant }\\Delta=b^{2}-4ac\\text{ avec }a=1\\text{, }b=-1\\text{, }c=-5"
|
||||
label.font = MTFontManager.fontManager.defaultFont
|
||||
|
||||
// Enable line wrapping by setting a maximum width
|
||||
label.preferredMaxLayoutWidth = 235
|
||||
```
|
||||
|
||||
You can also use `sizeThatFits` to calculate the size with a width constraint:
|
||||
|
||||
```swift
|
||||
let constrainedSize = label.sizeThatFits(CGSize(width: 235, height: .greatestFiniteMagnitude))
|
||||
```
|
||||
|
||||
#### Using Line Wrapping with SwiftUI
|
||||
|
||||
The `MathView` examples above include `sizeThatFits()` which automatically enables line wrapping when SwiftUI proposes a width constraint. No additional configuration is needed:
|
||||
|
||||
```swift
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
MathView(
|
||||
equation: "\\text{Calculer le discriminant }\\Delta=b^{2}-4ac\\text{ avec }a=1\\text{, }b=-1\\text{, }c=-5",
|
||||
fontSize: 17,
|
||||
labelMode: .text
|
||||
)
|
||||
}
|
||||
.frame(maxWidth: 235) // The equation will break across multiple lines
|
||||
```
|
||||
|
||||
#### Line Wrapping Behavior and Capabilities
|
||||
|
||||
SwiftMath implements **two complementary line breaking mechanisms**:
|
||||
|
||||
##### 1. Interatom Line Breaking (Primary)
|
||||
Breaks equations **between atoms** (mathematical elements) when content exceeds the width constraint. This is the preferred method as it maintains semantic integrity.
|
||||
|
||||
##### 2. Universal Line Breaking (Fallback)
|
||||
For very long text within single atoms, breaks at Unicode word boundaries using Core Text with number protection (prevents splitting numbers like "3.14").
|
||||
|
||||
#### Fully Supported Cases
|
||||
|
||||
These atom types work perfectly with interatom line breaking:
|
||||
|
||||
**✅ Variables and ordinary text:**
|
||||
```swift
|
||||
label.latex = "a b c d e f g h i j k l m n o p"
|
||||
label.preferredMaxLayoutWidth = 150
|
||||
// Breaks between individual variables at natural boundaries
|
||||
```
|
||||
|
||||
**✅ Binary operators (+, -, ×, ÷):**
|
||||
```swift
|
||||
label.latex = "a+b+c+d+e+f+g+h"
|
||||
label.preferredMaxLayoutWidth = 100
|
||||
// Breaks cleanly: "a+b+c+d+"
|
||||
// "e+f+g+h"
|
||||
```
|
||||
|
||||
**✅ Relations (=, <, >, ≤, ≥, etc.):**
|
||||
```swift
|
||||
label.latex = "a=1, b=2, c=3, d=4, e=5"
|
||||
label.preferredMaxLayoutWidth = 120
|
||||
// Breaks after commas and operators
|
||||
```
|
||||
|
||||
**✅ Mixed text and simple math:**
|
||||
```swift
|
||||
label.latex = "\\text{Calculer }\\Delta=b^{2}-4ac\\text{ avec }a=1\\text{, }b=-1"
|
||||
label.preferredMaxLayoutWidth = 200
|
||||
// Breaks between text and math atoms naturally
|
||||
```
|
||||
|
||||
**✅ Punctuation (commas, periods):**
|
||||
```swift
|
||||
label.latex = "\\text{First, second, third, fourth, fifth}"
|
||||
label.preferredMaxLayoutWidth = 150
|
||||
// Breaks at commas and spaces
|
||||
```
|
||||
|
||||
**✅ Brackets and parentheses (simple):**
|
||||
```swift
|
||||
label.latex = "(a+b)+(c+d)+(e+f)"
|
||||
label.preferredMaxLayoutWidth = 120
|
||||
// Breaks between parenthesized groups
|
||||
```
|
||||
|
||||
**✅ Greek letters and symbols:**
|
||||
```swift
|
||||
label.latex = "\\alpha+\\beta+\\gamma+\\delta+\\epsilon+\\zeta"
|
||||
label.preferredMaxLayoutWidth = 150
|
||||
// Breaks between Greek letters
|
||||
```
|
||||
|
||||
**✅ Fractions (NEW!):**
|
||||
```swift
|
||||
label.latex = "a+\\frac{1}{2}+b+\\frac{3}{4}+c"
|
||||
label.preferredMaxLayoutWidth = 150
|
||||
// Fractions stay inline if they fit, break to new line only when needed
|
||||
// Example: "a + ½ + b" stays on one line if it fits
|
||||
```
|
||||
|
||||
**✅ Radicals/Square roots (NEW!):**
|
||||
```swift
|
||||
label.latex = "x+\\sqrt{2}+y+\\sqrt{3}+z"
|
||||
label.preferredMaxLayoutWidth = 150
|
||||
// Radicals stay inline if they fit, break to new line only when needed
|
||||
// Example: "x + √2 + y" stays on one line if it fits
|
||||
```
|
||||
|
||||
**✅ Mixed fractions and radicals (NEW!):**
|
||||
```swift
|
||||
label.latex = "a+\\frac{1}{2}+\\sqrt{3}+b"
|
||||
label.preferredMaxLayoutWidth = 200
|
||||
// Intelligently breaks between complex mathematical elements
|
||||
```
|
||||
|
||||
#### Limited Support Cases
|
||||
|
||||
These cases work but with some constraints:
|
||||
|
||||
**⚠️ Atoms with superscripts/subscripts:**
|
||||
```swift
|
||||
label.latex = "a^{2}+b^{2}+c^{2}+d^{2}+e^{2}"
|
||||
label.preferredMaxLayoutWidth = 150
|
||||
// Works, but uses fallback breaking mechanism
|
||||
// May not break at the most optimal positions
|
||||
```
|
||||
**Note**: Scripted atoms (with superscripts/subscripts) trigger the universal breaking mechanism which breaks within accumulated text rather than at atom boundaries. This still works but may not be as clean as pure interatom breaking.
|
||||
|
||||
**⚠️ Very long single text atoms:**
|
||||
```swift
|
||||
label.latex = "\\text{This is an extremely long piece of text within a single text command}"
|
||||
label.preferredMaxLayoutWidth = 200
|
||||
// Uses Unicode word boundary breaking with Core Text
|
||||
// Protects numbers from being split (e.g., "3.14" stays together)
|
||||
```
|
||||
|
||||
#### Remaining Unsupported Cases
|
||||
|
||||
These atom types still force line breaks (not yet optimized):
|
||||
|
||||
**⚠️ Large operators (∑, ∫, ∏, lim):**
|
||||
```swift
|
||||
label.latex = "\\sum_{i=1}^{n} x_i + \\int_{0}^{1} f(x)dx"
|
||||
// Each operator forces a new line
|
||||
```
|
||||
|
||||
**⚠️ Matrices and tables:**
|
||||
```swift
|
||||
label.latex = "A = \\begin{pmatrix} 1 & 2 \\\\ 3 & 4 \\end{pmatrix}"
|
||||
// Matrix always on own line
|
||||
```
|
||||
|
||||
**⚠️ Delimited expressions (\left...\right):**
|
||||
```swift
|
||||
label.latex = "\\left(\\frac{a}{b}\\right) + c"
|
||||
// The parenthesized group forces line breaks
|
||||
```
|
||||
|
||||
**⚠️ Colored expressions:**
|
||||
```swift
|
||||
label.latex = "a + \\color{red}{b} + c"
|
||||
// Colored portion causes line break
|
||||
```
|
||||
|
||||
**⚠️ Math accents:**
|
||||
```swift
|
||||
label.latex = "\\hat{x} + \\tilde{y} + \\bar{z}"
|
||||
// Accents may cause line breaks
|
||||
```
|
||||
|
||||
#### Best Practices
|
||||
|
||||
**DO:**
|
||||
- Use interatom breaking for simple equations with operators and relations
|
||||
- Use for mixed text and math where you want natural breaks
|
||||
- Use for long sequences of variables, numbers, and operators
|
||||
- Set appropriate `preferredMaxLayoutWidth` based on your layout needs
|
||||
|
||||
**DON'T:**
|
||||
- Expect natural breaking in expressions with large operators (∑, ∫, etc. - not yet optimized)
|
||||
- Expect natural breaking in expressions with \left...\right delimiters (not yet optimized)
|
||||
- Use extremely narrow widths (less than ~80pt) which may cause poor breaks
|
||||
|
||||
#### Examples
|
||||
|
||||
**Excellent use case (discriminant formula):**
|
||||
```swift
|
||||
label.latex = "\\text{Calculer le discriminant }\\Delta=b^{2}-4ac\\text{ avec }a=1\\text{, }b=-1\\text{, }c=-5"
|
||||
label.preferredMaxLayoutWidth = 235
|
||||
// ✅ Breaks naturally at good points between atoms
|
||||
```
|
||||
|
||||
**Good use case (simple arithmetic):**
|
||||
```swift
|
||||
label.latex = "5+10+15+20+25+30+35+40+45+50"
|
||||
label.preferredMaxLayoutWidth = 150
|
||||
// ✅ Breaks between operators cleanly
|
||||
```
|
||||
|
||||
**Excellent use case (fractions inline - NEW!):**
|
||||
```swift
|
||||
label.latex = "a+\\frac{1}{2}+b+\\frac{3}{4}+c"
|
||||
label.preferredMaxLayoutWidth = 200
|
||||
// ✅ Fractions stay inline when they fit!
|
||||
// Breaks intelligently: "a + ½ + b" on line 1, "+ ¾ + c" on line 2
|
||||
```
|
||||
|
||||
**Excellent use case (radicals inline - NEW!):**
|
||||
```swift
|
||||
label.latex = "x+\\sqrt{2}+y+\\sqrt{3}+z"
|
||||
label.preferredMaxLayoutWidth = 150
|
||||
// ✅ Radicals stay inline when they fit!
|
||||
// Example: "x + √2 + y" on line 1, "+ √3 + z" on line 2
|
||||
```
|
||||
|
||||
**Alternative for complex expressions:**
|
||||
```swift
|
||||
// Instead of trying to break this:
|
||||
label.latex = "x = \\frac{-b \\pm \\sqrt{b^2-4ac}}{2a}"
|
||||
// Consider it as a single display equation without width constraint
|
||||
label.preferredMaxLayoutWidth = 0 // No breaking
|
||||
```
|
||||
|
||||
#### Technical Details
|
||||
|
||||
- **Line spacing**: New lines are positioned at `fontSize × 1.5` below the previous line
|
||||
- **Breaking algorithm**: Greedy - breaks immediately when projected width exceeds constraint
|
||||
- **Width calculation**: Includes inter-element spacing according to TeX spacing rules
|
||||
- **Number protection**: Numbers in patterns like "3.14", "1,000", etc. are kept intact
|
||||
- **Supports locales**: English, French, Swiss number formats
|
||||
|
||||
### Included Features
|
||||
This is a list of formula types that the library currently supports:
|
||||
|
||||
* Simple algebraic equations
|
||||
* Fractions and continued fractions (including `\frac`, `\dfrac`, `\tfrac`, `\cfrac`)
|
||||
* Exponents and subscripts
|
||||
* Trigonometric formulae
|
||||
* Square roots and n-th roots
|
||||
* Calculus symbols - limits, derivatives, integrals (including `\iint`, `\iiint`, `\iiiint`)
|
||||
* Big operators (e.g. product, sum)
|
||||
* Big delimiters (using `\left` and `\right`)
|
||||
* Greek alphabet
|
||||
* Combinatorics (`\binom`, `\choose` etc.)
|
||||
* Geometry symbols (e.g. angle, congruence etc.)
|
||||
* Ratios, proportions, percentages
|
||||
* Math spacing
|
||||
* Overline and underline
|
||||
* Math accents
|
||||
* Matrices (including `\smallmatrix` and starred variants like `pmatrix*` with alignment)
|
||||
* Multi-line subscripts and limits (`\substack`)
|
||||
* Equation alignment
|
||||
* Change bold, roman, caligraphic and other font styles (`\bf`, `\text`, etc.)
|
||||
* Style commands (`\displaystyle`, `\textstyle`)
|
||||
* Most commonly used math symbols
|
||||
* Colors for both text and background
|
||||
* **Inline and display math mode delimiters** (see below)
|
||||
|
||||
### LaTeX Math Delimiters
|
||||
|
||||
`SwiftMath` now supports all standard LaTeX math delimiters for both inline and display modes. The parser automatically detects and handles these delimiters:
|
||||
|
||||
#### Inline Math (Text Style)
|
||||
Use these delimiters for inline math within text, which renders more compactly:
|
||||
|
||||
```swift
|
||||
// Dollar signs (TeX style)
|
||||
label.latex = "$E = mc^2$"
|
||||
|
||||
// Parentheses (LaTeX style)
|
||||
label.latex = "\\(\\sum_{i=1}^{n} x_i\\)"
|
||||
|
||||
// Cases environment in inline mode
|
||||
label.latex = "\\(\\begin{cases} x + y = 5 \\\\ 2x - y = 1 \\end{cases}\\)"
|
||||
```
|
||||
|
||||
#### Display Math (Display Style)
|
||||
Use these delimiters for standalone equations with larger operators and limits:
|
||||
|
||||
```swift
|
||||
// Double dollar signs (TeX style)
|
||||
label.latex = "$$\\int_{0}^{\\infty} e^{-x^2} dx = \\frac{\\sqrt{\\pi}}{2}$$"
|
||||
|
||||
// Square brackets (LaTeX style)
|
||||
label.latex = "\\[\\sum_{k=1}^{n} k^2 = \\frac{n(n+1)(2n+1)}{6}\\]"
|
||||
|
||||
// Equation environment
|
||||
label.latex = "\\begin{equation} x^2 + y^2 = z^2 \\end{equation}"
|
||||
|
||||
// Cases environment in display mode
|
||||
label.latex = "\\begin{cases} x + y = 5 \\\\ 2x - y = 1 \\end{cases}"
|
||||
```
|
||||
|
||||
**Note:** The difference between inline and display modes:
|
||||
- **Inline mode** (`$...$` or `\(...\)`) renders compactly, suitable for math within text
|
||||
- **Display mode** (`$$...$$`, `\[...\]`, or environments) renders with larger operators and limits positioned above/below
|
||||
|
||||
All delimiters are automatically stripped during parsing, and the math mode is set appropriately. No additional configuration is needed!
|
||||
|
||||
#### Backward Compatibility
|
||||
Equations without explicit delimiters continue to work as before, defaulting to display mode:
|
||||
|
||||
```swift
|
||||
label.latex = "x = \\frac{-b \\pm \\sqrt{b^2-4ac}}{2a}" // Works as always
|
||||
```
|
||||
|
||||
Note: SwiftMath only supports the commands in LaTeX's math mode. There is
|
||||
also no language support for other than west European langugages and some
|
||||
Cyrillic characters. There would be two ways to support more languages:
|
||||
|
||||
1) Find a math font compatible with `SwiftMath` that contains all the glyphs
|
||||
for that language.
|
||||
2) Add support to `SwiftMath` for standard Unicode fonts that contain all
|
||||
langauge glyphs.
|
||||
|
||||
Of these two, the first is much easier. However, if you want a challenge,
|
||||
try to tackle the second option.
|
||||
|
||||
### Example
|
||||
|
||||
The [SwiftMathDemo](https://github.com/mgriebling/SwiftMathDemo) is a SwiftUI version
|
||||
of the Objective-C demo included in `iosMath` that uses `SwiftMath` as a Swift package dependency.
|
||||
|
||||
### Advanced configuration
|
||||
|
||||
`MTMathUILabel` supports some advanced configuration options:
|
||||
|
||||
##### Math mode
|
||||
|
||||
You can change the mode of the `MTMathUILabel` between Display Mode
|
||||
(equivalent to `$$` or `\[` in LaTeX) and Text Mode (equivalent to `$`
|
||||
or `\(` in LaTeX). The default style is Display. To switch to Text
|
||||
simply:
|
||||
|
||||
```swift
|
||||
label.labelMode = .text
|
||||
```
|
||||
|
||||
##### Text Alignment
|
||||
The default alignment of the equations is left. This can be changed to
|
||||
center or right as follows:
|
||||
|
||||
```swift
|
||||
label.textAlignment = .center
|
||||
```
|
||||
|
||||
##### Font size
|
||||
The default font-size is 30pt. You can change it as follows:
|
||||
|
||||
```swift
|
||||
label.fontSize = 25
|
||||
```
|
||||
##### Font
|
||||
The default font is *Latin Modern Math*. This can be changed as:
|
||||
|
||||
```swift
|
||||
label.font = MTFontManager.fontmanager.termesFont(withSize:20)
|
||||
```
|
||||
|
||||
This project has 12 fonts bundled with it, but you can use any OTF math
|
||||
font. A python script is included that generates the `.plist` files
|
||||
required for an `.otf` font to work with `SwiftMath`. If you generate
|
||||
(and test) any other fonts please contribute them back to this project for
|
||||
others to benefit.
|
||||
|
||||
|
||||
|
||||
Note: The `KpMath-Light`, `KpMath-Sans`, `Asana` fonts currently incorrectly
|
||||
render very large radicals. It appears that the font files do
|
||||
not properly define the offsets required to typeset these glyphs. If
|
||||
anyone can fix this, it would be greatly appreciated.
|
||||
|
||||
##### Text Color
|
||||
The default color of the rendered equation is black. You can change
|
||||
it to any other color as follows:
|
||||
|
||||
```swift
|
||||
label.textColor = .red
|
||||
```
|
||||
|
||||
It is also possible to set different colors for different parts of the
|
||||
equation. Just access the `displayList` field and set the `textColor`
|
||||
of the underlying displays of which you want to change the color.
|
||||
|
||||
##### Fallback Font for Unicode Text
|
||||
By default, math fonts only support a limited set of characters (Latin, Greek, common math symbols).
|
||||
To display other Unicode characters like Chinese, Japanese, Korean, emoji, or other scripts in `\text{}`
|
||||
commands, you can configure a fallback font:
|
||||
|
||||
```swift
|
||||
let mathFont = MTFontManager().font(withName: MathFont.latinModernFont.rawValue, size: 30)
|
||||
|
||||
// Set a fallback font for unsupported characters (defaults to nil)
|
||||
#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 世界 🌍}" // English, Chinese, and emoji
|
||||
```
|
||||
|
||||
When the main math font doesn't contain a glyph for a character, the fallback font will be used automatically.
|
||||
This is particularly useful for:
|
||||
- Chinese text: `\text{中文}`
|
||||
- Japanese text: `\text{日本語}`
|
||||
- Korean text: `\text{한국어}`
|
||||
- Emoji: `\text{Math is fun! 🎉📐}`
|
||||
- Mixed scripts: `\text{Equation: 方程式}`
|
||||
|
||||
**Note**: The fallback font only applies to characters within `\text{}` commands, not regular math mode.
|
||||
|
||||
##### Custom Commands
|
||||
You can define your own commands that are not already predefined. This is
|
||||
similar to macros is LaTeX. To define your own command use:
|
||||
|
||||
```swift
|
||||
MTMathAtomFactory.addLatexSymbol("lcm", value: MTMathAtomFactory.operator(withName: "lcm", limits: false))
|
||||
```
|
||||
|
||||
This creates an `\lcm` command that can be used in the LaTeX.
|
||||
|
||||
##### Content Insets
|
||||
The `MTMathUILabel` has `contentInsets` for finer control of placement of the
|
||||
equation in relation to the view.
|
||||
|
||||
If you need to set it you can do as follows:
|
||||
|
||||
```swift
|
||||
label.contentInsets = UIEdgeInsets(top: 0, left: 10, bottom: 0, right: 20)
|
||||
```
|
||||
|
||||
##### Error handling
|
||||
|
||||
If the LaTeX text given to `MTMathUILabel` is
|
||||
invalid or if it contains commands that aren't currently supported then
|
||||
an error message will be displayed instead of the label.
|
||||
|
||||
This error can be programmatically retrieved as `label.error`. If you
|
||||
prefer not to display anything then set:
|
||||
|
||||
```swift
|
||||
label.displayErrorInline = true
|
||||
```
|
||||
|
||||
## Future Enhancements
|
||||
|
||||
Note this is not a complete implementation of LaTeX math mode. There are
|
||||
some important pieces that are missing and will be included in future
|
||||
updates. This includes:
|
||||
|
||||
* Support for explicit big delimiters (`\big`, `\Big`, `\bigg`, `\Bigg`, etc.)
|
||||
* `\middle` delimiter for use between `\left` and `\right`
|
||||
* Fine spacing commands (`\,`, `\:`, `\;`, `\!`)
|
||||
* Bold symbol command (`\boldsymbol`)
|
||||
* Addition of missing plain TeX commands
|
||||
|
||||
For a complete list of missing features and their implementation status, see [MISSING_FEATURES.md](MISSING_FEATURES.md).
|
||||
|
||||
## License
|
||||
|
||||
`SwiftMath` is available under the MIT license. See the [LICENSE](./LICENSE)
|
||||
file for more info.
|
||||
|
||||
### Fonts
|
||||
This distribution contains the following fonts. These fonts are
|
||||
licensed as follows:
|
||||
* Latin Modern Math:
|
||||
[GUST Font License](GUST-FONT-LICENSE.txt)
|
||||
* Tex Gyre Termes:
|
||||
[GUST Font License](GUST-FONT-LICENSE.txt)
|
||||
* [XITS Math](https://github.com/khaledhosny/xits-math):
|
||||
[Open Font License](OFL.txt)
|
||||
* [KpMath Light/KpMath Sans](http://scripts.sil.org/OFL):
|
||||
[SIL Open Font License](OFL.txt)
|
||||
|
Before Width: | Height: | Size: 199 KiB After Width: | Height: | Size: 199 KiB |
|
Before Width: | Height: | Size: 154 KiB After Width: | Height: | Size: 154 KiB |
|
Before Width: | Height: | Size: 7.2 KiB After Width: | Height: | Size: 7.2 KiB |
|
Before Width: | Height: | Size: 9.5 KiB After Width: | Height: | Size: 9.5 KiB |
|
Before Width: | Height: | Size: 8.3 KiB After Width: | Height: | Size: 8.3 KiB |
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 6.3 KiB After Width: | Height: | Size: 6.3 KiB |
|
Before Width: | Height: | Size: 9.0 KiB After Width: | Height: | Size: 9.0 KiB |
|
Before Width: | Height: | Size: 9.8 KiB After Width: | Height: | Size: 9.8 KiB |
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 5.3 KiB After Width: | Height: | Size: 5.3 KiB |
|
Before Width: | Height: | Size: 7.6 KiB After Width: | Height: | Size: 7.6 KiB |
|
Before Width: | Height: | Size: 5.9 KiB After Width: | Height: | Size: 5.9 KiB |
|
Before Width: | Height: | Size: 8.3 KiB After Width: | Height: | Size: 8.3 KiB |
|
Before Width: | Height: | Size: 6.2 KiB After Width: | Height: | Size: 6.2 KiB |
|
Before Width: | Height: | Size: 8.8 KiB After Width: | Height: | Size: 8.8 KiB |
|
Before Width: | Height: | Size: 7.0 KiB After Width: | Height: | Size: 7.0 KiB |
|
Before Width: | Height: | Size: 9.6 KiB After Width: | Height: | Size: 9.6 KiB |
|
Before Width: | Height: | Size: 23 KiB After Width: | Height: | Size: 23 KiB |
|
Before Width: | Height: | Size: 26 KiB After Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 8.3 KiB After Width: | Height: | Size: 8.3 KiB |
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 6.0 KiB After Width: | Height: | Size: 6.0 KiB |
|
Before Width: | Height: | Size: 8.3 KiB After Width: | Height: | Size: 8.3 KiB |
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 5.0 KiB After Width: | Height: | Size: 5.0 KiB |
|
Before Width: | Height: | Size: 7.4 KiB After Width: | Height: | Size: 7.4 KiB |
|
Before Width: | Height: | Size: 6.2 KiB After Width: | Height: | Size: 6.2 KiB |
|
Before Width: | Height: | Size: 8.3 KiB After Width: | Height: | Size: 8.3 KiB |
|
Before Width: | Height: | Size: 7.9 KiB After Width: | Height: | Size: 7.9 KiB |
|
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 7.5 KiB After Width: | Height: | Size: 7.5 KiB |
|
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 10 KiB |
6
Tests/SwiftUIMathTests/Untitled.swift
Normal file
@@ -0,0 +1,6 @@
|
||||
//
|
||||
// Untitled.swift
|
||||
// swiftui-math
|
||||
//
|
||||
// Created by Guille Gonzalez on 31/12/25.
|
||||
//
|
||||