16

In a word game for iPhone:

app screenshot

I'm trying to use the following code in my custom view Tile.m:

- (void)awakeFromNib
{
    [super awakeFromNib];

    static NSDictionary* const letterValues = @{
                                         @"A": @1,
                                         @"B": @4,
                                         @"C": @4,
                                         // ...
                                         @"X": @8,
                                         @"Y": @3,
                                         @"Z": @10,
                                         };

    NSString* randomLetter = [kLetters substringWithRange:[kLetters rangeOfComposedCharacterSequenceAtIndex:arc4random_uniform(kLetters.length)]];
    int letterValue = [letterValues[randomLetter] integerValue];

    _smallLetter.text = _bigLetter.text = randomLetter;
    _smallValue.text = _bigValue.text = [NSString stringWithFormat:@"%d", letterValue];
}

Unfortunately this gives me compile error Initializer element is not a compile-time constant and I have to remove the static keyword to get my app compile in Xcode (here fullscreen):

Xcode screenshot

I think I initialize the NSDictionary correctly - by using the new Objective-C Literals syntax.

But why can't I use static here?

I thought it would be appropriate here to make sure that my letterValues constant is set only once?

Alexander Farber
  • 21,519
  • 75
  • 241
  • 416
  • move it out from class – sage444 Mar 20 '14 at 13:21
  • Moving it out from class results in same compile-time error – Alexander Farber Mar 20 '14 at 13:22
  • @Btw., that code is *valid* and works in Objective-C++ :-) – Martin R Mar 20 '14 at 13:41
  • Which code is valid (the code in my question doesn't compile in Xcode 5.1) and what do you mean by Objective-**C++**? – Alexander Farber Mar 20 '14 at 14:20
  • 3
    @AlexanderFarber: See http://stackoverflow.com/questions/3684112/what-is-objective-c - Objective-C++ is the "combination" of Objective-C with C++ (and is poorly documented). Objective-C++ files have the extension .mm. Your code should compile if you rename the file to "Tile.mm", because C++ has different rules for static initializers. - But this was just meant as a remark, not as *the* solution. - You can also have a look at http://stackoverflow.com/questions/21365532/meaning-of-static-nsmutablearray-and-multi-thread-usage, which is a different question with a similar answer. – Martin R Mar 20 '14 at 14:32

5 Answers5

45

You can only set a static variable during initialization with a constant. @{} creates an object, thus not a constant.

Do this instead:

- (void)awakeFromNib
{
    [super awakeFromNib];

    static NSDictionary* letterValues = nil;

    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        letterValues = @{
          @"A": @1,
          @"B": @4,
          @"C": @4,
          // ...
          @"X": @8,
          @"Y": @3,
          @"Z": @10,
          };
    });


    ...
}

Some other answers here suggest a check for nil instead of dispatch once, but that can cause issues when creating multiple tiles at the same time (via threads). dispatch_once implements the required locking.

Dave Wood
  • 13,143
  • 2
  • 59
  • 67
  • 1
    Using `dispatch_once` is the best and safest way to initialize this kind of data objects. This is the most correct answer here. – Léo Natan Mar 21 '14 at 13:39
5

You can use static, but the assignment can't be made on the same line. Try this:

- (void)awakeFromNib {
    [super awakeFromNib];

    static NSDictionary* letterValues = nil;
    if (!letterValues) {
        letterValues = @{@"A": @1,
                         @"B": @4,
                         @"C": @4,
                         // ...
                         @"X": @8,
                         @"Y": @3,
                         @"Z": @10};
    }
    ...
}

The reason is that the @{<key> : <value>} syntax is translated by the compiler into an Objective-C method ([[NSPlaceholderDictionary alloc] initWithObjects:forKeys:count:]), which cannot be resolved at compile-time.

Austin
  • 5,625
  • 1
  • 29
  • 43
5

Another alternative is using the Macro Approach:

Macro (The Easiest) Approach (2021)

#define kScreenNames @{ \
        @"HOME":                     @"Home Page", \
        @"SEARCH":                   @"Search Page", \
        @"MYLISTS":                  @"My List", \
        @"MYACCOUNT":                @"My Aaccount" \
}

it's short and elegant.

Then, you can use it like any other NSDictionary.

I hope this will help someone on the planet.

Best

MGY
  • 7,245
  • 5
  • 41
  • 74
3

NSDictionary objects can't be created at compile time. However, if you need a static object, you can create one. You can, for example, use the initialize method, like this:

static NSDictionary* letterValues;

+ (void)initialize
{
    if (self == [MyClass class]) {
        letterValues = @{
                         @"A": @1,
                         @"B": @4,
                         @"C": @4,
                         @"X": @8,
                         @"Y": @3,
                         @"Z": @10,
                         };
    }
}

The if statement is there to prevent multiple calls of your code in MyClass subclasses.

Adam
  • 26,549
  • 8
  • 62
  • 79
  • wouldn't `+ (void)load` be more appropriate? – dwery Jan 01 '15 at 14:51
  • @dwery It really depends on your use case. See [this question](http://stackoverflow.com/q/13326435/730701) to find out more about the difference between `load` and `initialize`. – Adam Jan 04 '15 at 21:49
2

If you support iOS 14 (and its year-aligned releases) or later, you can now do this, assuming your keys are NSStrings. This is because Xcode 13 supports Clang's -fobjc-constant-literals flag, which is on by default.

saagarjha
  • 2,221
  • 1
  • 20
  • 37