104

Xcode 6 has a new feature where fonts and font sizes in UILabel, UITextField, and UIButton can be set automatically based on the size class of the current device configuration, right in the storyboard. For example, you can set a UILabel to use font size 12 on "any width, compact height" (such as on iPhones in landscape) configurations and size 18 on "regular width, regular height" configurations (such as on iPads). More information is available here:

developer.apple.com/size_class

This is a great feature in theory because it could make it unnecessary to programmatically set different fonts on UI features based on the device configuration. Right now, I have some conditional code that sets the fonts based on the device type, but obviously, that means I have to set the fonts programmatically everywhere throughout the app. So I was initially really excited about this feature, but I found that it has a severe problem of actual usage for me (perhaps a bug). Note that I am building against SDK 8 and setting a minimum deployment target of iOS 8, so this has nothing to do with compatibility with old versions of iOS.

The problem is this: If I set different font sizes for different size classes and use the "System" font provided by iOS, everything works as expected, and the font sizes change based on the size class. If I use a custom font supplied by my application (yes, I have it set up correctly in my application bundle, as it works programmatically) and set the custom font to a label in an XCode 6 storyboard, that also works as expected. But when I try to use different sizes of the custom font for different size classes, in the storyboard, it suddenly doesn't work. The only difference in configuration is the font I've chosen (a custom one vs. the System font). Instead, all of the fonts show up on the device and simulator as the default system font at the default size, regardless of size class (and I verified via the debugger that it is substituting the system font for the actual one specified in the storyboard). So basically, the size class feature appears to be broken for custom fonts. Also, interestingly, the custom fonts actually display and adjust size properly in the XCode 6 "Preview" pane for the view controller: it stops working only when running on the actual iOS system (which makes me think that I'm configuring it correctly).

I tried multiple different custom fonts, and it doesn't seem to work for any of them, but it always works if I use "System" instead.

Anyway, has anyone else seen this problem in Xcode 6?

Any ideas on whether this is a bug in iOS 8, Xcode, or something

Am I doing wrong?

The only workaround I've found, as I said, is to continue to programmatically set the fonts like I have been for about three versions of iOS because that does work.

But I'd love to be able to use this feature if I could get it to work with custom fonts. Using the System font is not acceptable for our design.


ADDITIONAL INFO: As of Xcode 8.0, the bug is fixed.

Arsen Khachaturyan
  • 7,904
  • 4
  • 42
  • 42
mnemia
  • 1,549
  • 2
  • 12
  • 14
  • I have the same problem. Setting a custom font based on size class doesn’t seem to work. Strangely, the same code both previews correctly in Xcode *and* runs fine on iOS 7. – mohsenr Oct 08 '14 at 12:02
  • 1
    I hadn't tried it on iOS 7, since the new version of my app drops support for iOS 7, but that information makes me more confident that it's just a bug in iOS 8. For now, I'm just setting all custom fonts programmatically, but I'll consider filing a bug with Apple. – mnemia Oct 08 '14 at 14:56
  • 3
    I can confirm this is still the case in xCode 6.1 (6A1052d) app store release. – jodm Oct 29 '14 at 11:32
  • The bug report that mine was marked as a duplicate of is still "open" with Apple according to their system. – mnemia Dec 31 '14 at 17:05
  • 3
    Not a solution, but I've managed to get around the problem using a Category. My category UILabel+Font.h (I have one for buttons as well) contains the following code. -(void)awakeFromNib { float size = [self.font pointSize]; self.font = [UIFont fontWithName:@"YourCustomFont" size:size]; } So, this allows us to use the size classes with the System font to set the point values in different size classes, and the category ensures that the correct font is set. Maybe you could give that a go until Apple releases an update? – Daniel Retief Fourie Jan 15 '15 at 08:54
  • Yeah, that is a decent workaround for the problem, as at least that way you don't have to set it everywhere. I like it. Unfortunately, it gets more complex if you need to have multiple different fonts in different places. I still want the flexibility of being able to configure it in the storyboard, so I'll probably leave it as-is until they come out with a fix for this bug. – mnemia Jan 15 '15 at 19:14
  • 1
    @RetiefFourie .. you should not override system method awakeFromNib in your category. Instead create a method in your category and call for whichever label you want to set or, subclass your label. – cocoaNoob Apr 04 '15 at 04:29
  • 2
    Still not solved in Xcode 6.3.1. I'm angry on this. – Jun May 19 '15 at 11:14
  • As @Harry said it's still not working on v6.3.1. Will spend a lot of time doing everything programmatically. 7 months and still not patched ... Instead of just thinking about making more money (prices going up on all of their products) they should start thinking about the people thanks to whom they are able to make that money... – Moumou May 20 '15 at 10:13
  • Still nothing in 6.3.2. I had to go through a quite big storyboard file hacking things away. By now I think we can only hope for iOS 9 :) – joakim May 28 '15 at 19:59
  • Same here with Xcode 6.3.2. – Matteo Lallone Jun 02 '15 at 19:55
  • Still not working with Xcode 7 beta 5 – Antzi Aug 20 '15 at 11:51
  • 7
    Still not working with Xcode 7 final. Don't understand why Apple doesn't fix it. – ernesto Sep 22 '15 at 09:28
  • Just looked at the bug report that I made soon after the release of iOS 8, and the bug that mine was closed as a duplicate of is still open. I'd encourage others to report this in order to try to get some pressure on them to fix it. Probably it's not being prioritized for a fix just because there is a "workaround" (ie, various ways to do the same thing programmatically), but I'm surprised it's not fixed in iOS 9 since It makes one of their major new developer-friendly features in iOS 8 much less useful. – mnemia Sep 22 '15 at 15:18
  • I also have this problem with UIButtons, which you can't override setFont on as it's a method on the label of the button, not the button itself. Any ideas? – Mark Bridges Oct 14 '15 at 17:29
  • Still a bug in iOS 9 Xcode 7 (not sure of exact version, not at my OS X machine atm) – Jules Nov 13 '15 at 06:29
  • 5
    still not working on Xcode 7.2 iOS 9.2 – Mojtaba Dec 17 '15 at 01:20
  • still not working on Xcode 7.2.1 iOS 9.2 – haxpor Feb 29 '16 at 05:18
  • still not working.... what a strange bug.... apple should fix it as soon as possible – Divyanshu Sharma Mar 10 '16 at 06:07
  • Anyone file a radar about this bug? – JAL Jun 24 '16 at 17:09
  • 2
    Filed a radar, still does not work in 7.3.1 iOS 9.3. – Legoless Jul 16 '16 at 19:02
  • Apple updated my bug report: the original bug report was closed. I'll try with the last Xcode build when I can. – Quentin Hayot Aug 23 '16 at 09:08
  • As of Xcode 8.0 (8A218a) I can say that bug is gone :) – Ruslan Mansurov Sep 19 '16 at 15:55
  • Now it works pretty well with Xcode 8:) great job Apple... – Bartłomiej Semańczyk Dec 14 '16 at 08:47
  • Somehow this bug effects are still present in Xcode 10. I set system font in nib with multiple size class fonts for a label, and override programmatically to make sure correct font is displayed at runtime using awakeWithNib, it displays system font instead. – NightFury Mar 21 '19 at 13:04

14 Answers14

41

Fast fix:

1) Set fonts as System for size classes

Label attributes inspector

2) Subclass UILabel and override "layoutSubviews" method like:

- (void)layoutSubviews
{
  [super layoutSubviews];

   // Implement font logic depending on screen size
    if ([self.font.fontName rangeOfString:@"bold" options:NSCaseInsensitiveSearch].location == NSNotFound) {
        NSLog(@"font is not bold");
        self.font = [UIFont fontWithName:@"Custom regular Font" size:self.font.pointSize];
    } else {
        NSLog(@"font is bold");
        self.font = [UIFont fontWithName:@"Custom bold Font" size:self.font.pointSize];
    }

}

By the way, it is a very convenient technique for iconic fonts

Khaled Annajar
  • 15,542
  • 5
  • 34
  • 45
razor28
  • 1,077
  • 10
  • 17
  • While this is a good workaround if the font used throughout the app is the same, it's a bit less convenient if you need to use different fonts in different places, because then you have to have different subclasses, etc. In my case, I already had setup programmatic fonts, and there are a lot of different ways you can do that. But the point of my question isn't to ask how to do this programmatically: it's to ask why this doesn't work properly using Storyboards (it's an Apple bug). – mnemia Jan 22 '15 at 17:42
  • @mnemia I agree. My aim is just to show one way how handle that bug. – razor28 Jan 23 '15 at 04:53
  • 1
    Works great. You don't need to subclass UILabel though, you may just override this method in the view that holds the label and do `self.label.font = ...` This way you can use different fonts at different places. – KPM Jul 15 '15 at 11:59
  • Or you could set up an @IBInspectable, to store the font name, to reuse the same custom label class and request different fonts directly in the storyboard. – Craig Grummitt Oct 16 '15 at 16:11
  • Is there a similar solution for UITextField? – Chris Byatt Oct 16 '15 at 18:13
  • I can't get this to work I've created a subclass, set the self.label.font and set the subclass in IB and my property outlets variable object types, any further suggestions? – Jules Nov 12 '15 at 09:10
  • Works perfect for me. But I have used `- (id)initWithCoder:(NSCoder*)coder { self = [super initWithCoder:coder]; if (self) { self.font = [UIFont fontWithName:@"Custom-font-name" size:self.font.pointSize]; } return self; }` Instead of `- (void)layoutSubviews` – Dimple Shah Dec 29 '15 at 06:04
  • work for me iOS8/9 xCode Version 7.1.1. But **DO NOT FORGET** to change font to System – WINSergey Feb 09 '16 at 13:05
17

After trying everything, I eventually settled on a combination of the above solutions. Using Xcode 7.2, Swift 2.

import UIKit

class LabelDeviceClass : UILabel {

    @IBInspectable var iPhoneSize:CGFloat = 0 {
        didSet {
            if isPhone() {
                overrideFontSize(iPhoneSize)
            }
        }
    }

    @IBInspectable var iPadSize:CGFloat = 0 {
        didSet {
            if isPad() {
                overrideFontSize(iPadSize)
            }
        }
    }

    func isPhone() -> Bool {
        // return UIDevice.currentDevice().userInterfaceIdiom == .Phone
        return !isPad()
    }

    func isPad() -> Bool {
        // return UIDevice.currentDevice().userInterfaceIdiom == .Pad
        switch (UIScreen.mainScreen().traitCollection.horizontalSizeClass, UIScreen.mainScreen().traitCollection.verticalSizeClass) {
        case (.Regular, .Regular):
            return true
        default:
            return false
        }
    }

    func overrideFontSize(fontSize:CGFloat){
        let currentFontName = self.font.fontName
        if let calculatedFont = UIFont(name: currentFontName, size: fontSize) {
            self.font = calculatedFont
        }
    }

}
  • @IBInspectable lets you set the font size in the Storyboard
  • It uses a didSet observer, to avoid the pitfalls from layoutSubviews() (infinite loop for dynamic table view row heights) and awakeFromNib() (see @cocoaNoob's comment)
  • It uses size classes rather than the device idiom, in hopes of eventually using this with @IBDesignable
  • Sadly, @IBDesignable doesn't work with traitCollection according to this other stack article
  • The trait collection switch statement is performed on UIScreen.mainScreen() rather than self per this stack article
Community
  • 1
  • 1
Robert Chen
  • 5,179
  • 3
  • 34
  • 21
9

Workaround for UILabel: keep the same font size on all Size Classes, but instead change your label height accordingly in each Size Class. Your label must have autoshrink enabled. It worked nicely in my case.

Phil
  • 2,784
  • 2
  • 28
  • 26
  • 1
    I was able to do the same thing by changing my label width in each Size Class. Good tip! – dmzza Aug 26 '15 at 01:10
  • 1
    I didn't get the font to shrink by setting a smaller fixed height for my UILabel, how exactly did you get it to work? `adjustsFontSizeToFitWidth` seems to work only for width (which I cannot use because the text of the labels is dynamic). – ernesto Sep 22 '15 at 09:59
  • I have the same issue with adjust to fit width – Jules Nov 13 '15 at 06:28
  • 1
    I ended up setting the height constraint of the Label in proportion to the height of the superview. That works fine as long as you set the "Lines" parameter of the Label in IB to 0. – Dorian Roy Apr 17 '16 at 20:57
8

This (and other Xcode - Size Classes related) bug caused me some serious grief recently as I had to go through a huge storyboard file hacking things away.

For anyone else in this position, I'd like to add something on top of @razor28's answer to ease the pain.

In the header file of your custom subclass, use IBInspectable for your runtime attributes. This will make these attributes accessible from the "Attributes Inspector", visually right above the default position for font settings.

Example use:

@interface MyCustomLabel : UILabel

    @property (nonatomic) IBInspectable NSString *fontType;
    @property (nonatomic) IBInspectable CGFloat iphoneFontSize;
    @property (nonatomic) IBInspectable CGFloat ipadFontSize;

@end

This will very helpfully produce this output:

enter image description here

An added benefit is that now we don't have to add the runtime attributes manually for each label. This is the closest I could get to XCode's intended behaviour. Hopefully a proper fix is on its way with iOS 9 this summer.

joakim
  • 3,533
  • 2
  • 23
  • 28
3

Similar solution to @razor28's one but I think a little more universal. Also, works fine on older iOS versions

https://gist.github.com/softmaxsg/8a3ce30331f9f07f023e

Vitaly
  • 515
  • 3
  • 9
  • And sure, you can set property overrideFontName in Interface Builder which allows to avoid writing unnecessary code – Vitaly Apr 02 '15 at 20:02
  • This didn't work for me, I listed out all the installed fonts within the above method and my font was listed and assigned in the variable but didn't alter the font show in the label on the simulator and I assume the device – Jules Nov 13 '15 at 06:27
3

The bug is still valid in XCode 7.0 GM.

Razor28's solution causes infinite loops in some cases. My experience has been with using it in conjunction with SwipeView.

Instead, I suggest that you:

1) Subclass UILabel and override setFont:

- (void)setFont:(UIFont *)font
{
    font = [UIFont fontWithName:(@"Montserrat") size:font.pointSize];
    [super setFont:font];
}

2) Set the custom class of your UILabels and then set the font size classes by using System font

enter image description here

Foti Dim
  • 1,303
  • 13
  • 19
  • @Maarten1909 Are you referring to the workaround or the straightforward XCode way? If the first can you please specify the version so I try to reproduce it? – Foti Dim Sep 26 '15 at 07:29
  • It seems that I've been using the wrong font family name all time. It turns out that in such case the fonts are reset to a system font with 14.0 pint-size. My bad. But it might be useful to others! – Berendschot Sep 26 '15 at 13:57
0

The problem is still there that you cannot use the feature to set Fonts for different size classes from interface builder.

Just set font based on the device you want just like below:

if (Your Device is iPAd) //For iPad
{
   [yourLabel setFont:[UIFont fontWithName:@"FontName" size:FontSize]]; 
}
else  //For Other Devices then iPad
{
   [yourLabel setFont:[UIFont fontWithName:@"FontName" size:FontSize]]; 
}

This works perfectly on all devices.

Nirmit Dagly
  • 1,272
  • 1
  • 12
  • 25
0

None of these worked for me, but this did. You also need to use the system font in IB

#import <UIKit/UIKit.h>

@interface UILabelEx : UILabel


@end

#import "UILabelEx.h"
#import "Constants.h"

@implementation UILabelEx

- (void) traitCollectionDidChange: (UITraitCollection *) previousTraitCollection {
    [super traitCollectionDidChange: previousTraitCollection];

    self.font = [UIFont fontWithName:APP_FONT size:self.font.pointSize];   
}
@end
Jules
  • 7,568
  • 14
  • 102
  • 186
0

Still no signed right answer. This code works fine for me. You must disable font size for size classes in interface builder first. In IB you can use custom font.

- (void) traitCollectionDidChange: (UITraitCollection *) previousTraitCollection {
    [super traitCollectionDidChange: previousTraitCollection];

    if ((self.traitCollection.verticalSizeClass != previousTraitCollection.verticalSizeClass)
        || self.traitCollection.horizontalSizeClass != previousTraitCollection.horizontalSizeClass) {

         self.textField.font = [UIFont fontWithName:textField.font.fontName size:17.f];

    }
}
0

A combination of some of the later answers above were helpful. Here's how I solved the IB bug via a Swift UILabel extension:

import UIKit

// This extension is only required as a work-around to an interface builder bug in XCode 7.3.1
// When custom fonts are set per size class, they are reset to a small system font
// In order for this extension to work, you must set the fonts in IB to System
// We are switching any instances of ".SFUIDisplay-Bold" to "MuseoSans-700" and ".SFUIDisplay-Regular" to "MuseoSans-300" and keeping the same point size as specified in IB

extension UILabel {
    override public func traitCollectionDidChange(previousTraitCollection: UITraitCollection?) {
        super.traitCollectionDidChange(previousTraitCollection)

        if ((traitCollection.verticalSizeClass != previousTraitCollection?.verticalSizeClass) || traitCollection.horizontalSizeClass != previousTraitCollection?.horizontalSizeClass) {
            //let oldFontName = "\(font.fontName)-\(font.pointSize)"

            if (font.fontName == systemFontRegular) {
                font = UIFont(name: customFontRegular, size: (font?.pointSize)!)
                //xlog.debug("Old font: \(oldFontName) -> new Font: \(font.fontName) - \(font.pointSize)")
            }
            else if (font.fontName == systemFontBold) {
                font = UIFont(name: customFontBold, size: (font?.pointSize)!)
                //xlog.debug("Old font: \(oldFontName) -> new Font: \(font.fontName) - \(font.pointSize)")
            }
        }
    }
}
buczek
  • 2,011
  • 7
  • 29
  • 40
benjam
  • 1
-1

I am using Swift, XCode 6.4. So this is what I did

import Foundation
import UIKit

    @IBDesignable class ExUILabel: UILabel {

        @IBInspectable var fontName: String = "Default-Font" {
            didSet {
                self.font = UIFont(name: fontName, size:self.font.pointSize)
            }
        }

        override func layoutSubviews() {
            super.layoutSubviews()
            self.font = UIFont(name: fontName, size:self.font.pointSize)
        }
    }
  1. Goto Designer -> Identity Inspector -> Set the class to ExUILabel

  2. Then go to Attribute inspector in designer and set the font name.

kakopappa
  • 5,023
  • 5
  • 54
  • 73
-1

I've come up with even faster fix (I assume you always use your one custom font).

Create a category for UILabel and include in files using buggy storyboard with size classes and dedicated font setting for various classes:

@implementation UILabel (FontFixForSizeClassesAppleBug)

- (void)layoutSubviews
{
    [super layoutSubviews];
    if([[UIFont systemFontOfSize:10].familyName isEqualToString:self.font.familyName]) {
        //workaround for interface builder size classes bug which ignores custom font if various classes defined for label: http://stackoverflow.com/questions/26166737/custom-font-sizing-in-xcode6-size-classes-not-working-properly-w-custom-fonts
        self.font = [UIFont fontWithName:@"YOUR_CUSTOM_FONT_NAME" size:self.font.pointSize];
    }
}

@end

Just use your custom fonts in storyboard. When buggy interpreter will use system font instead your own this category will switch it to your custom font.

Heps
  • 945
  • 9
  • 24
  • -1: Not a good idea to override methods in a category like this. Reason: https://stackoverflow.com/questions/5272451/overriding-methods-using-categories-in-objective-c Instead, should swizzle (though hacky) or subclass. – JRG-Developer Sep 06 '16 at 15:21
-6

I had the same problem and found one not really clear, but fine solution!

  1. First you set the required font size in storyboard with system font name.
  2. Then to this label you assign a tag from 100 to 110 (or more, but 10 was always enough for me in one view controller).
  3. Then put this code to your VC's source file and don't forget to change font name. Code in swift.
override func viewDidLayoutSubviews() {
    for i in 100...110 {
        if let label = view.viewWithTag(i) as? UILabel {
            label.font = UIFont(name: "RotondaC-Bold", size: label.font.pointSize)
        }
    }
}
serg_ov
  • 563
  • 7
  • 18
  • 1
    Don't use tags. Use IBOutlets. Tags are bad. See WWDC 2015 session 231: “If you are using View With Tag or Set Tag UIView API and shipping code, I am going to encourage you to move away from that. As a replacement to this, declare properties on your classes, and then you will have real connections to those views that you need later.” – KPM Jul 15 '15 at 12:08
  • i sad that this solution is not perfect, but it works! – serg_ov Jul 15 '15 at 17:31
-8
  • add Fonts provided by application into your app .plist
  • add your .ttf file to item 0
  • use it [UIFont fontWithName:"example.ttf" size:10]
Lucifer
  • 29,392
  • 25
  • 90
  • 143
  • This is not an answer to this problem. Obviously you can use custom fonts programmatically, but what my question refers to is a bug in iOS 8 that prevents custom fonts from automatically changing depending on size class. – mnemia Jan 19 '15 at 17:24