On this thread, Core Text calculate letter frame in iOS, they were able to calculate the frame of each glyph very precisely using Core Text. The final rects hug the actual drawn glyphs perfectly.
Using NSLayoutManager's boundingRectForGlyphRange:inTextContainer: doesn't seem to return the glyph bounding boxes that precisely:
And the returned rects don't fully enclose more complex fonts (Zapfino example):
Does anyone know how to replicate the results from the above mentioned discussion without going into Core Text under iOS 7 only apps?
Thanks.
I've found a solution for this issue or at least sort of
Problem: boundingRectForGlyphRange results is off in case of RTL text.
So in case of RTL text is detected only:
Using NSLayoutManager method locationForGlyphAtIndex, for each letter in the range. These are the starting points of each glyph in the range.
Using these points I adjust the boundingRect correctly.
Here is an outline of it:
CGPoint endPoint = ((NSValue *)pointsOfGlyps.firstObject).CGPointValue;
CGPoint startPoint = ((NSValue *)pointsOfGlyps.lastObject).CGPointValue;
boundingRect.origin.x = startPoint.x;
boundingRect.size.width = endPoint.x - startPoint.x + letterWidth;
letterWidth is an approximation of a letter width calculated as the delta between two consecutive starting points.
Thanks for posting this Q!!
I do realize that this post is from 2014 (woah 8 years). I stumbled upon this very same bug in... 2022, just leaving, below, my final solution.
Please note that performing the Width calculation based on Glyph Location doesn't work for multiple lines (and the Character Average might also be off):
func rtlAccurateBoundingRect(forGlyphRange glyphRange: NSRange, in container: NSTextContainer) -> CGRect {
let output = boundingRect(forGlyphRange: glyphRange, in: container)
let characterRange = characterRange(forGlyphRange: glyphRange, actualGlyphRange: nil)
/// Determine the Bounds for each line, and calculate the enclusing Width / Origin
///
var maxWidth: CGFloat = .zero
var locationMin: CGFloat = .greatestFiniteMagnitude
enumerateLineFragments(forGlyphRange: glyphRange) { (fragRect, usedRect, textContainer, fragGlyphRange, stop) in
maxWidth = max(usedRect.width, maxWidth)
locationMin = min(usedRect.minX, locationMin)
}
/// Finally adjust the resulting CGRect
///
var updated = output
updated.origin.x = locationMin
updated.size.width = maxWidth
return updated
}
Related
so I need to be able to move the mouse to any given point (x and y). I'm trying to do it with CGDisplayMoveCursor while it moves the cursors it does not place it where I expect it. I have two monitors in my set up. I get the frame via NSScreen.screens[i].frame and these are their frames.
Built-in Retina Display
NSRect (0.0, 0.0, 1440.0, 900.0)
SwitchResX4 - LG HDR WQHD
NSRect (-1186.0, 900.0, 3840.0, 1600.0)
Then when I receive the new point I use NSRect.contains(NSPoint) to find which of the two monitors contain that point. That part works as expected.
But then I want to move the cursor to that point but it doesn't work as you would expect (the cursor moves but not to the points as you would expect based on the frames). Here is the code for that.
let point = NSPoint(x: x, y: y)
guard let monitor = Monitor.displayIdWithPoint(point) else {
SendlogCallbackEvent("D", "", "Found nil in monitor with point", #function, #file, #line)
}
if CGDisplayMoveCursorToPoint(monitor.directDisplayId, point) == CGError.failure {
SendlogCallbackEvent("D", "", "CGError.failer to move cursor", #function, #file, #line)
}
I have read that it probably has to do with how CG and NS handle coordinate system in a different way but I haven't been able to figure how to make it work. How do I "convert" my NSPoint to a CGPoint so it works with CGDisplayMove... function. Or what is the best way to do this.
This snippet of your code:
let point = NSPoint(x: x, y: y)
guard let monitor = Monitor.displayIdWithPoint(point) else {
SendlogCallbackEvent("D", "", "Found nil in monitor with point", #function, #file, #line)
}
gives me the impression that your point you're looking up is a point in the window system coordinate space (the coordinate space in which those two window frames are described).
But looking at the documentation of CGDisplayMoveCursorToPoint(_:_:), we see (emphasis mine):
point
The coordinates of a point in local display space. The origin is the upper-left corner of the specified display.
So I think you'll need to take your point, and transform it so that it's relative to the origin of your desired display:
let pointRelativeToScreen = point - monitor.frame.origin
if CGDisplayMoveCursorToPoint(monitor.directDisplayId, pointRelativeToScreen) == CGError.failure { ... }
Alternatively, there might be some other API which expects a cursor point in expressed in the window system's coordinates, which would automatically plop it onto the right screen for you. I don't have access to Xcode right now, so I can't check for this myself.
I've been trying to implement an infinite background animation, which should change between 4 images of equal height and then repeat the sequence. However, it does not seem to work properly.
Note anchorPoint = CGPoint(x: 0.5, y: 0)
func updateBackground(currentTime: TimeInterval){
var delta: CGFloat = 0.0
if lastUpdate != nil {
delta = CGFloat(currentTime - lastUpdate)
}
//First increment position
activeBackground1.position.y += delta*backgroundVelocity
activeBackground2.position.y += delta*backgroundVelocity
//Detect bounds surpass
if activeBackground1.position.y > activeBackground1.size.height + screen.height/2 {
lastSky = (lastSky + 1)%4
sky1 = SKTexture(imageNamed: "sky" + String(lastSky))
activeBackground1.texture = sky1
//Reposition: background1 new position is equal to minus the entire height of
//background2 + its y size.
activeBackground1.position.y = -abs(activeBackground2.size.height-activeBackground2.position.y)
}
if activeBackground2.position.y > activeBackground2.size.height + screen.height/2 {
lastSky = (lastSky + 1)%4
sky1 = SKTexture(imageNamed: "sky" + String(lastSky))
activeBackground2.texture = sky1
activeBackground2.position.y = -abs(activeBackground1.size.height-activeBackground1.position.y)
}
}
The update algorithm works fine, but when it is needed to reposition one of the two background, it seems there is an offset of about 10.0 CGFloat from one background to another. What am I doing wrong?
EDIT: It turned out that the error was located in my image, which presented some blank rows and therefore generated visualisation glitches. So my code works perfectly.
I do the test and most likely you should use something like:
activeBackground2.position.y = activeBackground1.size.height + activeBackground1.position.y
instead of
activeBackground2.position.y = -abs(activeBackground1.size.height-activeBackground1.position.y)
I did this example and it works correctly: https://github.com/Maetschl/SpriteKitExamples/tree/master/InfiniteBackground/InfiniteBackground
Feel free to see and use.
Your problem is floating point math causing rounding errors. I am on a phone right now so I can’t wrote code, but what you want to do is have 1 parent SKNode to handle your entire background.
Add your background slivers to the parent node.
You then place the moving action on the parent only.
As each sliver leaves the screen, you take the sliver, and move it to the end of the other slivers.
This jumping should always be done with integer math, leaving no holes.
Basically:
The floating point moving math is done on the parent node.
The integer based tile jumping is done on each of the slivers.
I'm trying to fit some text into a fixed-width label (actually the width depends on the screen size, but cannot change within the app), and expect UIKit to cleverly use a combination of font resizing and word-wrapping to get the proper result. However, it doesn't seem to work that way. Considering a UILabel with the following constraints:
aspect ratio = 1:1
label.width = 0.7 * parentView.width (all other relevant constraints set, no errors or warnings in IB)
and the following code:
label.font = label.font.withSize(100)
label.adjustsFontSizeToFitWidth = true
label.lineBreakMode = .byClipping
label.numberOfLines = 0
label.text = "Shooter team"
I would be hoping that it would resize the text and make it fit into two lines: "Shooter" and "team" (or, since the text could be anything, split it properly into words). However, when I set label.lineBreakMode to .byWordWrapping, it doesn't resize the text at all and so only one big letter is displayed (note: I'm using a big font size for it to resize because I can't know in advance how big the text is going to be, since the size depends on the screen size). Any other value for .lineBreakMode results in the text being resized but split into "Shoote" and "r team", which looks dumb. Changing autoshrink to e.g. Minimum font size = 8 doesn't seem to have any effect. See screenshot below.
Any suggestion of how I can get the proper splitting/resizing? I may have used the wrong terms for my searches but I haven't found any answer :-|
(Note: there will be a different question about how I can get the border of the encompassing view to be a nice circle prior to the view being displayed :-| )
First, to take advantage of adjustsFontSizeToFitWidth, you must also give it a scale factor (the smallest size you're willing to let the label shrink to). So if, for example, your label's font is sized at 30, you could let it shrink down to 24:
someLabel.font = UIFont(name: "someFont", size: 30)
someLabel.adjustsFontSizeToFitWidth = true
someLabel.minimumScaleFactor = (24/30)
Second, you may want to consider using an attributed title for your label to take advantage of paragraph styling. Paragraph styling lets you play with hyphenation rules:
let paragraphStyle = NSMutableParagraphStyle()
paragraphStyle.hyphenationFactor = 1.0
let attributedTitle = NSAttributedString(string: "Shooter team", attributes: [NSAttributedString.Key.foregroundColor: UIColor.red, NSAttributedString.Key.paragraphStyle: paragraphStyle])
someLabel.attributedText = attributedTitle
For lack of a better solution I've written the following function that empirically tries to find a font size that doesn't split a word in the middle. It's hackish but at least it works... Comments, suggestions or any better solution welcome!
func findFittingFont(for label: String) -> UIFont {
// note: the 'split' function is a personal addition that splits a string on a regex
// in this case, split between a non-word char and a word char
let words = label.split(pattern: "(?<=\\W)(?=\\w)")
var font = myLabel.font!
// the width we don't want to overflow
let maxWidth = myLabel.frame.width
var fontSize = myLabel.frame.height
var tooBig: Bool
repeat {
tooBig = false
font = font.withSize(fontSize)
// check for each word whether the rendered width is larger than the max width
for word in words {
// calculate the rendered width with the current font
let width = (word as NSString).size(withAttributes: [.font: font]).width
if width > maxWidth {
tooBig = true
// decrease the size by a factor
fontSize *= 0.9
break
}
}
// go on as long as there's an overflowing word
}
while tooBig
return font
}
I am using CoreText to render multiple columns of text. However, when I set the first letter of the 1st paragraph to a bold, larger font than the rest of the text, I incur 2 issues (both visible in the attached image):
The spacing underneath the first line is too big (I understand that this is because the 1st character could be a g,y,p,q etc.
Lines below the first line now do not line up with corresponding lines in the next column.
Any advice on how to overcome these 2 issues would be greatly appreciated, thank you.
According to the documentation kCTParagraphStyleSpecifierMaximumLineHeight should have solved the problem, but unfortunately does not seem to work at least on IOS 4.3.
CTParagraphStyleSetting theSettings[5] =
{
{ kCTParagraphStyleSpecifierParagraphSpacing, sizeof(CGFloat), &spaceBetweenParaghraphs },
{ kCTParagraphStyleSpecifierParagraphSpacingBefore, sizeof(CGFloat), &topSpacing },
{ kCTParagraphStyleSpecifierLineSpacing, sizeof(CGFloat), &spaceBetweenLines },
{ kCTParagraphStyleSpecifierMinimumLineHeight, sizeof(CGFloat), &lineHeight},
{ kCTParagraphStyleSpecifierMaximumLineHeight, sizeof(CGFloat), &lineHeight}
};
CTParagraphStyleRef paragraphStyle = CTParagraphStyleCreate(theSettings, 5);
To be fair documentation says it's available in OS v10.5 and later.
kCTParagraphStyleSpecifierMaximumLineHeight:
The maximum height that any line in the frame will occupy, regardless of the font size or size of any attached graphic. Glyphs and graphics exceeding this height will overlap neighboring lines. A maximum height of 0 implies no line height limit. This value is always nonnegative.Type: CGFloat.
Default: 0.0.
Application: CTFramesetter.
Available in Mac OS X v10.5 and later.
Declared in CTParagraphStyle.h.
It seems the only way to fix this is with a workaround, which is to create 3 frames for the first column,1 for the W, 1 for the rest of the first sentence and 1 for the rest of the first column.
Hello ist there a way to split a String for UITableView at a specific line to put the "rest" (the second part of the data) in an own cell
NSString *data;
CGsize *size = [data sizeOfStringWithFont:[UIFont systemFontOfSize:14] constrainToWidth:280.0];
if the size.height e.g. is greater than 1500 i want to split the string at this line position!
thank you
Use "constrainedToSize" (instead of just to width) and render as much as you can.
If you really want to take exactly the text that would not fit, you're going to have to do essentially a search, adding a word at a time and then doing the size check to see how high you have gotten. You could start out with a rough estimate by doing the whole string constrained to something only one line high with boundless width (say 999999) and then divide up the width into however many rows you are wishing to allow to get a rough starting point for adding/removing words from the string (it will not be exact because of word wrapping).
Fundamentally though it seems wierd to take the leftover text and put it in another cell. Are you really sure you don't simply want to change the height of the cell with the text to allow it to fit the whole thing?
I think Kendall has the right idea, but the constrained sizes should be reversed to get the exact height based on word wrapping. Take a sample CGSize that is the same width as your cell, but with a height larger than the max height you expect. In the sample code below, textSize will contain the height of your string as it would appear in your cell with an unbounded height.
CGSize sz = CGSizeMake (
yourCellWidth,
999999.0f );
CGSize textSize = [yourString sizeWithFont:yourCellfont
constrainedToSize:sz
lineBreakMode:UILineBreakModeWordWrap];
If the height is greater than 1500, you could start picking off substrings (substringWithRange) from the end and measuring them like above until you get something >= the remainder above 1500 that was returned by textSize.