UIFont monospaced digits + small caps - ios

I'm trying to create a UIFont with the following attributes:
Upper Case Small Caps
Lower Case Small Caps
Monospaced Digits
I'm using the system font (San Francisco), which does supports all theses features.
As far as I know, the only way to do it is to use multiple UIFontDescriptor.
Here is the code I'm using:
extension UIFont {
var withSmallCaps: UIFont {
let upperCaseFeature = [
UIFontDescriptor.FeatureKey.featureIdentifier : kUpperCaseType,
UIFontDescriptor.FeatureKey.typeIdentifier : kUpperCaseSmallCapsSelector
]
let lowerCaseFeature = [
UIFontDescriptor.FeatureKey.featureIdentifier : kLowerCaseType,
UIFontDescriptor.FeatureKey.typeIdentifier : kLowerCaseSmallCapsSelector
]
let features = [upperCaseFeature, lowerCaseFeature]
let smallCapsDescriptor = self.fontDescriptor.addingAttributes([UIFontDescriptor.AttributeName.featureSettings : features])
return UIFont(descriptor: smallCapsDescriptor, size: pointSize)
}
var withMonospacedDigits: UIFont {
let monospacedDigitsFeature = [
UIFontDescriptor.FeatureKey.featureIdentifier : kNumberSpacingType,
UIFontDescriptor.FeatureKey.typeIdentifier : kMonospacedNumbersSelector
]
let monospacedDigitsDescriptor = self.fontDescriptor.addingAttributes([UIFontDescriptor.AttributeName.featureSettings : [monospacedDigitsFeature]])
return UIFont(descriptor: monospacedDigitsDescriptor, size: pointSize)
}
}
I should be able to obtain a font with all the characteristics mentioned earlier with this line of code:
let font = UIFont.systemFont(ofSize: 16, weight: .regular).withSmallCaps.withMonospacedDigits
// OR
let font = UIFont.monospacedDigitSystemFont(ofSize: 16, weight: .regular).withSmallCaps
But for some reasons, it does not work. I can't get the font to have monospaced digits while having small caps at the same time.
What am I doing wrong?

I figured out why it does not work thanks to the reference document linked by #Carpsen90.
It seems like the Number Spacing feature is exclusive.
As stated in the document:
Features are classified as "exclusive" and "nonexclusive." This indicates whether several different selectors within a given feature type may be selected at once. Thus, it is possible to have both common and rare ligatures turned on, whereas it is impossible to display a given fraction as simultaneously vertical and diagonal.
So having both monospaced digits + small caps features at the same time is impossible.
EDIT:
I misread the document. Selectors of that feature are exclusive. But not the whole feature. So it should be possible.

Have a look at the reference document for more details.
I would suggest using attributed strings with small caps for all glyphs but numbers, and another font for monospaced digits. here is some sample code:
let monoSpacedDigits = UIFont.systemFont(ofSize: 13, weight: .medium).withMonospacedDigits
let smallCaps = UIFont.systemFont(ofSize: 16, weight: .regular).withSmallCaps
let attributedString = NSMutableAttributedString(string: """
H3ll0 7here
1111111111
2222222222
3333333333
4444444444
5555555555
6666666666
7777777777
8888888888
9999999999
0000000000
""", attributes: [NSAttributedStringKey.font : smallCaps])
do {
let regex = try NSRegularExpression(pattern: "[0-9]")
let range = NSRange(0..<attributedString.string.utf16.count)
let matches = regex.matches(in: attributedString.string, range: range)
for match in matches.reversed() {
attributedString.addAttribute(NSAttributedStringKey.font, value: monoSpacedDigits, range: match.range)
}
} catch {
// Do error processing here...
print(error)
}
myLabel.attributedText = attributedString
I've used a size of 13 and a medium weight to make the monospaced digits look as similar as possible to the small caps.

Related

How can I access a character's alternate font glyph to use in UILabel or UIButton text

I want to use alternate font glyphs in the text of a UILabel. For example, SF Mono has a "0" with no slash. I can see the alternate glyphs in Photoshop, if I use one as a text object and copy/paste it as a character to Xcode, it doesn't paste as the alternate, it pastes as the usual slashed version.
You aren't going to achieve this using mere copy and paste in Xcode. You're going to have to drop down to the level of core text and do it in code.
In this example, I'm using SFMono to display two zeroes, but one zero has the slash and the other doesn't:
let f = UIFont.monospacedSystemFont(ofSize: 16, weight: .regular)
let desc = f.fontDescriptor
let mas = NSMutableAttributedString(string: "00", attributes: [.font:f])
let d = [
UIFontDescriptor.FeatureKey.featureIdentifier: kStylisticAlternativesType,
UIFontDescriptor.FeatureKey.typeIdentifier: kStylisticAltThreeOnSelector
]
let desc2 = desc.addingAttributes([.featureSettings:[d]])
let f2 = UIFont(descriptor: desc2, size: 0)
mas.addAttributes([.font:f2], range: NSRange(location: 1, length: 1))
self.label.attributedText = mas
And here's the resulting label:

How to set a style for a specific word inside UITextView?

I have a UITextView in which I am trying to style a particular word. The problem I am facing is that on setting a style for the word, it's also applying the style to all the other occurrences of the word. I just want one particular instance of the word say first or third to have the custom style.
Consider the text present inside UITextView.
Sunset is the time of day when our sky meets the outer space solar winds.
There are blue, pink, and purple swirls, spinning and twisting, like clouds of balloons caught in
a whirlwind. The sun moves slowly to hide behind the line of horizon, while the
moon races to take its place in prominence atop the night sky. People slow to a crawl,
entranced, fully forgetting the deeds that must still be done. There is a coolness, a
calmness, when the sun does set.
If I set the style to sun then both the occurrences of the word is getting the style applied.
Here is the code
let normalAttr = [NSAttributedString.Key.font: UIFont(name: "Oswald", size: 19.0), NSAttributedString.Key.paragraphStyle : style]
let customAttr = [NSAttributedString.Key.font: UIFont(name: "Oswald", size: 19.0), NSAttributedString.Key.foregroundColor: UIColor.red]
let words = textView.text.components(separatedBy: " ")
let newText = NSMutableAttributedString()
for word in words {
if (word == selectedWord) {
newText.append(NSMutableAttributedString(string: word + " " , attributes: selectedAttributes as [NSAttributedString.Key : Any]))
} else {
newText.append(NSMutableAttributedString(string:word + " ", attributes: normalAttributes as [NSAttributedString.Key : Any]))
}
}
textView.attributedText = newText
I just want to apply the style to one word any help on how could I do that?
How are you choosing which instance to replace?
The simplest way to do this would be to just maintain your own counter:
var counter = 0
for word in words {
if (word == selectedWord) {
counter += 1
// myTarget being the first or third or whatever
let attributesToUse = (counter == myTarget) ? selectedAttributes : normalAttributes
newText.append(NSMutableAttributedString(string: word + " " , attributes: attributesToUse as [NSAttributedString.Key : Any]))
} else {
newText.append(NSMutableAttributedString(string:word + " ", attributes: normalAttributes as [NSAttributedString.Key : Any]))
}
}
But you can certainly get cleaner by using NSAttributedStrings and looking for the range of your text..
let myText = NSMutableAttributedString(string: textView.text, attributes: normalAttributes)
// this will only turn up the FIRST occurrence
if let range = myText.range(of: selectedWord) {
let rangeOfSelected = NSRange(range, in: myText)
myText.setAttributes(selectedAttributes, range: rangeOfSelected)
}
If you want to use arbitrary occurrence you can prob write an extension that creates an array of all the ranges and then pick the one that matters, this is a good reference for that: https://medium.com/#weijentu/find-and-return-the-ranges-of-all-the-occurrences-of-a-given-string-in-swift-2a2015907a0e
Def could be overkill though, you can also modify the methods in those article to instead to take in an int (occuranceNumber) and use a counter like above to return only the range of the nth occurrence, and then do the same thing with attributed strings.

How do I get a monospace font that respects acessibility settings

let bodyFontDescriptor = UIFontDescriptor
.preferredFontDescriptor(withTextStyle: UIFontTextStyle.body)
let bodyMonospacedFontDescriptor = bodyFontDescriptor.addingAttributes(
[
UIFontDescriptorFeatureSettingsAttribute: [
[
UIFontFeatureTypeIdentifierKey: kTextSpacingType,
UIFontFeatureSelectorIdentifierKey: kMonospacedTextSelector
]
]
])
let bodyMonospacedFont = UIFont(descriptor: bodyMonospacedFontDescriptor, size: 0.0)
textview.font = bodyMonospacedFont
This produces text with characters of variable width.
I need to get a monospace font without hardcoding courier new
and fixed size.
Deployment target is ios 9.0
Here is an extension to UIFontDescriptor that returns a preferred monospaced font descriptor for a given text style. There is no simple way to get a fully monospaced font using UIFont or UIFontDescriptor. This solution attempts to find a good monospaced font and falls back to Courier if needed.
extension UIFontDescriptor {
static let monoDescriptor: UIFontDescriptor = {
// Attempt to find a good monospaced, non-bold, non-italic font
for family in UIFont.familyNames {
for name in UIFont.fontNames(forFamilyName: family) {
let f = UIFont(name: name, size: 12)!
let fd = f.fontDescriptor
let st = fd.symbolicTraits
if st.contains(.traitMonoSpace) && !st.contains(.traitBold) && !st.contains(.traitItalic) && !st.contains(.traitExpanded) && !st.contains(.traitCondensed) {
return fd
}
}
}
return UIFontDescriptor(name: "Courier", size: 0) // fallback
}()
class func preferredMonoFontDescriptor(withTextStyle style: UIFontTextStyle) -> UIFontDescriptor {
// Use the following line if you need a fully monospaced font
let monoDescriptor = UIFontDescriptor.monoDescriptor
// Use the following two lines if you only need monospaced digits in the font
//let monoDigitFont = UIFont.monospacedDigitSystemFont(ofSize: 0, weight: .regular)
//let monoDescriptor = monoDigitFont.fontDescriptor
// Get the non-monospaced preferred font
let defaultFontDescriptor = preferredFontDescriptor(withTextStyle: style)
// Remove any attributes that specify a font family or name and remove the usage
// This will leave other attributes such as size and weight, etc.
var fontAttrs = defaultFontDescriptor.fontAttributes
fontAttrs.removeValue(forKey: .family)
fontAttrs.removeValue(forKey: .name)
fontAttrs.removeValue(forKey: .init(rawValue: "NSCTFontUIUsageAttribute"))
let monospacedFontDescriptor = monoDescriptor.addingAttributes(fontAttrs)
return monospacedFontDescriptor.withSymbolicTraits(defaultFontDescriptor.symbolicTraits) ?? monospacedFontDescriptor
}
}
Note the comments about whether you need a font that is fully monospaced or a font that just has monospaced digits. Comment/Uncomment those lines to suit your specific needs.
Sample usage:
let bodyMonospacedFont = UIFont(descriptor: .preferredMonoFontDescriptor(withTextStyle: .body), size: 0)
textview.font = bodyMonospacedFont
The following is some test code to confirm that the results of preferredMonoFontDescriptor(withTextStyle:) works properly for all styles:
let textStyles: [UIFontTextStyle] = [ .body, .callout, .caption1, .caption2, .footnote, .headline, .subheadline, .largeTitle, .title1, .title2, .title3 ]
for style in textStyles {
let nfont = UIFont(descriptor: .preferredFontDescriptor(withTextStyle: style), size: 0)
let mfont = UIFont(descriptor: .preferredMonoFontDescriptor(withTextStyle: style), size: 0)
print(style)
print(nfont)
print(mfont)
}
If you compare each pair of results, they have the same size, weight, and style, just a different font.

iOS Swift Relative font size

I have a UITextView, and I want to change its font size.
however, I want it to change relatively since it pulls from a file that has multiple font sizes in it and I want it to change accordingly.
for example, I have a word in font size 36 and one in font size 12 and I want to scale them by 0.75% to 27 and 9 respectively.
If I try:
textview.font = UIFont(name: textview.font.fontName, size: 20)
it will only change the entire UITextView font size.
thanks!
You can use this extension:
extension NSAttributedString {
#warn_unused_result
func scaleBy(scale: CGFloat) -> NSAttributedString {
let scaledAttributedString = NSMutableAttributedString(attributedString: self)
scaledAttributedString.enumerateAttribute(NSFontAttributeName, inRange: NSRange(location: 0, length: scaledAttributedString.length), options: NSAttributedStringEnumerationOptions(rawValue: 0)) { (value, range, _) in
if let oldFont = value as? UIFont {
let newFont = oldFont.fontWithSize(oldFont.pointSize * scale)
scaledAttributedString.removeAttribute(NSFontAttributeName, range: range)
scaledAttributedString.addAttribute(NSFontAttributeName, value: newFont, range: range)
}
}
return scaledAttributedString
}
}
Then just call something like:
textField.attributedText = textField.attributedText!.scaleBy(0.5)
Example:
You would have to write (or find) a parser for the rich text format file that could extract the font size data for each text element (I think this would be the \fsN tags in most cases) and then use that number (multiplied by 0.75) to set the size of each word or phrase individually. You could use an attributed string if the differently sized words need to be recombined into a single string, but that wouldn't be necessary if each word or phrase was in a separate label.
Personally, I would disregard the font sizes of the source data and impose a layout within the app that looks nice, if that's an option.

iOS Monospaced Custom Font

I have a custom font included in my Xcode 7, iOS 9 targeted project. I want to make the font monospaced. I tried this, and didn't work:
let originalFont = UIFont(name: "My Custom Font", size: 18)
let originalFontDescriptor = originalFont!.fontDescriptor()
let fontDescriptorFeatureSettings = [
[
UIFontFeatureTypeIdentifierKey: kNumberSpacingType,
UIFontFeatureSelectorIdentifierKey: kMonospacedNumbersSelector
]
]
let fontDescriptorAttributes = [UIFontDescriptorFeatureSettingsAttribute: fontDescriptorFeatureSettings]
let fontDescriptor = originalFontDescriptor.fontDescriptorByAddingAttributes(fontDescriptorAttributes)
let font = UIFont(descriptor: fontDescriptor, size: 0)
topLabel.font = font
With or without above code, the label displayed in proper custom font. It's just above code doesn't do anything.
My following answer is only making numbers (not the whole font) of an existing font monospaced (if the font supports it)
At least I was searching for making numbers monospaced when finding this Thread. So I hope it will help although it answers another question.
This works just fine, tested on Swift 5 and iOS14+13:
(As long as "your font is supporting the monospaced digits feature".)
extension UIFont {
var monospacedDigitFont: UIFont {
let oldFontDescriptor = fontDescriptor
let newFontDescriptor = oldFontDescriptor.monospacedDigitFontDescriptor
return UIFont(descriptor: newFontDescriptor, size: 0)
}
}
private extension UIFontDescriptor {
var monospacedDigitFontDescriptor: UIFontDescriptor {
let fontDescriptorFeatureSettings = [[UIFontDescriptor.FeatureKey.featureIdentifier: kNumberSpacingType, UIFontDescriptor.FeatureKey.typeIdentifier: kMonospacedNumbersSelector]]
let fontDescriptorAttributes = [UIFontDescriptor.AttributeName.featureSettings: fontDescriptorFeatureSettings]
let fontDescriptor = self.addingAttributes(fontDescriptorAttributes)
return fontDescriptor
}
}
Then you can use it on any label like this:
/// Label with monospacing activated
myLabel.font = myLabel.font.monospacedDigitFontDescriptor
/// Label with monospacing not activated (default is proportional spacing)
myLabel.font = myLabel.font
(source: https://blog.usejournal.com/proportional-vs-monospaced-numbers-when-to-use-which-one-in-order-to-avoid-wiggling-labels-e31b1c83e4d0)
The code you are using is not making font monospaced.
It's tweaking font to render digits in monospace mode. So all with this font digits will have same width.
Below is an example with 4 labels, 1 is using custom font Docis Light, 2nd is Docis Light with monospaced digits on, 3rd is system font of same size, 4th is system font with monospaced digits on:
As you see, this custom font already supports monospace digits feature out of the box with no tweak required.
If you need to use monospaced (not just digits) font, you have to use custom monospaced font (designed to be monospaced) or you can use built-in iOS monospaced fonts such as Courier or Menlo (See all available iOS fonts at http://iosfonts.com/)
This is how they look like with same scenario:
With or without tweaking, they are already monospaced and the digits are monospaced as well.
I answered similar question here, probably, I should just link the answer instead of images but it so much more visual.
Don't forget to import the header file. Hope it will work. This solution is in Objective-C
#import <CoreTextArcView.h>
UIFont *const existingFont = [UIFont preferredFontForTextStyle: UIFontTextStyleBody];
UIFontDescriptor *const existingDescriptor = [existingFont fontDescriptor];
NSDictionary *const fontAttributes = #{
UIFontFeatureTypeIdentifierKey
UIFontDescriptorFeatureSettingsAttribute: #[
#{
UIFontFeatureTypeIdentifierKey: #(kNumberSpacingType),
UIFontFeatureSelectorIdentifierKey: #(kMonospacedNumbersSelector)
}]
};
UIFontDescriptor *const monospacedDescriptor = [existingDescriptor fontDescriptorByAddingAttributes: fontAttributes];
UIFont *const proportionalFont = [UIFont fontWithDescriptor: monospacedDescriptor size: [existingFont pointSize]];

Resources