I have created a stepper to increase font sizes in my application, and now, I would like to save the last font size, so that when the user reopens the app, the last font size will be persistent. I am trying to save it to UserDefaults but it will not let me save my label, because I am using UIFont to establish the type of font I want, and CGFloat to be able to switch font size with my stepper.
When I run the app, and try to select the font size, the app crashes, and I cannot find the problem. I know that the problem is trying to save the data from UIFont into the UserDefaults, because when I commented out that line of code, the app has no issues running.
Also, after I try calling the stored UserDefault settings in the ViewDidAppear function, I get an error saying that I cannot assign Any? to type UIFont?
Any help would be appreciated. Again, my intent is that, when the user use the stepper to change font size, the font size will remain persistent, even after exiting and reopening the app.
import UIKit
import Foundation
class ViewController: UIViewController {
@IBOutlet weak var quotesLabel: UITextView!
@IBOutlet weak var welcomeLabel: UILabel!
@IBOutlet weak var topLabel: UILabel!
@IBOutlet weak var generateLabel: UILabel!
@IBOutlet weak var fontStepper: UIStepper!
@IBOutlet weak var minFontSizeLabel: UILabel!
@IBOutlet weak var adjustFontSizeLabel: UILabel!
@IBOutlet weak var maxFontSizeLabel: UILabel!
let quotesBook = QuotesBook()
let defaults = UserDefaults.standard
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib
fontStepper.wraps = true
fontStepper.autorepeat = true
generateLabel.font = UIFont(name: "HoeflerText-Italic", size: 15)
generateLabel.textColor = UIColor(red: 25/255, green: 23/255, blue: 170/255, alpha: 230/255)
generateLabel.textAlignment = NSTextAlignment.center
topLabel.font = UIFont(name: "HoeflerText-Italic", size: 20)
topLabel.textColor = UIColor(red: 25/255, green: 23/255, blue: 170/255, alpha: 230/255)
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
@IBAction func stepperPressed(_ sender: UIStepper) {
quotesLabel.isEditable = true
quotesLabel.font = UIFont (name: "Futura-MediumItalic", size: CGFloat(Float(sender.value)))
defaults.set(quotesLabel.font, forKey: "textSize")
}
@IBAction func showQuoteBttn(_ sender: Any) {
quotesLabel.textAlignment = NSTextAlignment.center
maxFontSizeLabel.isHidden = false
adjustFontSizeLabel.isHidden = false
minFontSizeLabel.isHidden = false
fontStepper.isHidden = false
generateLabel.isHidden = false
welcomeLabel.isHidden = true
topLabel.isHidden = true
quotesLabel.isHidden = false
quotesLabel.text = quotesBook.randomQuote()
}
override func viewDidAppear(_ animated: Bool) {
if let b = defaults.object(forKey: "textSize")
{
quotesLabel.font = b
}
}
You cannot save an (UI)Font directly into user defaults because it's not property list compliant.
A suitable solution is to save the String name and the Float size of the font separately. This can be accomplished with an extension for UserDefaults:
extension UserDefaults {
func set(font : UIFont, forKey key : String)
{
let fontName = font.fontName
let fontSize = font.pointSize
self.set(fontName, forKey: key + "_name")
self.set(Float(fontSize), forKey: key + "_size")
}
func font(forKey key : String) -> UIFont?
{
guard let fontName = string(forKey: key + "_name") else { return nil }
let fontSize = float(forKey: key + "_size")
if fontSize == 0.0 { return nil }
return UIFont(name: fontName, size: CGFloat(fontSize))
}
}
Alternatively – also with an extension – archive and unarchive the font to and from NSData:
extension UserDefaults {
func set(font: UIFont, forKey key: String)
{
let data = NSKeyedArchiver.archivedData(withRootObject: font)
self.set(data, forKey: key)
}
func font(forKey key: String) -> UIFont?
{
guard let data = data(forKey: key) else { return nil }
return NSKeyedUnarchiver.unarchiveObject(with: data) as? UIFont
}
}
(This is the first item that came up when I searched for a solution to this problem, so I might as well share a sustainable solution. Here using macOS, but NSFontDescriptor also exists on iOS, just substitute UIFont).
The recommended way of saving a font is not to do it (a bit flippant, sorry) - but in WWDC session 223 from 2013 (Using Fonts with TextKit), the recommendation is specifically to a) not cherry-pick attributes (in other words, not save only font name and size) and b) not save NSFont, but rather save NSFontDescriptor.
NSFontDescriptor is an easily overlooked class, but the documentation is quite clear that
The symbolic traits supersede the existing NSFontTraitMask type used by NSFontManager.
so we should be using it. Especially on iOS, you can do a lot of weird and wonderful things with it, including cascading preferences for the font you want to use, language-specific settings, etc etc.
Saving, assuming you have a font called 'font':
let fontDescriptor = font.fontDescriptor
let descriptorData = NSKeyedArchiver.archivedData(withRootObject: fontDescriptor)
userDefaults.set(descriptorData, forKey: "descriptorData")
Reading:
if let descriptorData = userDefaults.data(forKey: "descriptorData"){
if let fontDescriptor = NSKeyedUnarchiver.unarchiveObject(with: descriptorData) as? NSFontDescriptor {
let myFont = NSFont(descriptor: fontDescriptor, size: 0)
}
}
Setting the size to '0' means that it takes the size from the descriptor, otherwise you're overriding it.
This does not save the font colour; if you want to save a default text colour, you still need to do this separately.
edited to add UIKit
Ok, blink and iOS changes (I was wrong about NSFontDescriptor, it's UIFontDescriptor, and current macOS methods have been deprecated:
let font = UIFont.systemFont(ofSize: 14)
let fontDescriptor = font.fontDescriptor
let descriptorData = try NSKeyedArchiver.archivedData(withRootObject: fontDescriptor, requiringSecureCoding: true)
UserDefaults.standard.set(descriptorData, forKey: "descriptorData")
if let descriptorData = UserDefaults.standard.data(forKey: "descriptorData"){
if let fontDescriptor = try NSKeyedUnarchiver.unarchivedObject(ofClass: UIFontDescriptor.self, from: descriptorData) {
let myFont = UIFont(descriptor: fontDescriptor, size: 0)
}
}
(this compiles in a playground; I have not tested it in an iOS app)
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With