Home > Blockchain >  Can't subclass UIFont
Can't subclass UIFont

Time:07-14

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!

CodePudding user response:

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