Extra blank space when NSTextField is in editing mode - swift

I made an NSTextField subclass which adjusts its width with its content length. The idea (of overriding intrinsicContentSize) is from this question.
override var intrinsicContentSize: NSSize {
if isEditing {
if let fieldEditor =
self.window?.fieldEditor(false, for: self) as?
NSTextView
{
let rect = fieldEditor.layoutManager!.usedRect(
for: fieldEditor.textContainer!
)
let size = rect.size
return size
}
}
let size = self.cell!.cellSize
return size
}
However, there's an extra blank area after the last character. If I set the size.width manually (size.width -= 3.5, for example), the text will offset back and forth (horizontally) during editing.
I don't see this quirk in macOS's Finder when renaming its sidebar items. How to get rid of the extra space without making the text "jumping"?
Update 1:
I added a demo on GitHub.
Update 2:
Tried setting NSTextView's textContainerInset to a size of 0, 0, which doesn't solve the problem.
Update 3:
Updated the repo with #Михаил Масло 's answer. The text still jiggles during editing. The original implementation can be viewed by checking out the initial commit.

You can calculate size of the string with defined font directly try this (I've used your code in TableTextField.swift):
class TableTextField: NSTextField {
...
override var intrinsicContentSize: NSSize {
return stringValue.size(withConstraintedHeight: 1000, font: fieldEditor.font!)
}
...
}
extension String {
func size(withConstraintedHeight height: CGFloat, font: NSFont) -> CGSize {
let constraintRect = CGSize(width: .greatestFiniteMagnitude, height: height)
let boundingBox = self.boundingRect(with: constraintRect, options: .usesLineFragmentOrigin,
attributes: [NSAttributedStringKey.font: font], context: nil)
let size = boundingBox.size
return CGSize(width: size.width + 0.5, height: size.height)
}
}
I didn't tend to make it safe so probably you can make it better and exclude force-unwrap and following errors

Related

Getting UILabel Height by giving String, UIFont and Number of lines

I have found several good answers about how to calculate the height of UILabel with a given text and the UIFont:
extension String {
func heightWithConstrainedWidth(width: CGFloat, font: UIFont) -> CGFloat {
let constraintRect = CGSize(width: width, height: .greatestFiniteMagnitude)
let boundingBox = self.boundingRect(with: constraintRect, options: [.usesLineFragmentOrigin, .usesFontLeading], attributes: [NSAttributedStringKey.font: font], context: nil)
return boundingBox.height
}
}
However, I want to go a little bit further and calculate the height with a given text, font and number of lines. Is that even possible? So, if the height is supposed to be in 2 lines, I would like the height for 2 lines.
Thank you.
In Swift 5:
Rob pointed me in the right direction and I saw that the number of lines can be achieved by using the UIFont.lineHeight.
Solution for getting the right height with a given text, width and max number of lines.
func height(width: CGFloat, font: UIFont, maxLines: CGFloat = 0) -> CGFloat {
//Calculating height
let constraintRect = CGSize(width: width, height: .greatestFiniteMagnitude)
let boundingBox = self.boundingRect(with: constraintRect,
options: [.usesLineFragmentOrigin, .usesFontLeading],
attributes: [NSAttributedStringKey.font: font], context: nil)
let height = ceil(boundingBox.height)
if maxLines > 0 {
let lines = height / font.lineHeight
if lines >= maxLines {
return (boundingBox.height / lines) * maxLines
}
}
return height
}

How can I show the rest of a character that has gone slightly outside of UITextView?

I have a UITextView which changes size depending on the text the user inputs (the purple box), which is inside another UIView (the red box).
But when using a handwritten style font like this, the end character sometimes gets cut off at the edge:
I have tried used text1.clipsToBounds = false but that didn't show the edge of the character. Is there a way to show the full character without changing the width of the text view?
Also here is the code I am using to set up the text view:
let text1 = UITextView()
text1.text = ""
text1.font = UIFont(name: "Gotcha", size: 27)
text1.frame = CGRect(x: 10, y: 5, width: 70, height: 50)
text1.isScrollEnabled = false
text1.delegate = self
text1.textAlignment = .center
text1.isEditable = false
text1.isSelectable = false
holdingView.addSubview(text1)
The frame then gets updated with this function, and whenever the text is changed:
func adjustTextViewSize(_ textView: UITextView) {
let maxWidth = 300
let newSize = textView.sizeThatFits(CGSize(width: maxWidth, height: CGFloat.greatestFiniteMagnitude))
textView.frame = CGRect(x: (textView.frame.minX), y: (textView.frame.minY), width: newSize.width, height: newSize.height)
}
Thanks!
Update:
I solved this by adding an extra 30px to newSize.width for any font that is handwritten:
if fontFile?.isHandwritten == true {
currentView.widthConstraint?.constant = newSize.width + 30
currentTextHoldingView.widthConstraint?.constant = newSize.width + 30
}
call this function for get height according to string length
extension String {
func height(withConstrainedWidth width: CGFloat, font: UIFont) -> CGFloat {
let constraintRect = CGSize(width: width, height: .greatestFiniteMagnitude)
let boundingBox = self.boundingRect(with: constraintRect, options: .usesLineFragmentOrigin,
attributes: [NSAttributedString.Key.font: font], context: nil)
return ceil(boundingBox.height)
}
}

How to fix UITextView text content position in Swift

I have UIView class and in this class have UITextView for use as inputText
In textViewDidChange from row 2 through 5 with any inter, or adding text, I add the UITextView bounds.size.height as much as need.
The problem is, in rows 3 and 5, first line text does not appear.
To be roughly a row above its own space.
And is created from the bottom of the empty space, without scrolling.
But the text in the 4th row have exact position
class QuestionCell: UIView, UITextViewDelegate{
var paragraph = UITextView()
func setParagraph(){
let frame = CGRect(x: 50,
y: 0,
width: forground.width-55,
height: forground.height)
paragraph.frame = frame
paragraph.isEditable = true
paragraph.delegate = self
// forground is some UIView in this View
forground.addSubview(paragraph)
}
func textViewDidChange(_ textView: UITextView) {
paragraph.bounds.size.height = paragraph.paragraphHeight
}
}
extension UITextView{
var paragraphHeight: CGFloat{
let fixedWidth = self.width
let newSize = self.sizeThatFits(CGSize.init(width: fixedWidth,
height: CGFloat(MAXFLOAT)))
var newFrame = self.frame
newFrame.size = CGSize.init(width: CGFloat(fmaxf(Float(newSize.width),
Float(fixedWidth))),
height: newSize.height)
return newSize.height
}
}
During Creating you need to specify :
paragraph.isScrollEnabled = false
After creating, you may set it back.

Swift 3 - UITextView total line counter

Good day all hope your well.
This possibly seems like a repeat question but nothing is working for me.
I have looked at multiple instances of this question and I have tried many different things and it doesn't solve my problem.
I have a textview that is constrained to 11 Lines and then becomes scrollable..... I want to get the total line count of the text content not the display size on the screen all the solutions just provide me with 11 lines but the true line count is +- 30 lines.Even if I show 5 Lines of text it returns 11
How can i find out the line count of all the text collectivly.
Any help would be much appreciated
Use this extension for solve your problem!
extension String {
func lines(font : UIFont, width : CGFloat) -> Int {
let constraintRect = CGSize(width: width, height: .greatestFiniteMagnitude);
let boundingBox = self.boundingRect(with: constraintRect, options: .usesLineFragmentOrigin, attributes: [NSFontAttributeName: font], context: nil);
return Int(boundingBox.height/font.lineHeight);
}
}
Here what I'm doing
extension String {
func lines() -> CGFloat {
let font = //your font
let width = //constrained width to which text can expand
let constraintRect = CGSize(width: width, height: .greatestFiniteMagnitude)
let boundingBox = self.boundingRect(with: constraintRect, options: .usesLineFragmentOrigin, attributes: [NSFontAttributeName: font], context: nil)
return boundingBox.height/font.lineHeight
}
}
This will give you the number of Lines that are actually a string is taking.
Hope it helps!!

vertically align text in a CATextLayer?

I am working on a CATextLayer that I want to use in both Mac and iOS. Can I control the vertical alignment of the text within the layer?
In this particular case, I want to center it vertically -- but information about other vertical alignments would also be of interest.
EDIT: I found this, but I can't make it work.
The correct answer, as you've already found, is here in Objective-C and works for iOS. It works by subclassing the CATextLayer and overriding the drawInContext function.
However, I've made some improvements to the code, as shown below, using David Hoerl's code as a basis. The changes come solely in recalculating the vertical position of the text represented by the yDiff. I've tested it with my own code.
Here is the code for Swift users:
class LCTextLayer : CATextLayer {
// REF: http://lists.apple.com/archives/quartz-dev/2008/Aug/msg00016.html
// CREDIT: David Hoerl - https://github.com/dhoerl
// USAGE: To fix the vertical alignment issue that currently exists within the CATextLayer class. Change made to the yDiff calculation.
override func draw(in context: CGContext) {
let height = self.bounds.size.height
let fontSize = self.fontSize
let yDiff = (height-fontSize)/2 - fontSize/10
context.saveGState()
context.translateBy(x: 0, y: yDiff) // Use -yDiff when in non-flipped coordinates (like macOS's default)
super.draw(in: context)
context.restoreGState()
}
}
It is an late answer, but I have the same question these days, and have solved the problem with following investigation.
Vertical align depends on the text you need to draw, and the font you are using, so there is no one way solution to make it vertical for all cases.
But we can still calculate the vertical mid point for different cases.
According to apple's About Text Handling in iOS, we need to know how the text is drawn.
For example, I am trying to make vertical align for weekdays strings: Sun, Mon, Tue, ....
For this case, the height of the text depends on cap Height, and there is no descent for these characters. So if we need to make these text align to the middle, we can calculate the offset of the top of cap character, e.g. The position of the top of character "S".
According to the the figure below:
The top space for the capital character "S" would be
font.ascender - font.capHeight
And the bottom space for the capital character "S" would be
font.descender + font.leading
So we need to move "S" a little bit off the top by:
y = (font.ascender - font.capHeight + font.descender + font.leading + font.capHeight) / 2
That equals to:
y = (font.ascender + font.descender + font.leading) / 2
Then I can make the text vertical align middle.
Conclusion:
If your text does not include any character exceed the baseline, e.g. "p", "j", "g", and no character over the top of cap height, e.g. "f". The you can use the formula above to make the text align vertical.
y = (font.ascender + font.descender + font.leading) / 2
If your text include character below the baseline, e.g. "p", "j", and no character exceed the top of cap height, e.g. "f". Then the vertical formula would be:
y = (font.ascender + font.descender) / 2
If your text include does not include character drawn below the baseline, e.g. "j", "p", and does include character drawn above the cap height line, e.g. "f". Then y would be:
y = (font.descender + font.leading) / 2
If all characters would be occurred in your text, then y equals to:
y = font.leading / 2
Maybe to late for answer, but you can calculate size of text and then set position of textLayer. Also you need to put textLayer textAligment mode to "center"
CGRect labelRect = [text boundingRectWithSize:view.bounds.size options:NSStringDrawingUsesLineFragmentOrigin attributes:#{ NSFontAttributeName : [UIFont fontWithName:#"HelveticaNeue" size:17.0] } context:nil];
CATextLayer *textLayer = [CATextLayer layer];
[textLayer setString:text];
[textLayer setForegroundColor:[UIColor redColor].CGColor];
[textLayer setFrame:labelRect];
[textLayer setFont:CFBridgingRetain([UIFont fontWithName:#"HelveticaNeue" size:17.0].fontName)];
[textLayer setAlignmentMode:kCAAlignmentCenter];
[textLayer setFontSize:17.0];
textLayer.masksToBounds = YES;
textLayer.position = CGPointMake(CGRectGetMidX(view.bounds), CGRectGetMidY(view.bounds));
[view.layer addSublayer:textLayer];
Swift 3 version for regular and attributed strings.
class ECATextLayer: CATextLayer {
override open func draw(in ctx: CGContext) {
let yDiff: CGFloat
let fontSize: CGFloat
let height = self.bounds.height
if let attributedString = self.string as? NSAttributedString {
fontSize = attributedString.size().height
yDiff = (height-fontSize)/2
} else {
fontSize = self.fontSize
yDiff = (height-fontSize)/2 - fontSize/10
}
ctx.saveGState()
ctx.translateBy(x: 0.0, y: yDiff)
super.draw(in: ctx)
ctx.restoreGState()
}
}
thank #iamktothed, it works. following is swift 3 version:
class CXETextLayer : CATextLayer {
override init() {
super.init()
}
override init(layer: Any) {
super.init(layer: layer)
}
required init(coder aDecoder: NSCoder) {
super.init(layer: aDecoder)
}
override func draw(in ctx: CGContext) {
let height = self.bounds.size.height
let fontSize = self.fontSize
let yDiff = (height-fontSize)/2 - fontSize/10
ctx.saveGState()
ctx.translateBy(x: 0.0, y: yDiff)
super.draw(in: ctx)
ctx.restoreGState()
}
}
There is nothing stopping you from creating a CALayer hierarchy with a generic CALayer (container) that has the CATextLayer as a sublayer.
Instead of calculating font sizes for the CATextLayer, simply calculate the offset of the CATextLayer inside the CALayer so that it is vertically centred. If you set the alignment mode of the text layer to centred and make the width of the text layer the same as the enclosing container it also centres horizontally.
let container = CALayer()
let textLayer = CATextLayer()
// create the layer hierarchy
view.layer.addSublayer(container)
container.addSublayer(textLayer)
// Setup the frame for your container
...
// Calculate the offset of the text layer so that it is centred
let hOffset = (container.frame.size.height - textLayer.frame.size.height) * 0.5
textLayer.frame = CGRect(x:0.0, y: hOffset, width: ..., height: ...)
The sublayer frame is relative to its parent, so the calculation is fairly straightforward. No need to care at this point about font sizes. That's handled by your code dealing with the CATextLayer, not in the layout code.
Updating this thread (for single and multi line CATextLayer), combining some answers above.
class VerticalAlignedTextLayer : CATextLayer {
func calculateMaxLines() -> Int {
let maxSize = CGSize(width: frame.size.width, height: CGFloat(Float.infinity))
let font = UIFont(descriptor: self.font!.fontDescriptor, size: self.fontSize)
let charSize = font.lineHeight
let text = (self.string ?? "") as! NSString
let textSize = text.boundingRect(with: maxSize, options: .usesLineFragmentOrigin, attributes: [NSAttributedString.Key.font: font], context: nil)
let linesRoundedUp = Int(ceil(textSize.height/charSize))
return linesRoundedUp
}
override func draw(in context: CGContext) {
let height = self.bounds.size.height
let fontSize = self.fontSize
let lines = CGFloat(calculateMaxLines())
let yDiff = (height - lines * fontSize) / 2 - lines * fontSize / 10
context.saveGState()
context.translateBy(x: 0, y: yDiff) // Use -yDiff when in non-flipped coordinates (like macOS's default)
super.draw(in: context)
context.restoreGState()
}
}
gbk's code works. below is gbk's code updated for XCode 8 beta 6. Current as of 1 Oct 2016
Step 1. Subclass CATextLayer. In the code below I've named the subclass "MyCATextLayer" Outside your view controller class copy/paste the below code.
class MyCATextLayer: CATextLayer {
// REF: http://lists.apple.com/archives/quartz-dev/2008/Aug/msg00016.html
// CREDIT: David Hoerl - https://github.com/dhoerl
// USAGE: To fix the vertical alignment issue that currently exists within the CATextLayer class. Change made to the yDiff calculation.
override init() {
super.init()
}
required init(coder aDecoder: NSCoder) {
super.init(layer: aDecoder)
}
override func draw(in ctx: CGContext) {
let height = self.bounds.size.height
let fontSize = self.fontSize
let yDiff = (height-fontSize)/2 - fontSize/10
ctx.saveGState()
ctx.translateBy(x: 0.0, y: yDiff)
super.draw(in: ctx)
ctx.restoreGState()
}
}
Step 2. Within your view controller class in your ".swift" file, create your CATextLabel. In the code example I've named the subclass "MyDopeCATextLayer."
let MyDopeCATextLayer: MyCATextLayer = MyCATextLayer()
Step 3. Set your new CATextLayer with desired text/color/bounds/frame.
MyDopeCATextLayer.string = "Hello World" // displayed text
MyDopeCATextLayer.foregroundColor = UIColor.purple.cgColor //color of text is purple
MyDopeCATextLayer.frame = CGRect(x: 0, y:0, width: self.frame.width, height: self.frame.height)
MyDopeCATextLayer.font = UIFont(name: "HelveticaNeue-UltraLight", size: 5) //5 is ignored, set actual font size using ".fontSize" (below)
MyDopeCATextLayer.fontSize = 24
MyDopeCATextLayer.alignmentMode = kCAAlignmentCenter //Horizontally centers text. text is automatically centered vertically because it's set in subclass code
MyDopeCATextLayer.contentsScale = UIScreen.main.scale //sets "resolution" to whatever the device is using (prevents fuzzyness/blurryness)
Step 4. done
The code for Swift 3, based on code #iamktothed
If you use an attributed string for setting font properties, than you can use function size() from NSAttributedString to calculate height of string.
I think this code also resolve the problems described by #Enix
class LCTextLayer: CATextLayer {
override init() {
super.init()
}
override init(layer: Any) {
super.init(layer: layer)
}
required init(coder aDecoder: NSCoder) {
super.init(layer: aDecoder)
}
override open func draw(in ctx: CGContext) {
if let attributedString = self.string as? NSAttributedString {
let height = self.bounds.size.height
let stringSize = attributedString.size()
let yDiff = (height - stringSize.height) / 2
ctx.saveGState()
ctx.translateBy(x: 0.0, y: yDiff)
super.draw(in: ctx)
ctx.restoreGState()
}
}
}
I slightly modified this answer by #iamkothed. The differences are:
text height calculation is based on NSString.size(with: Attributes). I don't know if it's an improvement over (height-fontSize)/2 - fontSize/10, but I like to think that it is. Although, in my experience, NSString.size(with: Attributes) doesn't always return the most appropriate size.
added invertedYAxis property. It was useful for my purposes of exporting this CATextLayer subclass using AVVideoCompositionCoreAnimationTool. AVFoundation operates in "normal" y axis, and that's why I had to add this property.
Works only with NSString. You can use Swift's String class though, because it automatically casts to NSString.
It ignores CATextLayer.fontSize property and completely relies on CATextLayer.font property which MUST be a UIFont instance.
class VerticallyCenteredTextLayer: CATextLayer {
var invertedYAxis: Bool = true
override func draw(in ctx: CGContext) {
guard let text = string as? NSString, let font = self.font as? UIFont else {
super.draw(in: ctx)
return
}
let attributes = [NSAttributedString.Key.font: font]
let textSize = text.size(withAttributes: attributes)
var yDiff = (bounds.height - textSize.height) / 2
if !invertedYAxis {
yDiff = -yDiff
}
ctx.saveGState()
ctx.translateBy(x: 0.0, y: yDiff)
super.draw(in: ctx)
ctx.restoreGState()
}
}
class CenterTextLayer: CATextLayer {
override func draw(in ctx: CGContext) {
#if os(iOS) || os(tvOS)
let multiplier = CGFloat(1)
#elseif os(OSX)
let multiplier = CGFloat(-1)
#endif
let yDiff = (bounds.size.height - ((string as? NSAttributedString)?.size().height ?? fontSize)) / 2 * multiplier
ctx.saveGState()
ctx.translateBy(x: 0.0, y: yDiff)
super.draw(in: ctx)
ctx.restoreGState()
}
}
Credit goes to:
https://github.com/cemolcay/CenterTextLayer
So there is no "direct" way of doing this but you can accomplish the same thing by using text metrics:
http://developer.apple.com/library/ios/#documentation/UIKit/Reference/NSString_UIKit_Additions/Reference/Reference.html
... for example, find the size of the text then use that information to place it where you want in the parent layer. Hope this helps.
You need to know where CATextLayer will put the baseline of your text. Once you know that, offset the coordinate system within the layer, i.e. adjust bounds.origin.y by the difference between where the baseline normally sits and where you want it to be, given the metrics of the font.
CATextLayer is a bit of a black box and finding where the baseline will sit is a bit tricky - see my answer here for iOS - I've no idea what the behaviour is on Mac.
I'd like to propose a solution that takes multiline wrapping inside the available box into account:
final class CACenteredTextLayer: CATextLayer {
override func draw(in ctx: CGContext) {
guard let attributedString = string as? NSAttributedString else { return }
let height = self.bounds.size.height
let boundingRect: CGRect = attributedString.boundingRect(
with: CGSize(width: bounds.width,
height: CGFloat.greatestFiniteMagnitude),
options: NSString.DrawingOptions.usesLineFragmentOrigin,
context: nil)
let yDiff: CGFloat = (height - boundingRect.size.height) / 2
ctx.saveGState()
ctx.translateBy(x: 0.0, y: yDiff)
super.draw(in: ctx)
ctx.restoreGState()
}
}
Swift 5.3 for macOS
class VerticallyAlignedTextLayer : CATextLayer {
/* Credit - purebreadd - 6/24/2020
https://stackoverflow.com/questions/4765461/vertically-align-text-in-a-catextlayer
*/
func calculateMaxLines() -> Int {
let maxSize = CGSize(width: frame.size.width, height: frame.size.width)
let font = NSFont(descriptor: self.font!.fontDescriptor, size: self.fontSize)
let charSize = floor(font!.capHeight)
let text = (self.string ?? "") as! NSString
let textSize = text.boundingRect(with: maxSize, options: .usesLineFragmentOrigin, attributes: [NSAttributedString.Key.font: font!], context: nil)
let linesRoundedUp = Int(floor(textSize.height/charSize))
return linesRoundedUp
}
override func draw(in context: CGContext) {
let height = self.bounds.size.height
let fontSize = self.fontSize
let lines = CGFloat(calculateMaxLines())
let yDiff = -(height - lines * fontSize) / 2 - lines * fontSize / 6.5 // Use -(height - lines * fontSize) / 2 - lines * fontSize / 6.5 when in non-flipped coordinates (like macOS's default)
context.saveGState()
context.translateBy(x: 0, y: yDiff)
super.draw(in: context)
context.restoreGState()
}
}
Notice I am dividing fontSize by 6.5, which seems to work better for my application of this solution. Thanks #purebreadd!
As best I can tell, the answer to my question is "No."