UIFont: How to use Stylistic Alternate character?
Asked Answered
N

2

9

In my App I want to use stylistic alternate font for 'a' and not system font.

Attaching screenshot which explains the different rendering of the font.

enter image description here

How can I enable this behaviour for UILabel and UITextView so that it renders the correct One Storey 'a'?

I did find a YouTube video link which explains exactly this but he is using a custom font and it is hardcoded. I want to use system font only but with this alternate character.

I might be able to hardcode UILabel with the custom character, I am not sure because I want to use System font. I don't want to use custom Font. What about UITextView which is editable? How can we make it use alternate a as and when the user types?

Nelsonnema answered 24/12, 2019 at 6:50 Comment(9)
What app is this a screenshot of? I've been looking for something like that to explore font features.Reorder
Maybe TextEdit. That’s what I use for that purpose.Trader
@Trader Does TextEdit have this typography panel? I'm not familiar with that. I was looking for something that would show all the font features without my writing CoreText code to query it.Reorder
Yes it does. It’s the typography section of the Fonts panel. The nice thing is you can change a feature and use it in your document immediately to see what it does.Trader
@Trader !!! Thank you. I'd never seen that. Exactly what I was looking for.Reorder
It’s always been there. :)Trader
It is a part of MacOS standard Font panel, Go to settings and choose Typography.Nelsonnema
@Trader I was using Sketch and from there I opened Fonts panel to check the font settings. The screenshot was then created to explain the difference with and without Alternative Stylistic Sets. Thanks for the good work. I am reading both of your iOS books and it really helps me.Nelsonnema
@RobNapier So basically maybe any Cocoa app that uses the built-in Cocoa Fonts panel has this. I just resort to TextEdit because it is a pure Cocoa app that everyone has.Trader
R
14

This is a font feature called "Alternative Stylistic Sets" that you can configure with CoreText. Remember that not all fonts have this option, but the system fonts do. You need to figure out which alternative set you want, however.

First, create the font you're interested in:

import CoreText
import UIKit

let baseFont = UIFont.systemFont(ofSize: 72)

Then print out its features:

print(CTFontCopyFeatures(baseFont)!)

Find the section on Alternative Stylistic Sets, and specifically the set you want, "One storey a:"

    {
    CTFeatureTypeIdentifier = 35;
    CTFeatureTypeName = "Alternative Stylistic Sets";
    CTFeatureTypeSelectors =         (
                    {
            CTFeatureSelectorIdentifier = 2;
            CTFeatureSelectorName = "Straight-sided six and nine";
        },
                    {
            CTFeatureSelectorIdentifier = 4;
            CTFeatureSelectorName = "Open four";
        },
                    {
            CTFeatureSelectorIdentifier = 6;
            CTFeatureSelectorName = "Vertically centered colon";
        },
                    {
            CTFeatureSelectorIdentifier = 10;
            CTFeatureSelectorName = "Vertically compact forms";
        },
                    {
            CTFeatureSelectorIdentifier = 12;
            CTFeatureSelectorName = "High legibility";
        },
                    {
            CTFeatureSelectorIdentifier = 14;
            CTFeatureSelectorName = "One storey a";
        },
        ...

The important number is the selector (CTFeatureSelectorIdentifier), 14. With that you can create a new font descriptor and new font:

let descriptor = CTFontDescriptorCreateCopyWithFeature(
    baseFont.fontDescriptor,
    kStylisticAlternativesType as CFNumber,
    14 as CFNumber)

Or you can do this directly in UIKit if it's more convenient:

let settings: [UIFontDescriptor.FeatureKey: Int] = [
    .featureIdentifier: kStylisticAlternativesType,
    .typeIdentifier: 14
]

let descriptor = baseFont.fontDescriptor.addingAttributes([.featureSettings: [settings]])

(Note the somewhat surprising fact that .featureIdentifier is "CTFeatureTypeIdentifier" and .typeIdentifier is "CTFeatureSelectorIdentifier".)

And then you can create a new font (a zero size means to leave the size the same):

let font = UIFont(descriptor: descriptor, size: 0)

You can use that anywhere that accepts a UIFont.

Reorder answered 24/12, 2019 at 17:0 Comment(4)
“the somewhat surprising fact” I call it a bug, myself. :)Trader
@Trader I started with that belief (and originally called it a bug in the answer), but I'm not certain. I opened a feedback anyway (though I don't expect them to respond, or to fix it).Reorder
Yes, mine has been open for years.Trader
@RobNapier Thanks for the perfect in depth explanation. This did work!Nelsonnema
S
1

Here is an extension to simplify choosing an alternative stylistic set for a font, assuming you know its name:

Example usage:

guard let updatedFont = font.withAlternativeStylisticSet(withName: "Alternate y") else {
    fatalError("Alternative stylistic set is undefined")
}

UIFont extension:

import UIKit
import CoreText

extension UIFont {
    
    /// Returns the font, applying an alternative stylistic style set.
    func withAlternativeStylisticSet(withName name: String) -> UIFont? {
        guard let identifier = alternativeStylisticSetIdentifier(withName: name) else {
            return nil
        }
        
        let settings: [UIFontDescriptor.FeatureKey: Int] = [
            .featureIdentifier: kStylisticAlternativesType,
            .typeIdentifier: identifier
        ]
        
        let fontDescriptor = self.fontDescriptor.addingAttributes([.featureSettings: [settings]])
        return UIFont(descriptor: fontDescriptor, size: 0)
    }
    
    /// Returns the identifier for an alternative stylistic set
    private func alternativeStylisticSetIdentifier(withName selectorName: String) -> Int? {
        guard let ctFeatures = CTFontCopyFeatures(self) else {
            return nil
        }
        
        let features = ctFeatures as [AnyObject] as NSArray
        for feature in features {
            if let featureDict = feature as? [String: Any] {
                if let typeName = featureDict[kCTFontFeatureTypeNameKey as String] as? String {
                    if typeName == "Alternative Stylistic Sets" {
                        if let featureTypeSelectors = featureDict[kCTFontFeatureTypeSelectorsKey as String] as? NSArray {
                            for featureTypeSelector in featureTypeSelectors {
                                if let featureTypeSelectorDict = featureTypeSelector as? [String: Any] {
                                    if let name = featureTypeSelectorDict[kCTFontFeatureSelectorNameKey as String] as? String, let identifier = featureTypeSelectorDict[kCTFontFeatureSelectorIdentifierKey as String] as? Int {
                                        if name == selectorName {
                                            return identifier
                                        }
                                    }
                                }
                            }
                        }
                    }
                }
            }
        }
        
        return nil
    }

}
Snack answered 6/8, 2020 at 1:34 Comment(1)
I apologize for the absurd levels of nesting.Snack

© 2022 - 2024 — McMap. All rights reserved.