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))
}
}
Related
iOS 16 (finally) allowed us to specify an axis: in TextField, letting text entry span over multiple lines.
However, I don't want my text field to always fill the available horizontal space. It should fill the amount of space taken up by the text that has been entered into it. To do this, we can apply .fixedSize().
However, using this two things in conjunction causes the text field to completely collapse and take up no space. This bug (?) does not affect a horizontal-scrolling text field.
Is this basic behaviour simply broken, or is there an obtuse but valid reason these methods don't play nice?
This is very simple to replicate:
struct ContentView: View {
#State var enteredText: String = "Test Text"
var body: some View {
TextField("Testing", text: $enteredText, axis: .vertical)
.padding()
.fixedSize()
.border(.red)
}
}
Running this will produce a red box the size of your padding. No text is shown.
I don't want my text field to always fill the available horizontal space. It should fill the amount of space taken up by the text that has been entered into it.
That's a weird wish. If you want to remove the background of the TextField, then do it. But I don't think it's a good idea to have an autosizing TextField. One of the reasons against it is the fact that if you erase all the text then the TextField will collapse to the zero width and you'll never set the cursor into it.
I had exactly the same problem with multiline text. So far the use of axis:.vertical requires a fixed width for the text field. This was for me a major problem when designing a table view where the column width adapts to the widest text field.
I found a very good working solution which I summarised in the following ViewModifier :
struct DynamicMultiLineTextField: ViewModifier {
let minWidth: CGFloat
let maxWidth: CGFloat
let font: UIFont
let text: String
var sizeOfText : CGSize {
get {
let font = self.font
let stringValue = self.text
let attributedString = NSAttributedString(string: stringValue, attributes: [.font: font])
let mySize = attributedString.size()
return CGSize(width: min(self.maxWidth, max(self.minWidth, mySize.width)), height: mySize.height)
}
}
func body(content: Content) -> some View {
content
.frame(minWidth: self.minWidth, idealWidth: self.sizeOfText.width ,maxWidth: self.maxWidth)
}
}
extension View {
func scrollableDynamicWidth(minWidth: CGFloat, maxWidth: CGFloat, font: UIFont, text: String) -> some View {
self.modifier(DynamicMultiLineTextField(minWidth: minWidth, maxWidth: maxWidth, font: font, text: text))
}
Usage (only on a TextField with the option: axis:.vertical):
TextField("Content", text:self.$tableCell.value, axis: .vertical)
.scrollableDynamicWidth(minWidth: 100, maxWidth: 800, font: self.tableCellFont, text: self.tableCell.value)
The text field width changes as you type. If you want to limit the length of a line type "option-return" which starts a new line.
On macOS the situation seems to be a bit more complicated. The problem seems to be that TextField does not extent its rendering surface beyond its initial size. So - when the field grows the text is invisible because not rendered.
I am using the following ViewModifier to force a larger rendering surface. I fear this can be called a "hack":
// For a scrollable TextField
struct DynamicMultiLineTextField: ViewModifier {
let minWidth: CGFloat
let maxWidth: CGFloat
let font: NSFont
#Binding var text: String
#FocusState var isFocused: Bool
#State var firstActivation : Bool = true
#State var backgroundFieldSize: CGSize? = nil
var fieldSize : CGSize {
get {
if let theSize = backgroundFieldSize {
return theSize
}
else {
return self.sizeOfText()
}
}
}
func sizeOfText() -> CGSize {
let font = self.font
let stringValue = self.text
let attributedString = NSAttributedString(string: stringValue, attributes: [.font: font])
let mySize = attributedString.size()
let theSize = CGSize(width: min(self.maxWidth, max(self.minWidth, mySize.width + 5)), height: mySize.height)
return theSize
}
func body(content: Content) -> some View {
content
.frame(width:self.fieldSize.width)
.focused(self.$isFocused)
.onChange(of: self.isFocused, perform: { value in
if value && self.firstActivation {
let oldText = self.text
self.backgroundFieldSize = CGSize(width:self.maxWidth, height:self.sizeOfText().height)
Task() {#MainActor () -> Void in
self.text = "nonsense text nonsense text nonsense text nonsense text nonsense text nonsense text nonsense text nonsense text"
self.firstActivation = false
self.isFocused = false
}
Task() {#MainActor () -> Void in
self.text = oldText
try? await Task.sleep(nanoseconds: 1_000)
self.isFocused = true
self.backgroundFieldSize = nil
Task () {
self.firstActivation = true
}
}
}
})
}
}
extension View {
func scrollableDynamicWidth(minWidth: CGFloat, maxWidth: CGFloat, font: NSFont, text: Binding<String>) -> some View {
self.modifier(DynamicMultiLineTextField(minWidth: minWidth, maxWidth: maxWidth, font: font, text:text))
}
}
Usage:
TextField("Content", text:self.$tableCell.value, axis:.vertical)
.scrollableDynamicWidth(minWidth: 100, maxWidth: 800, font: self.tableCellFont, text: self.$tableCell.value)
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
}
}
}
I'm currently working on making a custom UITextView class.
As you can see in the image, there is bigger space between the text in line 1 and line 2.
The blue cursor represents the height of the text, however, like line 2's text, the gray background has a height around 2 times bigger than the text.
After some research, I thought change the NSMutableParagraphStyle's lineSpacing will fix the problem I have (but it didn't). Is changing the lineSpacing or MutableParagraphStyle won't change the line-height?
import UIKit
class CustomTextView: UITextView {
override init(frame: CGRect, textContainer: NSTextContainer?) {
super.init(frame: frame, textContainer: textContainer)
configure()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
init() {
super.init(frame: .zero, textContainer: nil)
configure()
}
private func configure() {
translatesAutoresizingMaskIntoConstraints = false
font = UIFont.myCustomFont()
let textStyle = NSMutableParagraphStyle()
textStyle.lineSpacing = 1.0
textStyle.paragraphSpacing = 1.0
let attributes: Dictionary = [NSAttributedString.Key.paragraphStyle: textStyle]
attributedText = NSAttributedString(string: self.text, attributes: attributes)
}
override func caretRect(for position: UITextPosition) -> CGRect {
var superRect = super.caretRect(for: position)
guard let font = self.font else { return superRect }
superRect.size.height = font.pointSize
return superRect
}
}
Added this part later...
So, when I delete the line for setting the font, I was able to get rid of the bigger space (but lost the custom font).
So I think the custom font has something like a bottom padding? I guess. I created a font like this, but do you see any problem or any clue why I get the bottom space, like the gray area in the image?
extension UIFont {
static func myCustomFont() -> UIFont {
return UIFont(name: "Custom Font", size: 18)!
}
}
On the storyboard I created a text view. Inserted two paragraphs of text content inside textview. Selected custom attributes on the storyboard and made some words bold. When I run the simulator, everything is ok. But when I set the font size programmatically with respect to the "view.frame.height", the bold words which I set on the storyboard resets to regular words.
Code: "abcTextView.font = abcTextView.font?.withSize(self.view.frame.height * 0.021)"
I couldn't get past this issue. How can I solve this?
The problem is that you're working with an AttributedString. Take a look at Manmal's excellent answer here if you want more context, and an explanation of how the code works:
NSAttributedString, change the font overall BUT keep all other attributes?
Here's an easy application of the extension he provides, to put it in the context of your problem:
class ViewController: UIViewController {
#IBOutlet weak var myTextView: UITextView!
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
let newString = NSMutableAttributedString(attributedString: myTextView.attributedText)
newString.setFontFace(font: UIFont.systemFont(ofSize: self.view.frame.height * 0.033))
myTextView.attributedText = newString
}
}
extension NSMutableAttributedString {
func setFontFace(font: UIFont, color: UIColor? = nil) {
beginEditing()
self.enumerateAttribute(
.font,
in: NSRange(location: 0, length: self.length)
) { (value, range, stop) in
if let f = value as? UIFont,
let newFontDescriptor = f.fontDescriptor
.withFamily(font.familyName)
.withSymbolicTraits(f.fontDescriptor.symbolicTraits) {
let newFont = UIFont(
descriptor: newFontDescriptor,
size: font.pointSize
)
removeAttribute(.font, range: range)
addAttribute(.font, value: newFont, range: range)
if let color = color {
removeAttribute(
.foregroundColor,
range: range
)
addAttribute(
.foregroundColor,
value: color,
range: range
)
}
}
}
endEditing()
}
}
Is there a way to build a view modifier that applies custom font and fontSize, as the below working example, and have in the same modifier the possibility to add kerning as well?
struct labelTextModifier: ViewModifier {
var fontSize: CGFloat
func body(content: Content) -> some View {
content
.font(.custom(Constants.defaultLabelFontSFProDisplayThin, size: fontSize))
}
}
extension View {
func applyLabelFont(size: CGFloat) -> some View {
return self.modifier(labelTextModifier(fontSize: size))
}
}
The above works well, however i cannot figure it out how to add kerning to the modifier as well
tried
content
.kerning(4)
, but did not work.
Suggestions?
Alternate is to use Text-only modifier, like
extension Text {
func applyLabelFont(size: CGFloat, kerning: CGFloat = 4) -> Text {
self
.font(.custom(Constants.defaultLabelFontSFProDisplayThin, size: size))
.kerning(kerning)
}
}