iOS Swift Relative font size - ios

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.

Related

Create custom NSAttributedString.Key

I'm trying to build a simple note app. At the moment, I'm focusing on the possibility to set the text with different text styles (e.g. body, headline, bold, italic, etc.). I used a NSAttributedString to set the different text styles. Now, I'd like to detect which style has been applied to the selected text.
I thought a good way to do it would have been to create a custom NSAttributedString.Key, so that I can assign it when setting the attributes (e.g. .textStyle: "headline", and read it when I need to detect it. I tried implementing it as an extension of NSAttributedString.Key but without success. What would be the correct way to do it?
Is there a better alternative?
You can simply create a TextStyle enumeration and set your cases "body, headline, bold, italic, etc" (You can assign any value to them if needed). Then you just need to create a new NSAttributedString key:
enum TextStyle {
case body, headline, bold, italic
}
extension NSAttributedString.Key {
static let textStyle: NSAttributedString.Key = .init("textStyle")
}
Playground Testing
let attributedString = NSMutableAttributedString(string: "Hello Playground")
attributedString.setAttributes([.textStyle: TextStyle.headline], range: NSRange(location: 0, length: 5))
attributedString.enumerateAttributes(in: NSRange(location: 0, length: attributedString.length), options: []) { attributes, range, stop in
print(attributes, range, stop )
print(attributedString.attributedSubstring(from: range))
}

Change only fontsize of NSAttributedString

I have a NSAttributedString that was loaded from a RTF file, so it already holds several font-attributes for different ranges.
Now I want to adapt the font size to the screensize of the device, but when I add a whole new font attribute with a new size, the other fonts disappear.
Is there a way to change only the font size for the whole string?
If you only want to change the size of any given font found in the attributed string then you can do:
let newStr = someAttributedString.mutableCopy() as! NSMutableAttributedString
newStr.beginEditing()
newStr.enumerateAttribute(.font, in: NSRange(location: 0, length: newStr.string.utf16.count)) { (value, range, stop) in
if let oldFont = value as? UIFont {
let newFont = oldFont.withSize(20) // whatever size you need
newStr.addAttribute(.font, value: newFont, range: range)
}
}
newStr.endEditing()
print(newStr)
This will keep all other attributes in place.
If you want to replace all fonts in a given attributed string with a single font of a given size but keep all other attributes such as bold and italic, see:
NSAttributedString, change the font overall BUT keep all other attributes?

UIFont monospaced digits + small caps

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.

PDFPage.attributedString returns the wrong font

I'm reading an existing PDF using PDFKit. I get an attributedString for a page, but the fonts in the string don't match the fonts actually in the PDF:
The fonts in the PDF (according to several different apps) are:
CourierFinalDraft (TypeType Roman) Embedded Subset
CourierFinalDraft-Bold (TrueType Roman) Embedded Subset
CourierFinalDraft-Italic (TrueType Roman) Embedded Subset
My Swift code to get the font is:
guard let page = pdf.page(at: pageNo) else { return }
guard let content = page.attributedString else { return }
content.enumerateAttributes(in: range, options:[]) {(dict: [String:Any], range: NSRange, stop: UnsafeMutablePointer<ObjCBool>) -> Void in
let font = dict[NSFontAttributeName] as! UIFont
}
All of the text is returned in a single range. The font returned has the following:
font-family: "Times New Roman"
font-name: "TimesNewRomanPSMT"
So the Bold and Italic text are returned in the same range as the normal text, and I can't distinguish among them, which is what I'm trying to do. As well as the font name I also look at:
font.fontDescriptor.symbolicTraits.contains(.traitItalic)
but of course this is always False as all text is returned in the same range as if it is normal.
This is using the XCode simulator, in case that's relevant. The PDF does render correctly (including bold and italics) on Safari in the Simulator. Unfortunately I can't try it out on a real iPhone.
This question is a few years old, but I found that I had to make a mutable copy of the data in order to interact with it and then use the information available to modify the underlying font and re-display it.
let str = content.mutableCopy() as! NSMutableAttributedString
str.beginEditing()
str.enumerateAttribute(.font,
in: NSRange(location: 0, length: str.length),
options: []) { value, range, _ in
guard let value = value as? UIFont else {
return
}
let isBold = value.fontDescriptor.symbolicTraits.contains(.traitBold)
let font = // Your font logic here based on traits
str.removeAttribute(.font, range: range)
str.addAttribute(.font, value: font, range: range)
str.addAttribute(.paragraphStyle, value: style, range: range)
}
str.endEditing()
I have not gone over performance issues on this yet for larger pdfs, but it does not seem to take up too many resources for pdfs between 10-20 pages. For a higher number of pages, I would most likely create an actual paging mechanism and then adjust for each page on the fly rather than loop the entire pdf file.

Limiting user's text entry to the height of the UITextView

I'm working with a UITextView and I want to make it so that once the user has filled the UITextView (you make it in storyboard, and these dimensions the user is not allowed to type outside of) the user cannot type anymore text. Basically, whats happening now is even if it looks like it's filled and I keep typing its like a never-ending text box which you can't see. What I assume is the dimensions you make it in storyboard is the only space you see text in.
Can someone help me?
http://www.prntscr.com/671n1u
You can use the UITextViewDelegate shouldChangeTextInRange: method to limit the text entry to the height of the text view:
func textView(textView: UITextView, shouldChangeTextInRange range: NSRange, replacementText text: String) -> Bool {
// Combine the new text with the old
let combinedText = (textView.text as NSString).stringByReplacingCharactersInRange(range, withString: text)
// Create attributed version of the text
let attributedText = NSMutableAttributedString(string: combinedText)
attributedText.addAttribute(NSFontAttributeName, value: textView.font, range: NSMakeRange(0, attributedText.length))
// Get the padding of the text container
let padding = textView.textContainer.lineFragmentPadding
// Create a bounding rect size by subtracting the padding
// from both sides and allowing for unlimited length
let boundingSize = CGSizeMake(textView.frame.size.width - padding * 2, CGFloat.max)
// Get the bounding rect of the attributed text in the
// given frame
let boundingRect = attributedText.boundingRectWithSize(boundingSize, options: NSStringDrawingOptions.UsesLineFragmentOrigin, context: nil)
// Compare the boundingRect plus the top and bottom padding
// to the text view height; if the new bounding height would be
// less than or equal to the text view height, append the text
if (boundingRect.size.height + padding * 2 <= textView.frame.size.height){
return true
}
else {
return false
}
}

Resources