Swift: Programmatically make UILabel bold without changing its size? - swift

I have a UILabel created programmatically. I would like to make the text of the label bold without specifying font size. So far I have only found:
UIFont.boldSystemFont(ofSize: CGFloat)
This is what I have exactly:
let titleLabel = UILabel()
let fontSize: CGFloat = 26
titleLabel.font = UIFont.boldSystemFont(ofSize: titleLabelFontSize)
But this way I am also setting the size. I would like to avoid that. Is there a way?
If there is no way, what would be a good workaround in Swift?
Thank you!

Why not just:
titleLabel.font = UIFont.boldSystemFont(ofSize: titleLabel.font.pointSize)

To just make the Font bold without altering the font size you could create an extension like this (which is based off the answer here:
extension UIFont {
func withTraits(traits:UIFontDescriptorSymbolicTraits...) -> UIFont {
let descriptor = self.fontDescriptor()
.fontDescriptorWithSymbolicTraits(UIFontDescriptorSymbolicTraits(traits))
return UIFont(descriptor: descriptor, size: 0)
}
func bold() -> UIFont {
return withTraits(.TraitBold)
}
}
So that way you could use it like this:
let titleLabel = UILabel()
titleLabel.font = titleLabel.font.bold() //no need to include size!
Update for Swift 4 syntax:
extension UIFont {
func withTraits(traits:UIFontDescriptorSymbolicTraits...) -> UIFont {
let descriptor = self.fontDescriptor
.withSymbolicTraits(UIFontDescriptorSymbolicTraits(traits))
return UIFont(descriptor: descriptor!, size: 0)
}
func bold() -> UIFont {
return withTraits(traits: .traitBold)
}
}

Related

Can't subclass UIFont

I use custom fonts in my iOS application and have setup the fonts like so:
private enum MalloryProWeight: String {
case book = "MalloryMPCompact-Book"
case medium = "MalloryMPCompact-Medium"
case bold = "MalloryMPCompact-Bold"}
extension UIFont {
enum Caption {
private static var bookFont: UIFont {
UIFont(name: MalloryProWeight.book.rawValue, size: 1)!
}
private static var mediumFont: UIFont {
UIFont(name: MalloryProWeight.medium.rawValue, size: 1)!
}
private static var boldFont: UIFont {
UIFont(name: MalloryProWeight.bold.rawValue, size: 1)!
}
static var book: UIFont {
return bookFont.withSize(10)
}
static var medium: UIFont {
mediumFont.withSize(10)
}
static var bold: UIFont {
boldFont.withSize(10)
}
}
So that at the call site I can do the following:
UIFont.Caption.bold
This works well; I have an NSAttributed extension that takes in. UIFont and color and returns an attributed string = so it all fits nicely.
However, I now have a requirement to set the LetterSpacing and LineHeight on each of my fonts.
I don't want to go and update the NSAttributed extension to take in these values to set them - I ideally want them accessible from UIFont
So, I tried to subclass UIFont to add my own properties to it - like so:
class MrDMyCustomFontFont: UIFont {
var letterSpacing: Double?
}
And use it like so
private static var boldFont: UIFont {
MrDMyCustomFontFont(name: MalloryProWeight.bold.rawValue, size: 1)!
}
However the compiler complains and I am unsure how to resolve it:
Argument passed to call that takes no arguments
So my question is two part:
How can I add my own custom property (and set it on a per-instance base) on UIFont
Else how do I properly subclass UIFont so that I can add my own properties there?
Thanks!
You can't subclass UIFont because it is bridged to CTFont via UICTFont. That's why the init methods are marked "not inherited" in the header. It's not a normal kind of class.
You can easily add a new property to UIFont, but it won't work the way you want it to. It'll be exactly what you asked for: per-instance. But it won't be copied, so the instance returned from boldFont.withSize(10) won't have the same value as boldFont. If you want the code, this is how you do it:
private var letterSpacingKey: String? = nil
extension UIFont {
var letterSpacing: Double? {
get {
(objc_getAssociatedObject(self, &letterSpacingKey) as? NSNumber)?.doubleValue
}
set {
objc_setAssociatedObject(self, &letterSpacingKey, newValue.map(NSNumber.init(value:)),
.OBJC_ASSOCIATION_RETAIN)
}
}
}
And then you can set it:
let font = UIFont.boldSystemFont(ofSize: 1)
font.letterSpacing = 1
print(font.letterSpacing) // Optional(1)
But you'll lose it anytime a derived font is created:
let newFont = font.withSize(10)
print(newFont.letterSpacing) // nil
So I don't think you want that.
But most of this doesn't really make sense. What would you do with these properties? "Letter spacing" isn't a font characteristic; it's a layout/style characteristic. Lying about the font's height metric is probably the wrong tool as well; configuring that is also generally a paragraph characteristic.
What you likely want is a "Style" that tracks all the things in question (font, spacing, paragraph styles, etc) and can be applied to an AttributedString. Luckily that already exists in iOS 15+: AttributeContainer. Prior to iOS 15, you can just use a [NSAttributedString.Key: Any].
Then, instead of an (NS)AttributedString extension to merge your font in, you can just merge your Container/Dictionary directly (which is exactly how it's designed to work).
extension AttributeContainer {
enum Caption {
private static var boldAttributes: AttributeContainer {
var container = AttributeContainer()
container.font = UIFont(name: MalloryProWeight.bold.rawValue, size: 1)!
container.expansion = 1
let paragraphStyle = NSMutableParagraphStyle()
paragraphStyle.lineSpacing = 1.5
container.paragraphStyle = paragraphStyle
return container
}
static var bold: AttributeContainer {
var attributes = boldAttributes
attributes.font = boldAttributes.font.withsize(10)
return attributes
}
}
}

Autoshrink fontsize of UILabeltext siwft

I have a multiline UILabel text which expands dynamically according to the text length. I have setup proper auto layout in storyboard and grows bigger if the text is lengthy.
But i want to reduce the font size to 24 if its 2-line and to 20 if its 3-line text.
If text is single line then, font size should be 34.
I tried minimum font scale = 20/34 = .58
but no use. Please suggest a solution
You could use a switch statement to determine the font size when a certain number of lines have been after changing the label's text.
Using the actualNumberOfLines() algorithm from https://stackoverflow.com/a/60993649/12783209 allows you to redeclare the font for this label whenever necessary.
func changeFontSizeIfNeeded(on label: inout UILabel){
switch label.actualNumberOfLines{
case 2:
label.font = label.font.withSize(24)
case 3:
label.font = label.font.withSize(20)
default:
//Change this to be the default font size
label.font = label.font.withSize(30)
}
}
And add this extension to your project:
extension UILabel{
func actualNumberOfLines()-> Int {
// You have to call layoutIfNeeded() if you are using autoLayout
self.layoutIfNeeded()
let myText = self.text! as NSString
let rect = CGSize(width: self.bounds.width, height: CGFloat.greatestFiniteMagnitude)
let labelSize = myText.boundingRect(with: rect, options: .usesLineFragmentOrigin, attributes: [NSAttributedString.Key.font: self.font as Any], context: nil)
return Int(ceil(CGFloat(labelSize.height) / self.font.lineHeight))
}
}

Fontsize of attributedString doesn't change

I have an attributedString and want to change only it's fontsize. To do that, I use another method that I found on StackOverflow. For most cases, this is working, but somehow it doesn't change the whole attributedString in one case.
Method to change the size:
/**
*A struct with static methods that can be useful for your GUI
*/
struct GuiUtils {
static func setAttributedStringToSize(attributedString: NSAttributedString, size: CGFloat) -> NSMutableAttributedString {
let mus = NSMutableAttributedString(attributedString: attributedString)
mus.enumerateAttribute(.font, in: NSRange(location: 0, length: mus.string.count)) { (value, range, stop) in
if let oldFont = value as? UIFont {
let newFont = oldFont.withSize(size)
mus.addAttribute(.font, value: newFont, range: range)
}
}
return mus
}
}
Working:
label.attributedText = GuiUtils.setAttributedStringToSize(attributedString: attributedString, size: fontSize)
Not working:
mutableAttributedString.replaceCharacters(in: gapRange, with: filledGap)
label.attributedText = GuiUtils.setAttributedStringToSize(attributedString: mutableAttributedString.replaceCharacters, size: fontSize)
Somehow, the replaced text does not change its size.
Excuse me, but do you sure that your filledGap attributed string has font attribute? Because if it doesn't – this part will not be handled by the enumerateAttribute block.
In this case your fix will be just to set any font to the whole filledGap string, to be sure that it's part will be handled by the enumerateAttribute block.

How to change font size of NSTableHeaderCell

I am trying to change the font size of my NSTableView within my code to allow the user to change it to their liking.
I was successful by changing the font size of each NSTableCellView but failed to do so by the header cells.
I was trying to do it like this
let headerCell = NSTableHeaderCell()
let font = NSFont(name: "Arial", size: 22.0)
headerCell.stringValue = "firstname"
headerCell.font = font
customerTable.tableColumns[0].headerCell = headerCell
The stringValue of the header cell will be set accordingly but the size does not change. How can I change the font size of my headers?
Thanks
Oliver
So, finally I was only able to solve this with subclassing NSTableHeaderCell. It was somehow strange as Swift and Cocoa always tend to favor composition over inheritance but anyway.
Swift 3.1
final class CustomTableHeaderCell : NSTableHeaderCell {
override init(textCell: String) {
super.init(textCell: textCell)
self.font = NSFont.boldSystemFont(ofSize: 18) // Or set NSFont to your choice
self.backgroundColor = NSColor.white
}
required init(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func draw(withFrame cellFrame: NSRect, in controlView: NSView) {
// skip super.drawWithFrame(), since that is what draws borders
self.drawInterior(withFrame: cellFrame, in: controlView)
}
override func drawInterior(withFrame cellFrame: NSRect, in controlView: NSView) {
let titleRect = self.titleRect(forBounds: cellFrame)
self.attributedStringValue.draw(in: titleRect)
}
}
Changing attributedStringValue will do the trick
override func viewDidLoad() {
super.viewDidLoad()
tableView.tableColumns.forEach { (column) in
column.headerCell.attributedStringValue = NSAttributedString(string: column.title, attributes: [NSFontAttributeName: NSFont.boldSystemFont(ofSize: 18)])
// Optional: you can change title color also jsut by adding NSForegroundColorAttributeName
}
}
https://i.stack.imgur.com/IXJyu.png
You can create a NSTableHeaderCell subclass and implement the property you want to change.
In Objective-C (I'm not good at Swift):
#implementation CustomTableHeaderCell
-(NSFont *)font {
return [NSFont fontWithName:#"Arial" size:22];
}
// you can alse custom textColor
-(NSColor *)textColor {
return [NSColor redColor];
}
#end
Assign CustomTableHeaderCell:
CustomTableHeaderCell *headerCell = [[CustomTableHeaderCell alloc] init];
headerCell.stringValue = #"Header title";
self.tableView.tableColumns[0].headerCell = headerCell;
In Cocoa, there are many things you can't change its style by cell.font = ..., you need to create a subcalss.

tvOS: Anyway to display a subtitle outside of the AVPlayer?

So the scenario is that there is a view where the user can enable/disable subtitles in an app I'm helping to develop.
On that view there is a sample text saying "This is what captions look like", and at the moment it's just a basic, unstyled UILabel. Ideally I would like it to be styled in a similar manner to how the user has customized their captions in the System Settings.
Is this possible in any way? I've envisioned two possible method:
Create an AVPlayer instance and a .vtt file with the text, load it into the view and pause the player. I'm not sure this is possible with a sample video (and it would somehow have to be transparent as there is an image behind the sample sub text).
Somehow get all the styling (font, size, background color, etc) the user has set for their subtitle and create an attributed string to match that
Method 2 seems like the most feasible way, but I don't know if we have access to those settings in code.
So I figured it out! It basically makes use a combination of the Media Accessibility API, which allows you to get the values the user has chosen for their captions/subtitle settings, Attributed Strings, and a subclass UILabel (although this could maybe be substituted with a UITextView as that will allow you to set it's UIEdgeInsets natively)
So, first, the subclass is to allow the UILabel to be inset. This is because captions can have a background color AND a text highlight color and without the inset, the text highlight is all you see. So the function the subclass is simple:
class InsetUILabel: UILabel {
override func drawTextInRect(rect: CGRect) {
let inset: CGFloat = 15
let insets: UIEdgeInsets = UIEdgeInsets(top: inset, left: inset/2, bottom: inset, right: inset/2)
super.drawTextInRect(UIEdgeInsetsInsetRect(rect, insets))
}
}
And for generating the actual label. This uses a label called textSample, but you can obviously make it a little more general.
import MediaAccessibility
func styleLabel(sampleText: String) {
let domain = MACaptionAppearanceDomain.User
// Background styling
let backgroundColor = UIColor(CGColor: MACaptionAppearanceCopyWindowColor(domain, nil).takeRetainedValue())
let backgroundOpacity = MACaptionAppearanceGetWindowOpacity(domain, nil)
textSample.layer.backgroundColor = backgroundColor.colorWithAlphaComponent(backgroundOpacity).CGColor
textSample.layer.cornerRadius = MACaptionAppearanceGetWindowRoundedCornerRadius(domain, nil)
// Text styling
var textAttributes = [String:AnyObject]()
let fontDescriptor = MACaptionAppearanceCopyFontDescriptorForStyle(domain, nil, MACaptionAppearanceFontStyle.Default).takeRetainedValue()
let fontName = CTFontDescriptorCopyAttribute(fontDescriptor, "NSFontNameAttribute") as! String
let fontColor = UIColor(CGColor: MACaptionAppearanceCopyForegroundColor(domain, nil).takeRetainedValue())
let fontOpacity = MACaptionAppearanceGetForegroundOpacity(domain, nil)
let textEdgeStyle = MACaptionAppearanceGetTextEdgeStyle(domain, nil)
let textHighlightColor = UIColor(CGColor: MACaptionAppearanceCopyBackgroundColor(domain, nil).takeRetainedValue())
let textHighlightOpacity = MACaptionAppearanceGetBackgroundOpacity(domain, nil)
let textEdgeShadow = NSShadow()
textEdgeShadow.shadowColor = UIColor.blackColor()
let shortShadowOffset: CGFloat = 1.5
let shadowOffset: CGFloat = 3.5
switch(textEdgeStyle) {
case .None:
textEdgeShadow.shadowColor = UIColor.clearColor()
case .DropShadow:
textEdgeShadow.shadowOffset = CGSize(width: -shortShadowOffset, height: shortShadowOffset)
textEdgeShadow.shadowBlurRadius = 6
case .Raised:
textEdgeShadow.shadowOffset = CGSize(width: 0, height: shadowOffset)
textEdgeShadow.shadowBlurRadius = 5
case .Depressed:
textEdgeShadow.shadowOffset = CGSize(width: 0, height: -shadowOffset)
textEdgeShadow.shadowBlurRadius = 5
case .Uniform:
textEdgeShadow.shadowColor = UIColor.clearColor()
textAttributes[NSStrokeColorAttributeName] = UIColor.blackColor()
textAttributes[NSStrokeWidthAttributeName] = -2.0
default:
break
}
textAttributes[NSFontAttributeName] = UIFont(name: fontName, size: (textSample.font?.pointSize)!)
textAttributes[NSForegroundColorAttributeName] = fontColor.colorWithAlphaComponent(fontOpacity)
textAttributes[NSShadowAttributeName] = textEdgeShadow
textAttributes[NSBackgroundColorAttributeName] = textHighlightColor.colorWithAlphaComponent(textHighlightOpacity)
textSample.attributedText = NSAttributedString(string: sampleText, attributes: textAttributes)
}
Now the text highlight section makes use of shadows, with values I think look pretty good, but you might want to tweak them a tiny bit. Hope this helps!