Getting rid of string constants in Objective-C

    The magic constants in the code are evil. String constants in code are even more evil.
    And it seems that you can’t get anywhere from them, they are everywhere:

    1) When loading objects from xibs:
    MyView* view = [[[NSBundle mainBundle] loadNibNamed:@"MyView" owner:self options:nil] lastObject];

    MyViewController* controller = [MyViewController initWithNibName:@"MyViewController" bundle:nil];

    2) When working with CoreData:
    NSFetchRequest *request = [[NSFetchRequest alloc] init];
    [request setEntity:[NSEntityDescription entityForName:@"MyCoreDataClass" inManagedObjectContext:moc]];
    [request setSortDescriptors:@[ [[NSSortDescriptor alloc] initWithKey:@"someProperty" ascending:NO] ]];

    3) If you use KVO, then the lines appear here:
    [self addObserver:someObservedObject 
           forKeyPath:@"someProperty"
              options:(NSKeyValueObservingOptionNew |  NSKeyValueObservingOptionOld) 
              context:nil];

    4) Well and KVC:
    NSInteger maxValue = [[arrayOfMyClassObjects valueForKeyPath:@"@max.someProperty"] intValue];

    5) But even if you prefer CoreData to work directly with SQLite, you are squeamish with xibs, then this code should be familiar to you:
    [self.tableView dequeueReusableCellWithIdentifier:@"MyTableViewCell"];

    6) Well, when Apple introduced the Storyboard to the world - it was wonderful, if not for one thing:
    [self performSegueWithIdentifier:@"MySegue" sender:nil]

    -(void)prepareForSegue:(UIStoryboardSegue *)segue sender:( id )sender {
       if ( [segue.identifier isEqual:@"MySegue"] );
    }

    Do you see the problem? It consists in the fact that the compiler does not check the contents of the lines in any way, because it does not know (and cannot, in principle, know) what is contained in them. And if you seal yourself or change the value of the corresponding fields in xcdatamodel / xib / storyboard / rename property, then the error will come out not at the compilation stage, but in runtime, and it will be longer and more expensive to catch and fix it.
    So what can be done?
    Some lines can be dealt with by administrative measures, and some by means of special tools.

    Download from xibs

    For example, if you start with the rule that the name of the xib must match the name of the class that it contains, then the code from the first example can be rewritten like this:
    MyView* view = [[[NSBundle mainBundle] loadNibNamed:NSStringFromClass([MyView class]) owner:self options:nil] lastObject];

    MyViewController* controller = [MyViewController initWithNibName:NSStringFromClass([MyViewController class] bundle:nil];

    The advantage of this solution is that if we decide to rename our class (Xcode -> Edit -> Refactor -> Rename leaving the "Rename related files" checkbox selected), then when renaming xib it will also be renamed, and, accordingly, loading view / controller does not suffer.

    Coredata

    With example 2, the solution is a little more complicated and complex.
    First we need MagicalRecord and mogenerator.

    If you're working with CoreData and still not using these great tools, then it's time to start.
    Add MagicalRecord to our podfile (well, or copying files to the project in the old fashioned way - for more details see gihab)
    pod MagicalRecord

    And install mogenerator:
    brew install mogenerator

    Or download the installer from the site and install it manually.

    mogenerator creates source files on the basis of the CoreData model file, which otherwise would have to be written by hand.
    Now we need to configure the project to run the utility with each build.
    Project -> Targets -> Add Build Phase -> Add Run Script:
    Go to the target settings, to the Build Phases tab and add a new phase in the form of a script:
    image
    Well, the script itself:
    image
    At the same level as our model, you need to create two folders Human and Machine . Click CMD + B - after which we add these two folders to the project. They will contain the generated model files from CoreData.

    KVO and KVC

    Another thing in Objective-C that uses string constants extensively is KeyPath for KVO and KVC. You can use the above mogenerator. If we have the MyCoreDataClass class in our model, then the mogenerator will create the MyCoreDataClassAttributes, MyCoreDataClassRelationships and MyCoreDataClassFetchedProperties structures. So now we can rewrite Example 2 as follows:
    NSArray* arr = [MyCoreDataClass MR_findAllSortedBy:MyCoreDataClassAttributes.someProperty ascending:NO inContext:moc];

    And example number 3 is like this:
    [self addObserver:myCoreDataClass
           forKeyPath:MyCoreDataClassAttributes.someProperty
              options:(NSKeyValueObservingOptionNew |  NSKeyValueObservingOptionOld) 
              context:nil];

    But such a solution is only suitable for CoreData. We need something more general. Valid-KeyPath

    library is quite suitable for this :
    #import "EXTKeyPathCoding.h"
    [self addObserver:myClass
           forKeyPath:KEY.__(MyClass, someProperty)
              options:(NSKeyValueObservingOptionNew |  NSKeyValueObservingOptionOld) 
              context:nil];

    Either EXTKeyPathCoding bibloteki libextobjc :
    pod libextobjc

    #import "MTKValidKeyPath.h"
    [self addObserver:myClass
           forKeyPath:@keypath(MyClass.new, someProperty)
              options:(NSKeyValueObservingOptionNew |  NSKeyValueObservingOptionOld) 
              context:nil];

    The advantages of both solutions will be autocompletion from the IDE at the time of writing the code itself, as well as verification of KeyPath itself at the compilation stage.

    If you use KVC, as in example 4, then to generate KeyPath you can use any of the libraries presented above, or you can use any of the LINQ-like libraries for Objective-C, for example LinqToObjectiveC
    pod LinqToObjectiveC

    NSInteger maxValue = [arrayOfMyClassObjects aggregate:^(MyClass* myClass, NSInteger aggregate){
       return MAX( [myClass.someProperty intValue],  aggregate);
    }];


    Storyboard

    As for the UI part, the ssgenerator utility will help us . Download it from here.
    Create another script for it in the project:
    image
    After that, add the generated files StoryboardSegue.h and StoryboardSegue.m to the project. These files contain categories for those controllers in the storyboard that contain a UIStoryboardSegue or UITableViewCell for which identifiers are defined. Now you can use:
    [self.tableView dequeueReusableCellWithIdentifier:self.cell.MyTableViewCell];

    [self performSegueWithIdentifier:self.segue.MySegue sender:nil]

    -(void)prepareForSegue:(UIStoryboardSegue *)segue sender:( id )sender {
       if ( [segue.identifier isEqual:self.segue.MySegue] );
    }


    Conclusion


    Getting rid of string constants in the code is not an end in itself, but a way to significantly save on writing and maintaining code due to checks at compile time. Some of the described methods require third-party utilities, and some require third-party libraries. For some programmers, the described techniques will seem “complicated” and “poorly readable,” but they are all aimed at identifying errors in the code as early as possible. So if you write code that does not change, code in which there are no errors - these methods are not for you.

    If you have in mind utilities and libraries that help you personally get rid of the constants in the code and make it more flexible and supported - write in the comments, I will gladly add them to this review.

    Also popular now: