50

In a Master-Detail app I'd like to display a TableView with 5 sections titled:

  1. Your Move
  2. Their Move
  3. Won Games
  4. Lost Games
  5. Options

So I create a blank Master-Detail app in Xcode 5.0.2 and then in its MasterViewController.m (which is a UITableViewController) I'm trying to implement the method:

- (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section {
    return _titles[section];
}

My question is however how to init the NSArray _titles?

I'm trying in the MasterViewController.m:

#import "MasterViewController.h"
#import "DetailViewController.h"

static NSArray *_titles_1 = @[
    @"Your Move",
    @"Their Move",
    @"Won Games",
    @"Lost Games",
    @"Options"
];

@interface MasterViewController () {
    NSMutableArray *_games;

    NSArray *_titles_2 = @[
                         @"Your Move",
                         @"Their Move",
                         @"Won Games",
                         @"Lost Games",
                         @"Options"
    ];
}
@end

@implementation MasterViewController

- (void)awakeFromNib
{
    if ([[UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPad) {
        self.clearsSelectionOnViewWillAppear = NO;
        self.preferredContentSize = CGSizeMake(320.0, 600.0);
    }
    [super awakeFromNib];
}

- (void)viewDidLoad
{
     ....
}

but both tries above give me syntax errors:

enter image description here

UPDATE:

To my surprise there are many suggestions for this simple question, but as an iOS/Objective-C newbie I'm not sure, which solution is most appropriate.

dispatch_once - isn't it a runtime operation to execute something once in a multi-threaded app? Isn't it overkill here? I was expecting a compile-time solution for initiating a const array...

viewDidLoad - when my app changes between background and foreground, wouldn't it unnecessary initiate my const array again and again?

Shouldn't I better set the NSArray in awakeFromNib (since I use stroyboard scenes for all my ViewControllers)? Or maybe in initSomething (is the correct method initWithStyle?)

Alexander Farber
  • 21,519
  • 75
  • 241
  • 416
  • Init it in `viewDidLoad`. – duci9y Dec 12 '13 at 13:19
  • You should change the signature to `static NSArray* const` instead. There are many ways to solve when to initialize the array as seen in the answer below. I like lazy inits, but it isn't really suited for this use case. – Hugo Tunius Dec 12 '13 at 13:26
  • What you're looking for is a (choke!) singleton-style initialization. It does not need to be in its own class, however, it can be a part of this class. – Hot Licks Dec 12 '13 at 13:39
  • @AlexanderFarber iOS apps are multi-threaded, and class methods can be called concurrently by multiple threads, so it's safer to use `dispatch_once` or some form of locking. I can tell you from first-hand experience that otherwise the potential for crasher bugs is very real. – jlehr Dec 12 '13 at 22:08

9 Answers9

82

Write a class method that returns the array.

+ (NSArray *)titles
{
    static NSArray *_titles;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        _titles = @[@"Your Move",
                    @"Their Move",
                    @"Won Games",
                    @"Lost Games",
                    @"Options"];
    });
    return _titles;
}

Then you can access it wherever needed like so:

NSArray *titles = [[self class] titles];
jlehr
  • 15,557
  • 5
  • 43
  • 45
  • 2
    The user is asking WHERE to create this, not how. You haven't provided any help, the users answer shows him trying to instantiate an object in an interface – Simon McLoughlin Dec 12 '13 at 13:29
  • 1
    @SimonMcLoughlin Quoting the OP, "My question is however **how** to init the NSArray _titles?" [emphasis added] – jlehr Dec 12 '13 at 13:30
  • 1
    @jlehr and if you read the rest of the question you will see he is getting syntax warnings due to the above mentioned issue. The user is asking for a basic understanding of how to instantiate an object in the flow of a viewController object. Fair point your solution might be the best way to create it, but the user is quite clearly looking for info as to WHERE to create it – Simon McLoughlin Dec 12 '13 at 13:33
  • Thanks! This seems to be a runtime solution (instead of the compile-time that I was expecting) - is it the best choice for iOS? – Alexander Farber Dec 12 '13 at 17:57
  • 3
    Objects can't be allocated at compile time, so this is typically what we do. Note that `dispatch_once` is a Grand Central Dispatch function that ensures the code in the provided block is only run once in the lifetime of the app. – jlehr Dec 12 '13 at 22:03
  • 3
    You can use the fully qualified class name to call the static method. Instead of [[self class] titles] you can just call [MyClassName titles]; – Brody Robertson Oct 29 '14 at 02:06
  • @BrodyRobertson Targeting a specific instead of obtaining a reference to the class dynamically class breaks inheritance. Also, not sure what you mean by 'fully qualified' class name -- Objective-C doesn't provide namespaces for classes. Did you have something else in mind? – jlehr Oct 30 '14 at 14:50
  • 1
    @jlehr a fully qualified name is an unambiguous name that specifies which object, function, or variable a call refers to without regard to the context of the call. I don't understand what you mean when you say calling a static method via it's qualified classname breaks inheritance. – Brody Robertson Nov 04 '14 at 15:41
  • Objective-C class methods are not static -- they're dynamically dispatched exactly the same way instance methods are, are inherited, and can be overridden in subclasses. – jlehr Apr 24 '18 at 13:58
12

You can init it in class method +initialize

static NSArray *_titles_1;

@implementation MasterViewController
+ (void)initialize {
    _titles_1 = @[
        @"Your Move",
        @"Their Move",
        @"Won Games",
        @"Lost Games",
        @"Options"
    ];
}
@end
Emmanuel
  • 2,897
  • 1
  • 14
  • 15
  • Thanks, but I've search and it seems (surprisingly!) that this method can be called more than once for a class - when an inheriting class does not implement own *+initialize* method. – Alexander Farber Dec 13 '13 at 12:36
  • 1
    @AlexanderFarber Then check that _titles_1 is nil or for the current class in `+initialize` – Emmanuel Dec 13 '13 at 12:51
  • If I check for `nil` - can't it happen that the `+initialize` method runs once, but for a wrong class - for the inheriting class? – Alexander Farber Dec 13 '13 at 13:10
  • +initialize runs once for the class itself, and once for every subclass that doesn't override it. That's why you compare "if (self == [MyIntendedClass class])" first. The check for nil makes it necessary to use synchronisation because two subclasses could be initialised from different threads. – gnasher729 Feb 17 '14 at 14:24
  • 1
    @gnasher729 `+initialize` is thread safe, according Apple documentation : `The runtime sends the initialize message to classes in a thread-safe manner.` – Emmanuel Feb 17 '14 at 15:03
8

You should declare your static array above @implementation.

static NSArray *titles_1 = nil;

@implementation ...

And define it in init or awakeFromNib or any other method like viewDidLoad, applicationDidFinishLaunching wherever you want.

- (void)initMethod{ //change the method name accordingly

    if (titles_1 == nil){
        [self setTitles_1:@[ @"Your Move", @"Their Move", @"Won Games", @"Lost Games", @"Options" ]];
    }
}
Anoop Vaidya
  • 46,283
  • 15
  • 111
  • 140
7

You can also do something like this:

static NSString * const strings[] = {
        [0] = @"string_1",
        [1] = @"string_2",
        [2] = @"string_3",
        [3] = @"string_4",
        // ...
    };

It's not an NSArray but you can access NSStrings like this strings[n]

Pablo A.
  • 2,042
  • 1
  • 17
  • 27
  • Sometimes, most always the simplest solution is not only the most effective, but the best... Being relatively new to Obj-C (7 years now) I'm still put off by the syntax, but I was pretty sure, a simple static array didn't need to involve 25 lines of code.. My C/C++ background told me this would probably work... NSString *dayNamesArray[7] = {@"blah",@"blah",@"blah",@"blah",@"blah",@"blah",@"and blah"}; I give you credit for the most common sense solution... Thanks! –  May 07 '17 at 12:09
6

I wonder, if the following would be a good way (answering my own question):

#import "MasterViewController.h"
#import "DetailViewController.h"

static const NSArray *_titles;

@interface MasterViewController () {
    NSMutableArray *_objects;
    NSMutableArray *_yourMove;
    NSMutableArray *_theirMove;
    NSMutableArray *_wonGames;
    NSMutableArray *_lostGames;
    NSMutableArray *_options;
}
@end

@implementation MasterViewController

+ (void)initialize
{
    // do not run for derived classes
    if (self != [MasterViewController class])
        return;

    _titles = @[
        @"Your Move",
        @"Their Move",
        @"Won Games",
        @"Lost Games",
        @"Options"
    ];
}

This way the const NSArray is initalized once and right before I need it (in the MasterViewController class). And the self check prevents this method from running again - when some inheriting class does not implement its own +initialize method.

Alexander Farber
  • 21,519
  • 75
  • 241
  • 416
2

You can't instantiate objects in this way, you can only declare them in interfaces. Do the following:

static NSArray *_titles_2;

@interface MasterViewController () {
    NSMutableArray *_games;
}
@end

@implementation MasterViewController
-(void)viewDidLoad()
{
     _titles_2 = @[
                         @"Your Move",
                         @"Their Move",
                         @"Won Games",
                         @"Lost Games",
                         @"Options"
    ];
}
@end
  • or likewise you could use the viewcontroller init method instead of viewDidLoad
Simon McLoughlin
  • 8,293
  • 5
  • 32
  • 56
  • That would mean every instance would have its own copy of the array. I don't think that's what the OP was looking for. – jlehr Dec 12 '13 at 13:28
  • fair enough, the point still stands that he's trying to instantiate the object in the interface, this would remove his xcode complier warnings, ill edit with a static array – Simon McLoughlin Dec 12 '13 at 13:30
  • If you look carefully, you'll see that the OP posted two different failed attempts, the first of which was an attempt to initialize a global constant with an `NSArray`. – jlehr Dec 12 '13 at 13:32
  • @jlehr - How many instances of this view controller do you think there are going to be? – Hot Licks Dec 12 '13 at 13:41
  • @HotLicks no that was a valid point, the user did clearly define a static array. The user more than likely had a reason for going out of there way to do that – Simon McLoughlin Dec 12 '13 at 13:45
  • @SimonMcLoughlin - Can you say "premature optimization"? – Hot Licks Dec 12 '13 at 16:23
  • If I use *viewDidLoad*, then the *_titles* will be assigned again and again, when I press Home button to send the app to background and then bring it to foreground? – Alexander Farber Dec 12 '13 at 18:21
2

dispatch_once works. It works in complicated cases, and it works in simple cases. So to be consistent, the best thing is to use it in all cases. That makes it for example a lot easier when you replace all your constant strings with calls to NSLocalizedString (). No code change needed.

Doing things in + (void)initialize isn't really wrong, but I have had situations where I first wanted to configure a class before really using it, and initialize is of course called just before any possible configuration method would start executing. And there are situations where you don't really have a class context. dispatch_once will always work.

gnasher729
  • 51,477
  • 5
  • 75
  • 98
0

Why don't use :

+(NSString*)titleForSection:(NSInteger)section {
    return (@[ @"title1", @"title2", @"title3" ])[section];
}
HardikS
  • 656
  • 3
  • 10
-1

I prefer to use Objective-C++,change filename from xxx.m to xxx.mm,and the initialize of NSArray will happen at runtime

no dispatch_once,no class method,just write it in one line:

static NSArray *const a = @[@"a",@"b",@"c"];
rpstw
  • 1,582
  • 14
  • 16