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
}
}
Related
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
}
}
}
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.
I have an attributedString and want to change only it's fontsize. To do that, I use another method that I found on StackOverflow. For most cases, this is working, but somehow it doesn't change the whole attributedString in one case.
Method to change the size:
/**
*A struct with static methods that can be useful for your GUI
*/
struct GuiUtils {
static func setAttributedStringToSize(attributedString: NSAttributedString, size: CGFloat) -> NSMutableAttributedString {
let mus = NSMutableAttributedString(attributedString: attributedString)
mus.enumerateAttribute(.font, in: NSRange(location: 0, length: mus.string.count)) { (value, range, stop) in
if let oldFont = value as? UIFont {
let newFont = oldFont.withSize(size)
mus.addAttribute(.font, value: newFont, range: range)
}
}
return mus
}
}
Working:
label.attributedText = GuiUtils.setAttributedStringToSize(attributedString: attributedString, size: fontSize)
Not working:
mutableAttributedString.replaceCharacters(in: gapRange, with: filledGap)
label.attributedText = GuiUtils.setAttributedStringToSize(attributedString: mutableAttributedString.replaceCharacters, size: fontSize)
Somehow, the replaced text does not change its size.
Excuse me, but do you sure that your filledGap attributed string has font attribute? Because if it doesn't – this part will not be handled by the enumerateAttribute block.
In this case your fix will be just to set any font to the whole filledGap string, to be sure that it's part will be handled by the enumerateAttribute block.
I am reading Designated Initializers with default values and convenience initializer and I am a little bit confused about them. If I can achieve every thing with Initializer with default values then why one should create convenience initializer.
I have created an example in both scenario and compare them.
Initializer with Default Values:
class Cat {
var color:String
var age:Int
init(color:String = "black",age:Int = 1) {
self.color = color
self.age = age
}
}
Convenience Initializer:
class Cat {
var color: String
var age: Int
//If both parameters color and age are given:
init (color: String, age: Int) {
self.color = color
self.age = age
}
//If only age is given:
convenience init (age: Int) {
self.init(color: "black", age: age)
}
//if only color is given:
convenience init (color: String) {
self.init(color: color, age: 1)
}
//if nothing given
convenience init () {
self.init(color: "black", age: 1)
}
}
Initializer varient calling:
//create Otto:
var Otto = Cat(color: "white", age: 10)
print("Otto is \(Otto.color) and \(Otto.age) year(s) old!")
//create Muffins:
var Muffins = Cat(age: 5)
print("Muffins is \(Muffins.color) and \(Muffins.age) year(s) old!")
//create Bruno:
var Bruno = Cat(color: "red")
print("Bruno is \(Bruno.color) and \(Bruno.age) year(s) old!")
//create Pico:
var Pico = Cat()
print("Pico is \(Pico.color) and \(Pico.age) year(s) old!")
Because sometimes initialising a class isn't that straightforward. Let's look at this Rectangle class. It has a width and height:
class Rectangle {
let width: Double
let height: Double
init(width: Double, height: Double) {
self.width = width
self.height = height
}
}
Here, you can add a convenience initialiser that takes a sideLength parameter:
convenience init(sideLength: Double) {
self.init(width: sideLength, height: sideLength)
}
Or, a convenience initializer that takes a aFourthOf parameter:
convenience init(aFourthOf rect: Rectangle) {
self.init(width: rect.width / 2, height: rect.height / 2)
}
There are lots of these situations.
That is a very interesting question.
You can always initialize using designated initializers but as the name suggests it ease the initialization when there are some complications or rare scenarios.
Here are a couple of examples
1. Look at the example below,
class Cat {
var color: String
var age: Int
//If both parameters color and age are given:
init (color: String, age: Int) {
self.color = color
self.age = age
}
//If only age is provided:
convenience init (age: Int) {
self.init(color: "black", age: age)
}
//if only color is provided:
convenience init (color: String) {
self.init(color: color, age: 1)
}
//if nothing is provided
convenience init () {
self.init(color: "black", age: 1)
}
}
2. While creating a storyboard instance we need to specify Name & Bundle. Since the bundle is Optional you can specify it as nil. Additionally, if you don't specify the bundle then XCode gives you a warning. But doing this every time for no reason becomes irritating.
You can't change the default init method for the UIStoryboard class. What should you do?? Simple create an extension to UIStoryboard and use a convenience initializer.
This way convenience initializer helps you code better.
As we observed above that there can be multiple cases during development where we need to add custom initialization. One important thing to keep in mind is Designated initializers must always delegate up (to base/super class).
Convenience initializers must always delegate across.
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