Sorry I don't have any code yet, but would appreciate some advice!
I have a countdown timer showing seconds with one decimal point in a UILabel (10.0, 9.9, 9.8, etc.). It works fine, but the decimal point moves around slightly depending on the size decimal value. Is there a way to align the text in the UILabel to the decimal point or should I create two labels (one for the seconds value aligned right and one for the decimal value aligned left)?
Thanks!
I think your suggestion of multiple labels is perfectly valid.
Here is an attributed string solution (for m:ss but it should work with floating point numbers also):
let string = "\t43:21" // Sample min:sec. Note the leading tab is required in this code.
let countdownFont = UIFont.systemFontOfSize(13)
let terminators = NSCharacterSet(charactersInString: "\t:.") // in some locales '.' is used instead of ':'
let tabStop = NSTextTab(textAlignment: .Right, location: 40, options: [NSTabColumnTerminatorsAttributeName: terminators])
let paragraphStyle = NSMutableParagraphStyle()
paragraphStyle.tabStops = [tabStop]
let attributeDictionary: [String: AnyObject] = [NSParagraphStyleAttributeName: paragraphStyle, NSFontAttributeName: countdownFont]
let attributedString = NSAttributedString(string: string, attributes: attributeDictionary)
self.countdownLabel.attributedText = attributedString
Related resources:
https://www.objc.io/issues/9-strings/string-rendering/#tables-with-numbers
https://library.oreilly.com/book/0636920034261/programming-ios-8-1st-edition/314.xhtml?ref=toc#_tab_stops
Of course a fixed width font, such as Courier or Menlo could also solve this problem, but they contrast fairly starkly.
Related
I need to use Kern attribute of NSAttributedString. As I can see in the documentation, default value of that attribute is 0.0. But I faced with strange behaviour for phrase Hello, world (for phrase "Hello" all fine):
NSDictionary<NSString*, id>* attributes = #{NSFontAttributeName: [UIFont systemFontOfSize:12]};
NSString* text = #"Hello, World";
NSAttributedString* string = [[NSAttributedString alloc] initWithString:text attributes:attributes];
CGSize size1 = [string size];
NSMutableDictionary<NSString*, id>* attributesWithKernel = [attributes mutableCopy];
attributesWithKernel[NSKernAttributeName] = #(0.0);
NSAttributedString* stringWithKern = [[NSAttributedString alloc] initWithString:text attributes:attributesWithKernel];
CGSize size2 = [stringWithKern size];
XCTAssertTrue(CGSizeEqualToSize(size1, size2)); //here test falls
//size1 = size1 = (width = 68.8125, height = 14.3203125)
//size2 = (width = 69.515625, height = 14.3203125)
To make size1 and size2 equal, kerning should be equal -7.105427357601002e-15 , I know that this is very close to 0.0, but it is strange, because this changes the width almost a pixel.
NSAttributedString has same behaviour in Objective-C and in Swift, example for swift:
let text = "Hello, World"
let attributes : [NSAttributedString.Key : Any] = [NSAttributedString.Key.font: UIFont.systemFont(ofSize: UIFont.systemFontSize)]
let str = NSAttributedString(string: text, attributes: attributes)
let size = str.size()
var attributesWithKern = attributes
attributesWithKern[NSAttributedString.Key.kern] = NSNumber(value: 0.0)
let strWithKern = NSAttributedString(string: text, attributes: attributesWithKern)
let sizeWithKern = strWithKern.size()
XCTAssertTrue(size == sizeWithKern)
How I can fix this behaviour?
P.S.
Now I just remove NSKernAttributeKey from an attribute string if the key equal to 0.0, but I do not think that it is a good solution.
I believe the docs here are wrong, and it is worth opening a radar about that. When no value is set, it is interpreted as "normal kerning." When 0 is set, it is interpreted as "disable kerning," which is why the width is a little wider (kerning typically is slightly negative, bringing kern-pair character, like "W" and "o" in this font, a little closer). I don't think there's any way to explicitly request "default kerning" without removing the attribute.
For your purposes, I believe you're doing the right thing by removing the value when it's zero, because you want default kerning, not to disable kerning.
The reason your tiny negative value is working is because it's not zero, so it's not disabling kerning, but it's so small that the behavior is very, very close to the default, and you're running into the precision of Double in the intermediate calculations (or possibly the precision of Float, depending on how it's implemented internally). You should find that your test passes for any value smaller (closer to zero) than this one, not just that value. In my tests, positive 7e-15 also works, for example.
I did some tests based on your code and I confirm this behaviour which looks like a bug. The point is for some letters strings are equal, for some not. For instance strings with "Hello Mo" or "Hello Oo" are equal, but for instance "Hello WoWoWo" differ a lot. So we see here some kerning being added for "W" without any reason. It can also depend on chosen font, I did not test it though. I see here the only solution that you've used - removing NSKernAttributeKey if it equals to 0.
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()
}
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
When developing for OS X, you have the option of specifying the DecimalTabStopType to get a tab stop on the decimal point of a column of numbers. However, this option isn't available in iOS - is there some way to achieve the same affect?
Currently there's a built-in support for decimal tab stops, see https://developer.apple.com/documentation/uikit/nstexttab/1535107-columnterminators
Can be used as follows
let paragraphStyle = NSMutableParagraphStyle()
let terms = NSTextTab.columnTerminators(for: NSLocale.current)
let tabStop0 = NSTextTab(textAlignment: .right, location: 100, options:
[NSTabColumnTerminatorsAttributeName:terms])
With a little bit of effort, you can easily achieve the same effect on iOS. First, you add a Right tab stop. Then, you add a Left tab stop a very tiny bit to the right. [It appears that the tabStop array is sorted by their offset, so you cannot use exactly the same offset.]
let centerTab = NSTextTab(textAlignment: .Right, location: width - 100, options: [:])
let leftTab = NSTextTab(textAlignment: .Left, location: width - 100 + 0.001, options: [:])
When you have a plan number - no decimal point - you would append text of "value\t" - the value is aligned to the left of the first tab stop, then the tab character takes you to the next tab. If you have a string with a decimal point, split the string into two parts, and then pass the string "firstPart" + "\t + "." "secondPart".
let nString: String
if case let array = item.value.componentsSeparatedByString(".") where array.count == 2 {
nString = array[0] + "\t." + array[1]
} else {
nString = item.value + "\t"
}
// append nString
You can also use this to align numbers some of which are values and others which have trailing percent signs (using similar techniques).
for example:
import Foundation
import UIKit
var str = NSString(string: "saldkjaskldjhf")
var font = UIFont.systemFontOfSize(14.0)
var attributes:[String:AnyObject] = [NSFontAttributeName: font]
var attriStrWithoutParagraph = NSAttributedString(string: str, attributes: attributes)
var size = attriStrWithoutParagraph.boundingRectWithSize(CGSize(width: CGFloat.max, height: CGFloat.max), options: NSStringDrawingOptions.UsesLineFragmentOrigin, context: nil)
var paragraphstyle = NSMutableParagraphStyle()
paragraphstyle.firstLineHeadIndent = CGFloat(20)
attributes[NSParagraphStyleAttributeName] = paragraphstyle
attriStrWithoutParagraph = NSAttributedString(string: str, attributes: attributes)
size = attriStrWithoutParagraph.boundingRectWithSize(CGSize(width: CGFloat.max, height: CGFloat.max), options: NSStringDrawingOptions.UsesLineFragmentOrigin, context: nil)
here is the output:
(0.0,0.0,87.276,16.702)
(0.0,0.0,87.276,16.702)
we can see the result is the same, so the firstlineindent is not considered in why it works like this???
You're specifying very large (effectively infinite) values (CGFloat.max) for the size that you're passing to -boundingRectWithSize:options:. So, the text will never wrap. It will always be laid out in one long line.
Furthermore, the docs for -boundingRectWithSize:options: say:
The origin of the rectangle returned from this method is the first glyph origin.
So, the result is always relative to where the first glyph is placed. You're basically measuring the size of the line. The indent doesn't change the size of the line. It changes where the first glyph is placed, but the result is relative to the first glyph, so it doesn't change the result.
It would change the result if you were providing a real limit for the width and making the paragraph wrap. In that case, the second line would be "outdented" relative to the first line (and the first glyph), so the bounding rectangle would change as you change the firstLineHeadIndent.
You can simply apply the desired indent yourself. That is, after you get the bounding rect, add the indent distance to the X coordinate of the origin (edit: or to the width, if you want a rect encompassing the indent and not just the text positioned by the indent). (Although it's not clear to me what it could mean to indent text in an "infinite" space.)
You could also provide an actual bounding size for your desired destination for the text.