Swift: Display (LaTeX) math expressions inline - ios

I would like to display math terms inside a text, in particular in an inline mode, i.e. inside a sentence.
Using LaTeX, this would for example look like:
"Given a right triangle having catheti of length \(a\) resp. \(b\) and a hypotenuse of length \(c\), we have
\[a^2 + b^2 = c^2.\]
This fact is known as the Pythagorean theorem."
Does anybody know how this can be achieved in Swift?
(I know that this example may be achieved in Swift without LaTeX-like tools. However, the expressions in my mind are in fact more complex than in this example, I do need the power of LaTeX.)
The optimal way would be a UITextView-like class which recognizes the math delimiters \(,\) resp. \[,\], recognizes LaTeX code inside these delimiters, and formats the text accordingly.
In the Khan Academy app, this problem seems to be solved as the screenshots in the Apple App Store/Google Play Store show inline (LaTeX) math.
I’ve found the package iosMath which provides a UILabel-like class MTMathUILabel. As this class can display solely formulas, this seems to be not good enough for my purpose, except if there was a method which takes a LaTeX source text such as in the example above, formats expressions such as \(a\) into tiny MTMathUILabels and sets these labels between the other text components. As I am new to Swift, I do not know whether and how this can be achieved. Moreover, this seems to be very difficult from a typographical point of view as there will surely occur difficulties with line breaks. And there might occur performance issues if there are a large number of such labels on the screen at the same time?
It is possible to achieve what I want using a WKWebView and MathJax or KaTeX, which is also a hack, of course. This leads to other difficulties, e.g. if one wants to set several of these WKWebViews on a screen, e.g. inside UITableViewCells.

Using iosMath, my solution on how to get a UILabel to have inline LaTeX is to include LATEX and ENDLATEX markers with no space. I replaced all ranges with an image of the MTMathUILabel, going from last range to first range so the positions don't get screwed up (This solution allows for multiple markers). The image returned from my function is flipped so i used .downMirrored orientation, and i sized it to fit my text, so you might need to fix the numbers a little for the flip scale of 2.5 and the y value for the attachment.bounds.
import UIKit
import iosMath
let question = UILabel()
let currentQuestion = "Given a right triangle having catheti of length LATEX(a)ENDLATEX resp. LATEX(b)ENDLATEX and a hypotenuse of length LATEX(c)ENDLATEX, we have LATEX[a^2 + b^2 = c^2]ENDLATEX. This fact is known as the Pythagorean theorem."
question.text = currentQuestion
if (question.text?.contains("LATEX"))! {
let tempString = question.text!
let tempMutableString = NSMutableAttributedString(string: tempString)
let pattern = NSRegularExpression.escapedPattern(for: "LATEX")
let regex = try? NSRegularExpression(pattern: pattern, options: [])
if let matches = regex?.matches(in: tempString, options: [], range: NSRange(location: 0, length: tempString.count)) {
var i = 0
while i < matches.count {
let range1 = matches.reversed()[i+1].range
let range2 = matches.reversed()[i].range
let finalDistance = range2.location - range1.location + 5
let finalRange = NSRange(location: range1.location, length: finalDistance)
let startIndex = String.Index(utf16Offset: range1.location + 5, in: tempString)
let endIndex = String.Index(utf16Offset: range2.location - 3, in: tempString)
let substring = String(tempString[startIndex..<endIndex])
var image = UIImage()
image = imageWithLabel(string: substring)
let flip = UIImage(cgImage: image.cgImage!, scale: 2.5, orientation: .downMirrored)
let attachment = NSTextAttachment()
attachment.image = flip
attachment.bounds = CGRect(x: 0, y: -flip.size.height/2 + 10, width: flip.size.width, height: flip.size.height)
let replacement = NSAttributedString(attachment: attachment)
tempMutableString.replaceCharacters(in: finalRange, with: replacement)
question.attributedText = tempMutableString
i += 2
}
}
}
func imageWithLabel(string: String) -> UIImage {
let label = MTMathUILabel()
label.latex = string
label.sizeToFit()
UIGraphicsBeginImageContextWithOptions(label.bounds.size, false, 0)
defer { UIGraphicsEndImageContext() }
label.layer.render(in: UIGraphicsGetCurrentContext()!)
return UIGraphicsGetImageFromCurrentImageContext() ?? UIImage()
}

Related

Getting string from label visible at runtime

I want to get the string which is visible at my label and having line break mode .byClipping .
My real string is :-
"This way your API interface will be nice and pretty, you’re not exposing the internals of your class to the world. If you have constants that are used in lots of classes all over the app then it makes sense to have a separate .h and .m file just for defining these constants."
but at run time i am getting :-
"This way your API interface will be nice and pretty, you’re not exposing the internals of your class to the world. If you have constants"
i want to get the below string in a variable at runtime.
I am not sure if this is natively doable. The closest idea I have is to test the string size and removing/adding characters. Check if the following works for you:
func findTextActuallyVisibleInLabel(_ label: UILabel) -> String? {
guard let originalText = label.text else { return nil }
var text = originalText
let labelSize = label.bounds
guard labelSize.height > 0 else { return text }
let bounds: CGRect = CGRect(x: 0.0, y: 0.0, width: labelSize.width, height: CGFloat.infinity)
while !text.isEmpty && label.textRect(forBounds: bounds, limitedToNumberOfLines: label.numberOfLines).height > labelSize.height {
text.removeLast()
label.text = text
}
label.text = originalText
return text
}
This seems to do its job in my case more or less. If I use wrapping that ends text with ... the result is incorrect.
Also some optimization may be nice; currently I just use the full string and start subtracting character by character. Maybe a bisection would be nicer here.
I do still use original label so it should take all possible parameters into account like wrapping, font size, lines... I modify the actual text on the label which is then reset to original.
Possibly view animations should be disabled while this operation is in progress.

Get all Glyphs of a UIFont that have a descender

Is there a way to get all the glyphs of a UIFont that contain a true descender? It seems that using CTLineGetTypographicBounds is not accurate and returns the exact same descent value for every line. I thought it would provide the information that I needed but it did not. So now I am looking to see if I can build a character set from the glyphs that contain true descenders unless there is another way. The ultimate goal would be able to see if a line of text is below the baseline.
let line = CTLineCreateWithAttributedString(NSAttributedString(string: s, attributes: attr))
//let's get the real descent test
var a : CGFloat = 0
var d : CGFloat = 0
var l : CGFloat = 0
let bounds = CTLineGetTypographicBounds(line, &a, &d, &l)
print("the descent is \(d)")
print("the ascent is \(a)")
print("the leading is \(l)")
Since it seems that your actual goal is to determine whether a string contains a character with a descender, you can use Core Text to look at the bounding rect of each glyph. If the bounding rect's origin in negative, this means the glyph starts below the baseline. This will be true for characters such as y but also ,.
func checkDescender(string: String) {
let uiFont = UIFont.systemFont(ofSize: 14) // Pick your font
let font = CTFontCreateWithName(uiFont.fontName as CFString, uiFont.pointSize, nil)
for ch in string.unicodeScalars {
let utf16codepoints = Array(ch.utf16)
var glyphs: [CGGlyph] = [0, 0]
let hasGlyph = CTFontGetGlyphsForCharacters(font, utf16codepoints, &glyphs, utf16codepoints.count)
if hasGlyph {
let rect = CTFontGetBoundingRectsForGlyphs(font, .default, glyphs, nil, 1)
// print("\(ch) has bounding box of \(rect)")
if rect.origin.y < 0 {
print("\(ch) goes below the baseline by \(-rect.origin.y)")
}
}
}
}
checkDescender(string: "Ymy,")
You might want to add additional checks to only look at letters depending on your needs.

Draw circle behind particular characters of UILabel

I am trying to draw circle behind particular parts of UILabel characters without having to use several labels or recompute frames.
I know that I can insert an image into UILabel using attributed text but I want the circle to stand behind the numbers (from 1 to 9). I don't want to store images from 1 to 9.
I also cannot find any pod achieving this.
let fullText = NSMutableAttributedString()
let imageAttachment = NSTextAttachment()
imageAttachment.image = UIImage(named: "\(number)_\(color == .yellow ? "yellow" : "gray")")
let imageString = NSAttributedString(attachment: imageAttachment)
let endString = NSAttributedString("nouveaux")
fullString.append(imageString)
fullString.append(endString)
mainLabel.attributedText = fullString
EDIT
I want to draw circle behind each digit of a string.
I would like it to be a generic tool if possible, some kind of parser that returns an attributed string if possible, a unique UIView if not.
Another use case is "4 messages | chat 2" (4 being yellow, 2 being gray)
I don't understand why you don't just use a utility function that draws the digit-in-a-circle in real time. That way, you can have any digit font, circle size, digit color, and circle color that you like. For example:
func circleAroundDigit(_ num:Int, circleColor:UIColor,
digitColor:UIColor, diameter:CGFloat,
font:UIFont) -> UIImage {
precondition((0...9).contains(num), "digit is not a digit")
let p = NSMutableParagraphStyle()
p.alignment = .center
let s = NSAttributedString(string: String(num), attributes:
[.font:font, .foregroundColor:digitColor, .paragraphStyle:p])
let r = UIGraphicsImageRenderer(size: CGSize(width:diameter, height:diameter))
return r.image {con in
circleColor.setFill()
con.cgContext.fillEllipse(in:
CGRect(x: 0, y: 0, width: diameter, height: diameter))
s.draw(in: CGRect(x: 0, y: diameter / 2 - font.lineHeight / 2,
width: diameter, height: diameter))
}
}
Here's how to use it:
let im = circleAroundDigit(3, circleColor: .yellow,
digitColor: .white, diameter: 30, font:UIFont(name: "GillSans", size: 20)!)
Here's the resulting image; note, though, that every aspect of this can be tweaked just by calling the function differently:
Okay, so now you've got an image. You say you already know how to insert an image inline into an attributed string and display that as part of the label, so I won't explain how to do that:
This is pretty straightforward using a UIStackView, setting the corner radius of the coloured view's layer…
result…
or with 2 stack views in an outer stack view≥
result…

How to get emoji path in iOS

My purpose : Draw outline of every glyph
example1:
input: text= "666棒"
display:
Attach:In the figure above, 1 is displayView,2 is inputView.
example2:
input: text= "666😁棒"
display:
Attach: In the figure above, 1 is displayView,2 is inputView,3 is nothing rendered.
Main ideas is :
Use CoreText obtained every CGglyph.
Get every glyph's CGPath
Use CAShapeLayer display the glyph on screen.
Main method:
let letters = CGMutablePath()
let font = CTFontCreateWithName(fontName as CFString?, fontSize, nil)
let attrString = NSAttributedString(string: text, attributes: [kCTFontAttributeName as String : font])
let line = CTLineCreateWithAttributedString(attrString)
let runArray = CTLineGetGlyphRuns(line)
for runIndex in 0..<CFArrayGetCount(runArray) {
let run : CTRun = unsafeBitCast(CFArrayGetValueAtIndex(runArray, runIndex), to: CTRun.self)
let dictRef : CFDictionary = unsafeBitCast(CTRunGetAttributes(run), to: CFDictionary.self)
let dict : NSDictionary = dictRef as NSDictionary
let runFont = dict[kCTFontAttributeName as String] as! CTFont
for runGlyphIndex in 0..<CTRunGetGlyphCount(run) {
let thisGlyphRange = CFRangeMake(runGlyphIndex, 1)
var glyph = CGGlyph()
var position = CGPoint.zero
CTRunGetGlyphs(run, thisGlyphRange, &glyph)
CTRunGetPositions(run, thisGlyphRange, &position)
let letter = CTFontCreatePathForGlyph(runFont, glyph, nil)
let t = CGAffineTransform(translationX: position.x, y: position.y)
if let letter = letter {
letters.addPath(letter, transform: t)
}
}
}
let path = UIBezierPath()
path.move(to: CGPoint.zero)
path.append(UIBezierPath(cgPath: letters))
let pathLayer = CAShapeLayer()
pathLayer.path = path.cgPath
self.layer.addSubLayer(pathLayer)
...
Question:
How to get emoji path ,in this case I can draw the emoji outline instead of draw the whole emoji? Another benefit is I can draw emoji path animated if I need.
Any help is appreciate!
************************ update 2.15.2017 ***********************
Thanks #KrishnaCA 's suggest.
I used bool supports = CTFontGetGlyphWithName(myFont, "😀") find that no font is support emoji.
Fortunately is Google's Noto provide good support for emoji fonts
You can find it there :google's Noto
I used font Noto Emoji
Display:
Only Noto Emoji and Noto Color Emoji support Emoji (I guess)
Hope to help people who come here!
I believe you need to check whether a glyph for an unicode corresponding to the CTFont exist or not. If it doesn't exist, fall back to any default CTFont that has a glpyh for an unicode
You can check that using the following code.
bool supports = CTFontGetGlyphWithName(myFont, "😀")
here, myFont is a CTFontRef object.
Please let me know if this is what you're not looking for
I believe you'll need CATextLayers to help you out.
I know it's a bit late, but sadly you can not - emojis are actually bitmaps drawn into the same context as shapes representing regular characters. You best bet is probably to draw emoji characters separately at needed scale into the context. This won't give you access to the actual vector data.
If you really need it in a vector form:
I'd go with finding Apple Emoji font redrawn in vector (I remember seeing it on the internet, not sure if it contains all the latest emojis though)
Mapping names of the individual vector images you found to the characters and then drawing the vector images

How can I prevent orphans in a label in swift?

I have a label that can have one or two lines. If it has two lines, I want the second line to have at least two (or maybe three) words, never just one. Any ideas about how I can accomplish that using swift?
Thanks in advance!
Daniel
Edit: I edited out my silly first thoughts that didn't really help.
Ok, after looking around a lot I came up with what I think is the best solution.
I wrote this function:
func numberOfLinesInLabelForText(text: String) -> Int {
let attributes = [NSFontAttributeName : UIFont.preferredFontForTextStyle(UIFontTextStyleHeadline)]
let screenSize: CGRect = UIScreen.mainScreen().bounds
let labelSize = text!.boundingRectWithSize(CGSizeMake((screenSize.width - 30), CGFloat.max), options: NSStringDrawingOptions.UsesLineFragmentOrigin, attributes: attributes, context: nil)
let lines = floor(CGFloat(labelSize.height) / bookTitleLabel.font.lineHeight)
return Int(lines)
}
You put in the string that will be displayed in the label and it gives you how many lines the label will have. I'm using dynamic type and the Headline style for this particular label, hence the preferredFontForTextStyle(UIFontTextStyleHeadline) part, but you can change that to the font and size your label uses.
Then I use (screenSize.width - 30) for my label's width because it's width is not fixed, so I'm using the screen size minus leading and trailing. This is probably not the most elegant solution, I'm open to better suggestions.
The rest is pretty straightforward.
After I have the number of lines I can do this:
func splittedString(text: String) -> String {
if numberOfLinesInLabel(text) == 2 {
var chars = Array(text.characters)
var i = chars.count / 2
var x = chars.count / 2
while chars[i] != " " && chars[x] != " " {
i--
x++
}
if chars[i] == " " {
chars.insert("\n", atIndex: i+1)
} else {
chars.insert("\n", atIndex: x+1)
}
return String(chars)
}
}
Instead of just avoiding orphans I decided to split the string in two at the breaking point nearest to its half, so that's what this last function does, but it wouldn't be hard to tweak it to suit your needs.
And there you have it!

Resources