swiftuikituifont

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:

  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!


Solution

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