Can't subclass UIFont

160 Views Asked by At

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:

  1. How can I add my own custom property (and set it on a per-instance base) on UIFont
  2. Else how do I properly subclass UIFont so that I can add my own properties there?

Thanks!

1

There are 1 best solutions below

2
Rob Napier On BEST ANSWER

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
        }
    }
}