Can't subclass UIFont - swift

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

Related

ViewModifier to change both font and tracking

I'm trying to change the Font and the tracking of a text in SwiftUI.
So far I have created an extension for Text that sets the tracking.
extension Text {
func setFont(as font: Font.MyFonts) -> Self {
self.tracking(font.tracking)
}
}
I have also created a View Modifier that sets the correct font from my enum
extension Text {
func font(_ font: Font.MyFonts) -> some View {
ModifiedContent(content: self, modifier: MyFont(font: font))
}
}
struct MyFont: ViewModifier {
let font: Font.MyFonts
func body(content: Content) -> some View {
content
.font(.custom(font: font))
}
}
static func custom(font: MyFonts) -> Font {
return Font(font.font as CTFont)
}
I can't seem to find any way to combine these, since the view modifier returns some View and the tracking can only be set on a Text. Is there any clever way to combine these so I can only set the view Modifier?
the enum of fonts look like this
extension Font {
enum MyFonts {
case huge
case large
case medium
/// Custom fonts according to design specs
var font: UIFont {
var font: UIFont?
switch self {
case .huge: font = UIFont(name: AppFontName.book, size: 40)
case .large: font = UIFont(name: AppFontName.book, size: 28
case .medium: font = UIFont(name: AppFontName.book_cursive, size: 18)
}
return font ?? UIFont.systemFont(ofSize: 16)
}
var tracking: Double {
switch self {
case .huge:
return -0.25
default:
return 0
}
}
}
This is the app font name struct that I'm using
public struct AppFontName {
static let book = "Any custom font name"
static let book_cursive = "any custom font name cursive"
}
I still have errors for missed .custom, but anyway seems the solution for your code is to use own Text.font instead of View.font, like
extension Text {
// func font(_ font: Font.MyFonts) -> some View {
// ModifiedContent(content: self, modifier: MyFont(font: font))
// }
func font(_ font: Font.MyFonts) -> Self {
self.font(Font.custom(font: font))
}
}

Swift protocol initializer precludes adding more stored properties to struct

TL;DR:
I want a protocol to provide default init behavior, but the compiler resists adopters adding more stored properties. I solved this with composition instead of inheritance, but what's wrong with my original approach?
Motivation
I want to automate the transformation of objects from design specifications to runtime specs. I use the example of scaling a CGSize but the intent is more general than just geometric layout. (IOW e.g. my solution won't be to adopt/reject/rewrite autolayout.)
Code
You can paste this right into a Playground, and it will run correctly.
protocol Transformable {
var size : CGSize { get } // Will be set automatically;
static var DESIGN_SPEC : CGSize { get } // could be any type.
init(size: CGSize) // Extension will require this.
}
// A simple example of transforming.
func transform(_ s: CGSize) -> CGSize {
CGSize(width: s.width/2, height: s.height/2)
}
// Add some default behavior.
// Am I sinning to want to inherit implementation?
extension Transformable {
init() { self.init(size: transform(Self.DESIGN_SPEC)) }
// User gets instance with design already transformed. No muss, fuss.
}
// Adopt the protocol...
struct T : Transformable {
let size: CGSize
static let DESIGN_SPEC = CGSize(width: 10, height: 10)
}
// ...and use it.
let t = T()
t.size // We get (5,5) as expected.
But every Eden must have its snake. I want a Transformable with another property:
struct T2 : Transformable {
// As before.
let size: CGSize
static let DESIGN_SPEC = CGSize(width: 10, height: 10)
let i : Int // This causes all sorts of trouble.
}
Whaa? Type 'T2' does not conform to protocol 'Transformable'
We have lost the synthesized initializer that sets the size member.
So... we put it back:
struct T3 : Transformable {
// As before.
let size: CGSize
static let DESIGN_SPEC = CGSize(width: 10, height: 10)
let i : Int
init(size: CGSize) {
self.size = size
self.i = 0 // But this is a hard-coded value.
}
}
But now our new member is statically determined. So we try adding another initializer:
struct T4 : Transformable {
// As before.
let size: CGSize
static let DESIGN_SPEC = CGSize(width: 10, height: 10)
let i : Int
init(size: CGSize) { self.size = size ; self.i = 0 }
// Try setting 'i':
init(i: Int) {
self.init() // Get the design spec properly transformed.
self.i = i // 'let' property 'i' may not be initialized directly;
} // use "self.init(...)" or "self = ..." instead
}
Declaring i as var shuts the compiler up. But i is immutable, and I want i that way. Explain to me why what I want is so wrong... This page is too small to include all the variations I tried, but perhaps I have missed the simple answer.

Style placeholder text NSSearchField and NSTextField?

I've been creating a MacOS app and am having trouble styling the font of an NSSearchField (named searchField). My code so far is as follows:
Declared at top of single main viewController class:
let normalTextStyle = NSFont(name: "PT Mono", size: 14.0)
let backgroundColour = NSColor(calibratedHue: 0.6,
saturation: 0.5,
brightness: 0.2,
alpha: 1.0)
let normalTextColour = NSColor(calibratedHue: 0.5,
saturation: 0.1,
brightness: 0.9,
alpha: 1.0)
Declared in viewDidLoad:
searchField.backgroundColor = backgroundColour
searchField.textColor = normalTextColour
searchField.font = normalTextStyle
searchField.centersPlaceholder = false
searchField.currentEditor()?.font = normalTextStyle
let attrStr = NSMutableAttributedString(string: "Search...",
attributes: [NSForegroundColorAttributeName: normalTextColour])
searchField.placeholderAttributedString = attrStr
Generally this works except in one condition: when the search field has focus but no search term has been entered. In this case the placeholder text has the correct colour but the font seems to return to the default (Helvetica 12 point?). As soon as something is typed in or the field loses focus, then the correct font is used once more.
I have tried with no luck looking through the Apple docs for some kind of font or colour settings not currently being set. I have fiddled about with all the font setting I could find in the interface builder, including cocoa bindings and the normal settings in the inspector.
Do I need to set some value of the currentEditor? I am guessing not because the font is changed once text is entered.. I am stuck - can anyone help?
EDIT: I've now tried with an NSTextField and the results are the same. Does anyone have any ideas?
I eventually managed to find an answer. I created a new class TitleTextFormatter of type Formatter, which is 'is intended for subclassing. A custom formatter can restrict the input and enhance the display of data in novel ways'. All I needed to do was override certain default functions to get what I needed:
import Cocoa
class TitleTextFormatter: Formatter {
override func string(for obj: Any?) -> String? {
/*
* this function receives the object it is attached to.
* in my case it only ever receives an NSConcreteAttributedString
* and returns a plain string to be formatted by other functions
*/
var result: String? = nil
if let attrStr = obj as? NSAttributedString {
result = attrStr.string
}
return result
}
override func getObjectValue( _ obj: AutoreleasingUnsafeMutablePointer<AnyObject?>?,
for string: String,
errorDescription error: AutoreleasingUnsafeMutablePointer<NSString?>?) -> Bool {
/*
* this function in general is overridden to provide an object created
* from the input string. in this instance, all I want is an attributed string
*/
let titleParagraphStyle = NSMutableParagraphStyle()
titleParagraphStyle.alignment = .center
let titleAttributes = [NSAttributedStringKey.foregroundColor: NSColor.mainText,
NSAttributedStringKey.font: NSFont.titleText,
NSAttributedStringKey.paragraphStyle: titleParagraphStyle]
let titleAttrStr = NSMutableAttributedString(string: string,
attributes: titleAttributes)
obj?.pointee = titleAttrStr
return true
}
override func attributedString(for obj: Any,
withDefaultAttributes attrs: [NSAttributedStringKey : Any]? = nil) -> NSAttributedString? {
/*
* is overridden to show that an attributed string is created from the
* formatted object. this happens to duplicate what the previous function
* does, only because the object I want to create from string is an
* attributed string
*/
var titleAttrStr: NSMutableAttributedString?
if let str = string(for: obj) {
let titleParagraphStyle = NSMutableParagraphStyle()
titleParagraphStyle.alignment = .center
let titleAttributes = [NSAttributedStringKey.foregroundColor: NSColor.mainText,
NSAttributedStringKey.font: NSFont.titleText,
NSAttributedStringKey.paragraphStyle: titleParagraphStyle]
titleAttrStr = NSMutableAttributedString(string: str,
attributes: titleAttributes)
}
return titleAttrStr
}
}
and then in viewDidLoad I added the following:
let titleTextFormatter = TitleTextFormatter()
titleTextField.formatter = titleTextFormatter

Adding failing optional initializer in Swift in extension

I'm trying to add a convenience failing initializer to UIFont as an extension. So UIFont already has a failing initializer:
open class UIFont : NSObject, NSCopying {
...
public init?(name fontName: String, size fontSize: CGFloat)
...
}
So I want to create a convenience initializer that would call this one, something similar to the code below:
public convenience init?(name fontName: String, size fontSize: CGFloat, weight fontWeight: String) {
self.init(name: fontName + "-" + fontWeight, size: fontSize)
}
So far so good, this works perfectly. Now I want to add some custom logic, for example:
public convenience init?(name fontName: String, size fontSize: CGFloat, weight fontWeight: String) {
if let font = self.init(name: fontName + "-" + fontWeight, size: fontSize) {
self = font
}
else if let font = self.init(name: fontName, size: fontSize) {
self = font
}
else {
//
// Add some failback code
//
return nil
}
}
This is where problems start. What I want to do is a basic convenience API, but I cannot manage to get the font conditionally initialized. In the upper example there is a syntax error in if let font = self.init line, and it says: Initializer for conditional binding must have Optional type, not '()'. But this initializer actually is optional, as defined by UIFont class (see above), so I don't see the problem. I tried returning font instead of assigning it to self, but does not make a difference.
So my question is: How to correctly implement conditional logic in a failing initializer?
It could be that I am doing something wrong here, or this could just be Swift missing feature/bug?
Swift initialisation is one of the most confusing part of the language, especially if you're coming from Objective-C. The short answer is that:
Unlike Objective-C initializers, Swift initializers do not return a value.
(from https://developer.apple.com/library/content/documentation/Swift/Conceptual/Swift_Programming_Language/Initialization.html).
Therefore when you write:
public convenience init?(
name fontName: String, size fontSize: CGFloat, weight fontWeight: String
) {
let font = self.init(name: fontName + "-" + fontWeight, size: fontSize)
the type of font is Void. Yeah, I know, it does raise an eyebrow, since it's different model than in ObjC. In Swift you delegate the work of initialisation to another initialiser, but you're not managing the instance directly. So you can call it a missing feature.
The easy workaround is to make a factory method instead of initialiser:
public static func from(
name fontName: String, size fontSize: CGFloat, weight fontWeight: String
) -> UIFont? {
if let font = self.init(name: fontName + "-" + fontWeight, size: fontSize) {
return font
}
else if let font = self.init(name: fontName, size: fontSize) {
return font
}
else {
//
// Add some failback code
//
return nil
}
}

Swift: Programmatically make UILabel bold without changing its size?

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