How do I fix clipped ascenders and descenders in a custom font? - swift

I have a custom font that I'm using in my app, Cassandra-Personal. It's a signature style font. Whenever I use it inside of a Text("someText") the font is clipped on the top and bottom due to ascenders and descenders present in the font. A couple of fixes that I've attempted are to change the .frame(), .baselineOffset(), and I also attempted to play around with the kerning/tracking to no avail. In short the text, regardless of the frame, is clipped. Whenever I .baselineOffset() it fixes the one side or the other, but not both sides. For obvious reasons I can't offset both sides simultaneously.
The View
struct GroupListHeaderView: View {
let headerTitle: String
var body: some View {
ZStack(alignment: .center) {
Rectangle().fill(Color(UIColor.yellow))
Text(headerTitle)
.font(Fonts.header)
.frame(height: 100)
}.frame(height: 100)
}
}
Addt'l Supplementary Code
struct Fonts {
// Other fonts removed, not relevant.
static let header = Font.custom(FontName.cassandra.rawValue, size: 30)
}
public enum FontName: String {
// Other cases removed, not relevant.
case cassandra = "CassandraPersonalUse-Regular"
}
Example #1
Baseline Offset of 30
Example #2
Baseline Offset of -30
Example #3
Frame height: 100

I faced with this problem today.
I have a workaround, but this is not the final solution. I add an invisible character '|' before and after the string.
func titleLabel(of title: String, fontName: String, fontSize: CGFloat, textColor: UIColor) -> AttributedString {
let result = NSMutableAttributedString()
var beginAttributes: [NSAttributedString.Key: Any] = [:]
beginAttributes[.font] = UIFont(name: fontName, size: fontSize)
beginAttributes[.foregroundColor] = UIColor.clear
beginAttributes[.baselineOffset] = -fontSize / 4
let beginAttributedString = NSAttributedString(string: "|", attributes: beginAttributes)
result.append(beginAttributedString)
var mainAttributes: [NSAttributedString.Key: Any] = [:]
mainAttributes[.font] = UIFont(name: fontName, size: fontSize)
mainAttributes[.foregroundColor] = textColor
let mainAttributedString = NSAttributedString(string: title, attributes: mainAttributes)
result.append(mainAttributedString)
var endAttributes: [NSAttributedString.Key: Any] = [:]
endAttributes[.font] = UIFont(name: fontName, size: fontSize)
endAttributes[.foregroundColor] = UIColor.clear
endAttributes[.baselineOffset] = fontSize / 4
let endAttributedString = NSAttributedString(string: "|", attributes: endAttributes)
result.append(endAttributedString)
let string = AttributedString(result)
return string
}
// useage:
Text(self.titleLabel(of: "Some Title", fontName: "CassandraPersonalUse-Regular", fontSize: 30, textColor: .black))

This down code should solve your issue:
Note: You can not give a fixed size to ZStack! because it would overridden the needed size for Text! it may get big or small size!
Also I think you would need use backgroundColor for Text instead of using Rectangle().
struct GroupListHeaderView: View {
let headerTitle: String
var body: some View {
Rectangle().fill(Color(UIColor.yellow))
.overlay(Text(headerTitle).font(Font.custom("CassandraPersonalUse-Regular", size: 30)))
}
}

Related

NSTextAttachment.bounds does not work on iOS 15

To add an image attachment to an attributed string I used the following code that works fine before iOS 15:
let textAttachment = NSTextAttachment()
textAttachment.image = UIImage(named:"imageName")!
textAttachment.bounds = CGSize(origin: CGPoint(x: x, y: y), size: CGSize(width: width, height: height))
let textAttachmentString = NSMutableAttributedString(attachment: textAttachment)
let attributedText = NSMutableAttributedString(string: "base text", attributes: [:])
attributedText.append(textAttachmentString)
The problem is that after iOS 15 textAttachment.bounds.origin.x does not work: there is no space between text/image and the origin of the attachment is unchanged.
Probably NSTextAttachmentViewProvider should be used, but I don't know how to procede.
A while back I got around the problem with an inelegant workaround, I wrote a function that inserts an empty text attachment into the content to create an horizontal space between the text and the icon:
func addIconToText(_ toText: NSAttributedString, icon: UIImage, iconOrigin: CGPoint, iconSize: CGSize, atEnd: Bool)->NSAttributedString{
// Inserts a textAttachment containing the given icon into the attributed string (at the beginning or at the end of the text depending on the value of atEnd)
// Constructs an empty text attachment to separate the icon from the text instead of using NSTextAttachment.bounds.origin.x because it does not work with iOS 15
let iconTextAttachment = NSTextAttachment(data: icon.withRenderingMode(.alwaysOriginal).pngData(), ofType: "public.png")
iconTextAttachment.bounds = CGRect(origin: CGPoint(x: 0, y: iconOrigin.y), size: iconSize)
let iconString = NSMutableAttributedString(attachment: iconTextAttachment)
let emptyIconSize = CGSize(width: abs(iconOrigin.x), height: abs(iconOrigin.y))
let emptyIconTextAttachment = NSTextAttachment(image: UIGraphicsImageRenderer(size: emptyIconSize).image(actions: {
con in
UIColor.clear.setFill()
con.fill(CGRect(origin: .zero, size: emptyIconSize))
}))
emptyIconTextAttachment.bounds = CGRect(origin: .zero, size: emptyIconSize)
let emptyIconString = NSMutableAttributedString(attachment: emptyIconTextAttachment)
var finalString: NSMutableAttributedString!
if atEnd{
finalString = NSMutableAttributedString(attributedString: toText)
finalString.append(emptyIconString)
finalString.append(iconString)
}else{
finalString = NSMutableAttributedString(attributedString: iconString)
finalString.append(emptyIconString)
finalString.append(toText)
}
return finalString
}

Set NSAttributedString center vertically each line using baselineOffset

I want to set label's line height and I use minimumLineHeight and maximumLineHeight of NSMutableParagraphStyle
extension UILabel {
func setTextWithLineHeight(text: String?, lineHeight: CGFloat) {
if let text = text {
let style = NSMutableParagraphStyle()
style.maximumLineHeight = lineHeight
style.minimumLineHeight = lineHeight
let attributes: [NSAttributedString.Key: Any] = [
.paragraphStyle: style
.baselineOffset: (lineHeight - font.lineHeight) / 4 // added!!️️🤟
]
let attrString = NSAttributedString(string: text,
attributes: attributes)
self.attributedText = attrString
}
}
}
I add .baselineOffset attribute based on a answer NSAttributedString text always sticks to bottom with big lineHeight , because without it, text is sticks to bottom like this.
image
What I want is set text center vertically so using baselineOffset, I solved the problem. However I wonder why it set baseOffline as (attributes.lineHeight - font.lineHeight) / 4 not (attributes.lineHeight - font.lineHeight) / 2

How to use SF Rounded in UIKit

I have got a label and I spent quite a lot time trying to change the font of my UILabel to SF Rounded Bold.
Things like titleLabel.font = UIFont(name: "SFRounded-Bold", size: 34.0) or titleLabel.font = UIFont(name: "SanFranciscoRounded-Bold ", size: 34.0) don't work.
Is it even possible to use SF Rounded in UIKit? It is not listed in fonts list in scene editor and no one ever asked how to use it but in SwiftUI I can use SF Rounded without any problem.
Swift 5, iOS 13+
Here's an extension version of the other post's answer
import UIKit
extension UIFont {
class func rounded(ofSize size: CGFloat, weight: UIFont.Weight) -> UIFont {
let systemFont = UIFont.systemFont(ofSize: size, weight: weight)
let font: UIFont
if let descriptor = systemFont.fontDescriptor.withDesign(.rounded) {
font = UIFont(descriptor: descriptor, size: size)
} else {
font = systemFont
}
return font
}
}
To use it:
let label = UILabel()
label.text = "Hello world!"
label.font = .rounded(ofSize: 16, weight: .regular)
More concise version of Kevin's answer:
import UIKit
extension UIFont {
class func rounded(ofSize size: CGFloat, weight: UIFont.Weight) -> UIFont {
let systemFont = UIFont.systemFont(ofSize: size, weight: weight)
guard #available(iOS 13.0, *), let descriptor = systemFont.fontDescriptor.withDesign(.rounded) else { return systemFont }
return UIFont(descriptor: descriptor, size: size)
}
}
Usage:
let label = UILabel()
label.font = .rounded(ofSize: 16, weight: .regular)
Expanding on Artem's answer:
extension UIFont {
func rounded() -> UIFont {
guard let descriptor = fontDescriptor.withDesign(.rounded) else {
return self
}
return UIFont(descriptor: descriptor, size: pointSize)
}
}
Now you can add .rounded() to any font to get its rounded variant if it exists.

SKLabelNode Border and Bounds Issue

I am attempting to create text that has an outline. I am currently using SKLabelNode with NSAttributedString, which you can now do in SpriteKit as of iOS 11. The problem is, if the stroke width is too thick, then the outline gets cut off by what appears to be the bounding rectangle of the SKLabelNode. Please see below for the image and code.
extension SKLabelNode {
func addStroke(_ strokeColor:UIColor) {
let font = UIFont(name: self.fontName!, size: self.fontSize)
let attributes:[NSAttributedStringKey:Any] = [.strokeColor: strokeColor, .strokeWidth: 20.0, .font: font!]
let attributedString = NSMutableAttributedString(string: " \(self.text!) ", attributes: attributes)
let label1 = SKLabelNode()
label1.horizontalAlignmentMode = self.horizontalAlignmentMode
label1.text = self.text
label1.zPosition = -1
label1.attributedText = attributedString
self.addChild(label1)
}
}
I looked at expanding the frame of the SKLabelNode serving as the border text, but that is a get-only property. I tried to add leading/trailing spaces, but they appear to be automatically trimmed. Using a negative value for strokeWidth works but creates an inner stroke, I'd prefer to have an outer stroke.
Any ideas? Thanks in advance for the help!
Mike
You shouldn't need to create a separate node for the stroke.
Use negative width values to only render the stroke without fill.
Use .foregroundColor to fill.
You should first check to see if an attributed string is already present to ensure you do not clobber it.
Here is the code:
extension SKLabelNode {
func addStroke(color:UIColor, width: CGFloat) {
guard let labelText = self.text else { return }
let font = UIFont(name: self.fontName!, size: self.fontSize)
let attributedString:NSMutableAttributedString
if let labelAttributedText = self.attributedText {
attributedString = NSMutableAttributedString(attributedString: labelAttributedText)
} else {
attributedString = NSMutableAttributedString(string: labelText)
}
let attributes:[NSAttributedStringKey:Any] = [.strokeColor: color, .strokeWidth: -width, .font: font!, .foregroundColor: self.fontColor!]
attributedString.addAttributes(attributes, range: NSMakeRange(0, attributedString.length))
self.attributedText = attributedString
}
}

Extra blank space when NSTextField is in editing mode

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