I have to program a see test for an optician. So they need that the Numbers are exactly have a specific height.
He has a IPad ans streams it to a TV. I know i must consider the PPI of the TV. Here is my Code :
func calcPoints() -> Float {
// he gives the Visus with a TextField
let visus = Float(textFieldVisus.text!)
// Here you put in the PPI of the device
let ppi = Float(textFieldDPI.text!)
// Calculate the lenght of the Number
let lenght = ((0.29 * 5) / visus!) * 5
// Calculate the Points (because TextFiels work with Points)
let points = ((ppi! / 25.4) * lenght) * 0.75
// here you divide with 2 because you have a retina Display on the IPad
return (points / 2)
}
func passeUIan() {
// Now i can give the Points to the TextField
textField1.bounds.size.width = CGFloat(calcPoints())
textField1.bounds.size.height = CGFloat(calcPoints())
textField1.font = UIFont(name: (textField1.font?.fontName)!, size: CGFloat(calcPoints()))
}
but when i measure off the lenght on the TV it is wrong.
Normaly it must be 7.25mm but it is approximately 9mm.
i dont know what is wrong. I search for this problem since 2 weeks...
You need to first familiarize yourself with the different font metrics. The font size is usually (but not always) the difference between the ascender and descenders. For your purpose, the height of an uppercase letter is called the "cap height" and the height of a lowercase letter is called the "x-height".
There's no formula to translate the font size to either cap height or x-height. Their relationships different from fonts to fonts, and even variants (bold, italic, small caps, display, book) within a font.
The function below use binary search to look for a point size that matches your desired height (in inches):
// desiredHeight is in inches
func pointSize(inFontName fontName: String, forDesiredCapHeight desiredHeight: CGFloat, ppi: CGFloat) -> CGFloat {
var minPointSize: CGFloat = 0
var maxPointSize: CGFloat = 5000
var pointSize = (minPointSize + maxPointSize) / 2
// Finding for exact match may not be possible. UIFont may round off
// the sizes. If it's within 0.01 in (0.26 mm) of the desired height,
// we consider that good enough
let tolerance: CGFloat = 0.01
while let font = UIFont(name: fontName, size: pointSize) {
let actualHeight = font.capHeight / ppi * UIScreen.main.scale
if abs(actualHeight - desiredHeight) < tolerance {
return pointSize
} else if actualHeight < desiredHeight {
minPointSize = pointSize
} else {
maxPointSize = pointSize
}
pointSize = (minPointSize + maxPointSize) / 2
}
return 0
}
Example: find the point size that make the capital letter 1-inch tall in Helvetica. (326 is the PPI for iPhone 6 / 6S / 7, which I used to test):
let size = pointSize(inFontName: "Helvetica", forDesiredCapHeight: 1, ppi: 326)
label.font = UIFont(name: fontName, size: size)
label.text = "F"
(tips: a UILabel handles font size much better than a UITextField)
Related
Is there a way to get the left yAxis width? I'm drawing custom markers and would like to avoid to draw over the left yAxis.
There is a solution for the android version of this library but it doesn't work in iOS because of different render methods: How to get the width of y-axis label in MPAndroidChart
You can retrieve the axis width by the requiredSize() method.
chartView.leftAxis.requiredSize()
This is what requiredSize() method actually does:
#objc open func requiredSize() -> CGSize
{
let label = getLongestLabel() as NSString
var size = label.size(withAttributes: [NSAttributedString.Key.font: labelFont])
size.width += xOffset * 2.0
size.height += yOffset * 2.0
size.width = max(minWidth, min(size.width, maxWidth > 0.0 ? maxWidth : size.width))
return size
}
Use this only after you set the chart data, cause the chart will automatically calculate the width with the longest label in your axis.
Hope it helps
Does this give you what you need?
let yourBarChartView = BarChartView()
let leftAxis = yourBarChartView.leftAxis
let width = leftAxis.axisLineWidth
Hope this helps!
The app I'm working on supports hundreds of different fonts. Some of these fonts, particularly the script fonts, have significant ascenders and descenders. When sizeToFit() is called on a UITextView with some of these fonts, I end up with significant top and bottom padding (the image on the left). The goal is to end up with the image on the right, such that the tallest glyph is aligned flush with the top of the text view's bounding box.
Here's the log for the image above:
Point Size: 59.0
Ascender: 70.21
Descender: -33.158
Line Height: 103.368
Leading: 1.416
TextView Height: 105.0
My first thought was to look at the height of each glyph in the first line of text, and then calculate the offset between the top of the container and the top of the tallest glyph. Then I could use textContainerInset to adjust the top margin accordingly.
I tried something like this in my UITextView subclass:
for location in 0 ..< lastGlyphIndexInFirstLine {
let glphyRect = self.layoutManager.boundingRect(forGlyphRange: NSRange(location: location, length: 1), in: self.textContainer)
print(glphyRect.size.height) // prints 104.78399999999999 for each glyph
}
Unfortunately, this doesn't work because boundRect(forGlyphRange:in:) doesn't appear to return the rect of the glyph itself (I'm guessing this is always the same value because it's returning the height of the line fragment?).
Is this the simplest way to solve this problem? If it is, how can I calculate the distance between the top of the text view and the top of the tallest glyph in the first line of text?
This doesn't appear to be possible using TextKit, but it is possible using CoreText directly. Specifically, CGFont's getGlyphBBoxes returns the correct rect in glyph space units, which can then be converted to points relative to the font size.
Credit goes to this answer for making me aware of getGlyphBBoxes as well as documenting how to convert the resulting rects to points.
Below is the complete solution. This assumes you have a UITextView subclass with the following set beforehand:
self.contentInset = .zero
self.textContainerInset = .zero
self.textContainer.lineFragmentPadding = 0.0
This function will now return the distance from the top of the text view's bounds to the top of the tallest used glyph:
private var distanceToGlyphs: CGFloat {
// sanity
guard
let font = self.font,
let fontRef = CGFont(font.fontName as CFString),
let attributedText = self.attributedText,
let firstLine = attributedText.string.components(separatedBy: .newlines).first
else { return 0.0 }
// obtain the first line of text as an attributed string
let attributedFirstLine = attributedText.attributedSubstring(from: NSRange(location: 0, length: firstLine.count)) as CFAttributedString
// create the line for the first line of attributed text
let line = CTLineCreateWithAttributedString(attributedFirstLine)
// get the runs within this line (there will typically only be one run when using a single font)
let glyphRuns = CTLineGetGlyphRuns(line) as NSArray
guard let runs = glyphRuns as? [CTRun] else { return 0.0 }
// this will store the maximum distance from the baseline
var maxDistanceFromBaseline: CGFloat = 0.0
// iterate each run
for run in runs {
// get the total number of glyphs in this run
let glyphCount = CTRunGetGlyphCount(run)
// initialize empty arrays of rects and glyphs
var rects = Array<CGRect>(repeating: .zero, count: glyphCount)
var glyphs = Array<CGGlyph>(repeating: 0, count: glyphCount)
// obtain the glyphs
self.layoutManager.getGlyphs(in: NSRange(location: 0, length: glyphCount), glyphs: &glyphs, properties: nil, characterIndexes: nil, bidiLevels: nil)
// obtain the rects per-glyph in "glyph space units", each of which needs to be scaled using units per em and the font size
fontRef.getGlyphBBoxes(glyphs: &glyphs, count: glyphCount, bboxes: &rects)
// iterate each glyph rect
for rect in rects {
// obtain the units per em from the font ref so we can convert the rect
let unitsPerEm = CGFloat(fontRef.unitsPerEm)
// sanity to prevent divide by zero
guard unitsPerEm != 0.0 else { continue }
// calculate the actual distance up or down from the glyph's baseline
let glyphY = (rect.origin.y / unitsPerEm) * font.pointSize
// calculate the actual height of the glyph
let glyphHeight = (rect.size.height / unitsPerEm) * font.pointSize
// calculate the distance from the baseline to the top of the glyph
let glyphDistanceFromBaseline = glyphHeight + glyphY
// store the max distance amongst the glyphs
maxDistanceFromBaseline = max(maxDistanceFromBaseline, glyphDistanceFromBaseline)
}
}
// the final top margin, calculated by taking the largest ascender of all the glyphs in the font and subtracting the max calculated distance from the baseline
return font.ascender - maxDistanceFromBaseline
}
You can now set the text view's top contentInset to -distanceToGlyphs to achieve the desired result.
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.
I'm making an app chat and I have a problem with the chat message's size.
I've calculate my message's height like this and I've got the correct height
maximumMessageWidth = 300.0
let size = CGSize(width: maximumMessageWidth, height: 1000.0)
let messageAttributeText = messageBody.attributedText
let height = messageAttributeText?.boundingRect(with: size, options: .usesLineFragmentOrigin, context: nil).height
To avoid the special case like this image. I have to calculate the width
It has the big space after my message
I want it to look like this:
This is the code I use to calculate the message width (I used the correct height which I've calculated the width)
let size = CGSize(width: 1000.0, height: height)
let messageAttributeText = messageBody.attributedText
var width = messageAttributeText?.boundingRect(with: size, options: .usesLineFragmentOrigin, context: nil).width
but width in this code seem not right. Because I think it does care about my height property. It assume that my text is in only 1 line.
But I want to calculate the width, so that my text will `fill the whole label' just like the second image
Does anyone know how to calculate the width in my case ?
You can do that by casting String to NSString
var string = "Hello, World"
let nsString = string as NSString
let size = nsString.size(attributes: [NSFontAttributeName: UIFont.systemFont(ofSize: 14)])
The size will give you width and height of the string.
You can apply this to your NSAttributedStrings string property.
After a bit of research, I find out this:
The calculate width code doesn't work right because it do not care about the height property.
If height = 100 or height = 1000 it will return the same result because it assume to draw all the text in just one line.
So to calculate the width, I used binary search
var minWidth:CGFloat = 0.0
var maxWidth = maxWidth
while true {
if (minWidth >= maxWidth) {
width = minWidth
break
}
let testWidth = (maxWidth + minWidth) / 2.0
if calculateMessageHeight(width: testWidth) > messageHeight {
minWidth = testWidth + 2.0
continue
} else {
maxWidth = testWidth - 2.0
continue
}
I am pretty new to drawing programmatically. I've managed to draw a line graph, complete with points, lines, auto-scaling axes, and axis labels. In other screens, I can change characteristics of the graph and when I return to the screen, I refresh it with setNeedsDisplay() in the viewWillAppear function of the containing viewController. The lines are redrawn perfectly when I do this.
The new data that is added in other screens may require rescaling the axes. The problem is that when the graph is redrawn, the number labels on the axes are just added to the graph, without removing the old ones, meaning that some labels may be overwritten, while some old ones just remain there next to the new ones.
I think I see why this happens, in that I am creating a label and adding a subview, but not removing it. I guess I figured that since the lines are erased and redrawn, the labels would be, too. How do I cleanly relabel my axes? Is there a better way to do this? My function for creating the labels is listed below. This function is called by drawRect()
func createXAxisLabels(interval: Float, numIntervals: Int) {
let xstart: CGFloat = marginLeft
let yval: CGFloat = marginTop + graphHeight + 10 // 10 pts below the x-axis
var xLabelVals : [Float] = [0]
var xLabelLocs : [CGFloat] = [] // gives the locations for each label
for i in 0...numIntervals {
xLabelLocs.append(xstart + CGFloat(i) * graphWidth/CGFloat(numIntervals))
xLabelVals.append(Float(i) * interval)
}
if interval >= 60.0 {
xUnits = "Minutes"
xUnitDivider = 60
}
for i in 0...numIntervals {
let label = UILabel(frame: CGRectMake(0, 0, 50.0, 16.0))
label.center = CGPoint(x: xLabelLocs[i], y: yval)
label.textAlignment = NSTextAlignment.Center
if interval < 1.0 {
label.text = "\(Float(i) * interval)"
} else {
label.text = "\(i * Int(interval/xUnitDivider))"
}
label.font = UIFont.systemFontOfSize(14)
label.textColor = graphStructureColor
self.addSubview(label)
}
}
drawRect should just draw the rectangle, not change the view hierarchy. It can be called repeatedly. Those other labels are other views and do their own drawing.
You have a couple of options
Don't use labels -- instead just draw the text onto the rect.
-or-
Add/remove the labels in the view controller.
EDIT (from the comments): The OP provided their solution
I just replaced my code inside the for loop with:
let str : NSString = "\(xLabelVals[i])"
let paraAttrib = NSMutableParagraphStyle()
paraAttrib.alignment = NSTextAlignment.Center
let attributes = [
NSFontAttributeName: UIFont.systemFontOfSize(14),
NSForegroundColorAttributeName: UIColor.whiteColor(),
NSParagraphStyleAttributeName: paraAttrib]
let xLoc : CGFloat = CGFloat(xLabelLocs[i] - 25)
let yLoc : CGFloat = yval - 8.0
str.drawInRect(CGRectMake(xLoc, yLoc, 50, 16), withAttributes: attributes)