Deprecated iOS Methods: Rotation Setup

I haven’t updated my code for a while because I wanted to continue to support iOS5 on first generation iPads. Since Apple allows older devices to download older versions of the software, and my code in the App Store appears to be stable, I decided to update the existing code and get rid of deprecated code warnings. (I have 56 deprecated method warnings that I need to get rid of.)

First up is shouldAutorotateToInterfaceOrientation. This method, along with shouldAutorotate is sprinkled throughout my code because it was required for each view.

The old code, in the AppDelegate.m file, supported iOS 5-9 and looked like this:


#pragma mark - Handle rotation for different versions of the OS
- (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation {
    return YES;
}

// iOS 6 and above
- (BOOL)shouldAutorotate {
    return YES;
}

- (NSUInteger)supportedInterfaceOrientations {
    
    #ifdef UIInterfaceOrientationMaskAll
        return UIInterfaceOrientationMaskAll;  // iOS6+ method.
    #else
        return 0;
    #endif
}

I replaced it in the AppDelegate.m with this line:


- (NSUInteger)supportedInterfaceOrientations {
    return UIInterfaceOrientationMaskAll;
}

Update 2018-03-20
The current version of Xcode prefers that NSUInteger be replaced with UIInterfaceOrientationMask .

and removed lots of occurrences of shouldAutorotateToInterfaceOrientation.

“In iOS 6 and iOS 7, your app supports the interface orientations defined in your app’s Info.plist file.” Source

You don’t need to, but because I like to have clean code, I removed these lines in my .plist files:


<key>UISupportedInterfaceOrientations</key>
<array>
    <string>UIInterfaceOrientationPortrait</string>
    <string>UIInterfaceOrientationLandscapeLeft</string>
    <string>UIInterfaceOrientationLandscapeRight</string>
    <string>UIInterfaceOrientationPortraitUpsideDown</string>
</array>
<key>UISupportedInterfaceOrientations~ipad</key>
<array>
    <string>UIInterfaceOrientationPortrait</string>
    <string>UIInterfaceOrientationPortraitUpsideDown</string>
    <string>UIInterfaceOrientationLandscapeLeft</string>
    <string>UIInterfaceOrientationLandscapeRight</string>
</array>

You can remove them with a text editor or using Xcode and deleting Supported interface orientations and Supported interface orientations (iPad).

Next up is willRotateToInterfaceOrientation and willAnimateRotationToInterfaceOrientation

NS_DESIGNATED_INITIALIZER warnings

I programmatically create several tables and the code has worked fine for years. It did not generate any warnings two weeks ago when I last ran it. I’ve since updated to Xcode 8.3 and 10.10.3 and now get three warnings for each UITableViewController.


Method override for the designated initializer of the superclass '-initWithStyle:' not found.
Method override for the designated initializer of the superclass '-initWithCoder:' not found.
Method override for the designated initializer of the superclass '-initWithNibName:bundle:' not found.

The code to initialize the table is similar for all of my tables:


- (instancetype)initInManagedObjectContext:(NSManagedObjectContext *)context 
                 withScoreKeeper:(ScoreKeeper *)scorer 
                    withWordList:(WordList *)wordlist {

    self = [super initWithStyle:UITableViewStyleGrouped];

    if (self) {
        _mObjContext = context;
        _scoreKeeper = scorer;
        _wordList = wordlist;
    }
    return self;
}

and the .h looks like this:


@interface SettingsTableViewController : UITableViewController {
    UIPopoverController *popover;

}
    - (instancetype)initInManagedObjectContext:(NSManagedObjectContext *)context 
                     withScoreKeeper:(ScoreKeeper *)scorer 
                        withWordList:(WordList *)wordlist NS_DESIGNATED_INITIALIZER;

Here’s what I found out. I am not a programmer, so parts could be mistaken.

When I opened my project for the first time with Xcode 5 and Xcode 6, I was prompted to “Convert to Modern Objective-C Syntax”. (The latest version of Xcode puts it under Edit > Convert > Convert to Modern Objective-C Syntax.) I allowed Xcode to convert everything and understood most of the changes. I understood how NS_DESIGNATED_INITIALIZER works but not why it works. Since I had no compiler errors and everything worked as before, I promptly forgot about it. In the latest version of Xcode, they appear to have updated the compiler and that’s what triggered my warning message.

From Apple’s notes: Adopting Modern Objective C

Using this macro introduces a few restrictions:

The implementation of a designated initializer must chain to a superclass init method (with [super init…]) that is a designated initializer for the superclass.

The implementation of a convenience initializer (an initializer not marked as a designated initializer within a class that has at least one initializer marked as a designated initializer) must delegate to another initializer (with [self init…]).

If a class provides one or more designated initializers, it must implement all of the designated initializers of its superclass.

Here’s what I think happened. First, I got three warning messages because UITableViewController has three designated initializers.


- (instancetype)initWithStyle:(UITableViewStyle)style NS_DESIGNATED_INITIALIZER;
- (instancetype)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil NS_DESIGNATED_INITIALIZER;
- (instancetype)initWithCoder:(NSCoder *)aDecoder NS_DESIGNATED_INITIALIZER;

I violated the third restriction because I did not implement all of the designated initializers of the superclass.

Removing the NS_DESIGNATED_INITIALIZER macro from the .h files made the warnings go away.

The question then arises, Do I care that these classes have no designated initializers? Probably not.
First, there are no other initializers in these classes, so I won’t accidentally call the wrong one. Second, I’m not a programmer by training, so when I started writing apps, I used the procedural programming style that I was used to. Until recently, I had never subclassed a class. So I won’t be subclassing this one and there won’t be any subclasses to worry about. Now that I know a bit more about Objective C, it turns out that every class I wrote was a subclass of one of iOS’s classes and that actually explains a bit about why I was getting the errors.

I did not realize that I could create the table view object by calling a method on its superclass. For example, this call works:


SettingsTableViewController *stvc = [[SettingsTableViewController alloc] initWithStyle: UITableViewStyleGrouped];

It works even when I have NS_DESIGNATED_INITIALIZER set. Presumably no other warnings are sent because the compiler is already complaining about not calling a designated initializer of super.

And as long as the views called in the table view do not need any of the objects that were passed in, everything is fine. If a view that is linked to in the table does need one of the objects, then obviously the app crashes.

Since I never want to call anything but the designated initializer, it was suggested that I use NSAssert() to make sure that I don’t call the designated initializers of my superclass, and make all the warnings go away with this code:


- (instancetype)initWithStyle:(UITableViewStyle)style {
    NSAssert(NO, @"%@", @"Tried to implement with initWithStyle");
    self = [self initInManagedObjectContext:nil withScoreKeeper:nil withWordList:nil];
    return self;
}

- (instancetype)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil {
    NSAssert(NO, @"%@", @"Tried to implement with initWithNibName");
    self = [self initInManagedObjectContext:nil withScoreKeeper:nil withWordList:nil];
    return self;
}
- (instancetype)initWithCoder:(NSCoder *)aDecoder {
    NSAssert(NO, @"%@", @"Tried to implement with initWithCoder");
    self = [self initInManagedObjectContext:nil withScoreKeeper:nil withWordList:nil];
    return self;
}

Now I get this error in the log when I try to call initWithStyle directly.

*** Assertion failure in -[SettingsTableViewController initWithStyle:], /Users/jscarry/Words/Words/Settings/SettingsTableViewController.m:37

This stackoverflow question has some useful info.

This article explains more about why it is implemented.

Update: I found this article (UITableViewController Designated Initializer Woes) where they have a more complete explanation of the issue and a more complete fix.

Setting colors with an NSString

I wanted to color some letters in an NSAttributed string using a random list of colors that I generated. What I wanted to do was something like this:


NSString colorName = [NSString stringWithFormat:@"%@Color", colorList[i] ];
imageColor = [UIColor colorName];

But you can’t feed an NSString to the UIColor method. Use a selector to do it.
Like this:
 
NSString *colorname = [NSString stringWithFormat:@"%@Color", colorList[i] ];
SEL labelColor = NSSelectorFromString(colorname);
UIColor *imageColor = [Utilities uiColorFromColorName:colorname];
imageColor = [UIColor performSelector:labelColor];

I had to create pink separately since it is not in the Apple supplied color list. However, I don’t really like the other colors so I created a Utility method to create a UIColor from our RGB color values. It generates a UIColor that I can use for my attributed string.


UIColor *imageColor = [Utilities uiColorFromColorName:[lettersExploded[2] lowercaseString] ];


+ (UIColor *)uiColorFromColorName:(NSString *)colorName {

UIColor *imageColor = nil;

    if ( [colorName isEqualToString:@"black"] ) {
        imageColor = [UIColor colorWithRed:0.0f/255.0f green:0.0f/255.0f blue:0.0f/255.0f alpha:1];
    } else if ( [colorName isEqualToString:@"blue"] ) {
        imageColor = [UIColor colorWithRed:8.0f/255.0f green:41.0f/255.0f blue:247.0f/255.0f alpha:1];
    } else if ( [colorName isEqualToString:@"brown"] ) {
        imageColor = [UIColor colorWithRed:95.0f/255.0f green:47.0f/255.0f blue:0.0f/255.0f alpha:1];
    } else if ( [colorName isEqualToString:@"gray"] ) {
        imageColor = [UIColor colorWithRed:140.0f/255.0f green:140.0f/255.0f blue:140.0f/255.0f alpha:1];
    } else if ( [colorName isEqualToString:@"green"] ) {
        imageColor = [UIColor colorWithRed:51.0f/255.0f green:153.0f/255.0f blue:51.0f/255.0f alpha:1];
    } else if ( [colorName isEqualToString:@"orange"] ) {
        imageColor = [UIColor colorWithRed:255.0f/255.0f green:116.0f/255.0f blue:0.0f/255.0f alpha:1];
    } else if ( [colorName isEqualToString:@"pink"] ) {
        imageColor = [UIColor colorWithRed:255.0f/255.0f green:90.0f/255.0f blue:148.0f/255.0f alpha:1];
    } else if ( [colorName isEqualToString:@"purple"] ) {
        imageColor = [UIColor colorWithRed:140.0f/255.0f green:0.0f/255.0f blue:140.0f/255.0f alpha:1];
    } else if ( [colorName isEqualToString:@"red"] ) {
        imageColor = [UIColor colorWithRed:233.0f/255.0f green:17.0f/255.0f blue:0.0f/255.0f alpha:1];
    } else if ( [colorName isEqualToString:@"white"] ) {
        imageColor = [UIColor colorWithRed:255.0f/255.0f green:255.0f/255.0f blue:255.0f/255.0f alpha:1];
    } else if ( [colorName isEqualToString:@"yellow"] ) {
        imageColor = [UIColor colorWithRed:255.0f/255.0f green:255.0f/255.0f blue:0.0f/255.0f alpha:1];
    }

  return imageColor;
}

An example of using Enums

After I posted my deviceType code on Stackoverflow, one of the commenters suggested that I use enums. Now I know what they are because Apple uses them all the time but I didn’t know how useful they were. I did some research on enums and they are nice. They make the code a bit more readable, but mostly they allow the compiler to help you type and catch errors. Xcode will autocomplete your deviceType for you and will give you the error: Use of undeclared identifier if you try to use a value that isn’t defined. I suspect that they are slightly faster in comparisons since they use integers rather than strings. Also, you can use them directly in switch statements. Here’s the code rewritten as an enum. I prefixed the values with LF but you should use whatever is appropriate for your project.

This is in my header file


// Devices as of Fall 2014
typedef NS_ENUM(NSInteger, LFdeviceType) {
    LFDeviceTypePhoneClassic,
    LFDeviceTypePhoneRetina3_5,
    LFDeviceTypePhoneRetina4,
    LFDeviceTypePhone6,
    LFDeviceTypePhone6Plus,
    LFDeviceTypePadClassic,
    LFDeviceTypePadRetina,
};

And this is in my .m file.


+ (NSInteger)deviceType {
    CGSize screenSize = [[UIScreen mainScreen] bounds].size;
    CGFloat deviceScale = [UIScreen mainScreen].scale;
    LFdeviceType device = LFDeviceTypePhoneClassic;

    if (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPhone) {
        device = LFDeviceTypePhoneClassic; // Just in case it doesn't make it through the conditionals
        // Classic has a resolution of 480 × 320
        if( (screenSize.height == 480 || screenSize.width == 480) && deviceScale == 1.0f ) {
            device = LFDeviceTypePhoneClassic;
        // Retina has a resolution of 960 × 640
        } else if( (screenSize.height == 480 || screenSize.width == 480) && deviceScale == 2.0f ) {
            device = LFDeviceTypePhoneRetina3_5;
        // Retina 4" has a resolution of 1136 x 640
        } else if (screenSize.height == 568 || screenSize.width == 568 ) {
            device = LFDeviceTypePhoneRetina4;
        // iPhone 6 has a resolution of 1334 by 750
        } else if (screenSize.height == 667 || screenSize.width == 667 ) {
            device = LFDeviceTypePhone6;
        // iPhone 6 Plus has an actual size of 2208 × 1242 and resolution of 1920 by 1080
        // Reported size is 736 x 414
        } else if (screenSize.height == 736 || screenSize.width == 736 ) {
            device = LFDeviceTypePhone6Plus;
        }

    } else if (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPad) {
        device = LFDeviceTypePadClassic; // Just in case it doesn't make it through the conditionals
        if(deviceScale == 1.0f) {
            device = LFDeviceTypePadClassic;
        } else if (deviceScale == 2.0f) {
            device = LFDeviceTypePadRetina;
        }
    }
    //NSLog(@"The device is %@ scale is %f and the height is %f and width is %f", device, deviceScale, screenSize.height, screenSize.width);

    return device;
}

Call it like this:

if ( (   [Utilities deviceType] == LFDeviceTypePhoneClassic 
      || [Utilities deviceType] == LFDeviceTypePhoneRetina3_5) &&
        numberOfFoilsOnScreen > 7 ) {
        numberOfFoilsOnScreen = 7;
}