Objective-C Multiple Initialisers
Asked Answered
M

2

17

I have a simple question about creating multiple initialisers within an objective-c class. Basically I have a class that represents a single row in my database (users). I currently have an initialiser which initialises the class based upon the users UserID (which is also the primary key within the database), when passed the UserID the class will them connect to a webservice parse the results and return an object initialised to the corresponding row in the database.

Within this database are a number of unique fields (username and emailaddress), I would also like to be able to initialise my object based upon these values. But I am unsure of how to have more than one initialiser, everything I have read states that I am free to have multiple initialisers, as long as each calls the designated initialiser. If someone could help me out with this, that would be great.

My initialiser code is as follows:

- (id) initWithUserID:(NSInteger) candidate {
    self = [super init];
    if(self) {
        // Load User Data Here
        NSString *soapMessage = [NSString stringWithFormat:
                                 @"<?xml version=\"1.0\" encoding=\"utf-8\"?>\n"
                                 "<soap:Envelope xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\" xmlns:soap=\"http://schemas.xmlsoap.org/soap/envelope/\">\n"
                                 "<soap:Body>\n"
                                 "<GetByUserID xmlns=\"http://tempuri.org/\">\n"
                                 "<UserID>%d</UserID>\n"
                                 "</GetByUserID>\n"
                                 "</soap:Body>\n"
                                 "</soap:Envelope>\n", candidate
                                 ];
        NSLog(@"%@",soapMessage);

        // Build Our Request
        NSURL *url = [NSURL URLWithString:@"http://photoswapper.mick-walker.co.uk/UsersService.asmx"];
        NSMutableURLRequest *theRequest = [NSMutableURLRequest requestWithURL:url];
        NSString *msgLength = [NSString stringWithFormat:@"%d", [soapMessage length]];

        [theRequest addValue: @"text/xml; charset=utf-8" forHTTPHeaderField:@"Content-Type"];
        [theRequest addValue: @"http://tempuri.org/GetByUserID" forHTTPHeaderField:@"SOAPAction"];
        [theRequest addValue: msgLength forHTTPHeaderField:@"Content-Length"];
        [theRequest setHTTPMethod:@"POST"];
        [theRequest setHTTPBody: [soapMessage dataUsingEncoding:NSUTF8StringEncoding]];

        NSError *WSerror;
        NSURLResponse *WSresponse;

        webData = [NSURLConnection sendSynchronousRequest:theRequest returningResponse:&WSresponse error:&WSerror];

        xmlParser = [[NSXMLParser alloc] initWithData: webData];
        [xmlParser setDelegate: self];
        [xmlParser setShouldResolveExternalEntities: YES];
        [xmlParser parse];
    }
    return self;
}

Following from Laurent's comment, I have tried to implement my own solution, I would be grateful if you could inform me of any obvious gotcha's with this solution:

I am not totally sure I understand you're meaning, I have tried to implement my own solution. I would be grateful if you could let me know what you think:

- (id) init {
    self = [super init];
    if(self){
        // For simplicity I am going to assume that the 3 possible
        // initialation vectors are mutually exclusive.
        // i.e if userName is used, then userID and emailAddress
        // will always be nil
        if(self.userName != nil){
            // Initialise object based on username
        }
        if(self.emailAddress != nil){
            // Initialise object based on emailAddress
        }
        if(self.userID != 0){ // UserID is an NSInteger Type
            // Initialise object based on userID
        }
    }
    return self;
}
- (id) initWithUserID:(NSInteger) candidate {
    self.userID = candidate;
    return [self init];
}
- (id) initWithEmailAddress:(NSString *) candidate {
    self.emailAddress = candidate;
    return [self init];
}
- (id) initWithUserName:(NSString *) candidate {
    self.userName = candidate;
    return [self init];
}

Regards

Melosa answered 8/1, 2010 at 10:32 Comment(1)
WARNING - likely application failure: you need to initialise self using the designated initialiser BEFORE you set self.userID or similar. This is because 1) the object memory may be reallocated to a new location by the call to [super init] in [self init], losing your initialised variable and 2) userID may be overwritten by the call to [self init]. Do this: if (self = [self init]) { self.userID = candidate } return self;Paderewski
E
17

The designated initializer's definition is here. It is a strong requirement to ensure that your instances are consistent whatever the initializer you use. For a full reference, see the Objective-C Programming Guide: the initializer implementation is well documented.

Edit 2: Fix typo reported by @NSResponder

Edit:

I think calling init after setting the member is not reliable. Members may have weird values that will fail the test for initialization.

A better way to do it is to call the "init" method first (which will set default values for members) and then to set the member. This way, you have the same code structure for all your initializers:

- (id) init {
    self = [super init];
    if(self){
        self.userID = 0;
        self.userName = nil;
        self.emailAddress = nil;
    }
    return self;
}

- (id) initWithUserID:(NSInteger) candidate {
    self = [self init];
    if(self){
        self.userID = candidate;
    }
    return self;
}
Emee answered 8/1, 2010 at 10:48 Comment(5)
I have edited my original post, could you let me know what you think of this solution. Thanks very much MickMelosa
Can I ask a question? What is the designated initializer in the class? If the class is subclassed, init can't be the designated initializer which should be called by subclass's initializer because at that time userID, emailAddress, and userName are not assigned a meanful value yet.Fillmore
@yehnan: You are right. I have updated my answer to provide a more reliable solution, with a proper designated initializer.Emee
Laurent, you shouldn't send [super init] in the -initWithUserID: method, you should send [self init]. You don't want to miss whatever happens in the -init method.Amalea
I have fixed the broken links.Emee
A
9

The way I tend to do this kind of thing is for the designated initializer to be the method that takes the most parameters, and the simpler initializers just send the more detailed -init messages. For example:

   // simple initializer
- (id) init
  {
  return [self initWithWidth: 1.0];
  }

  // not so simple initializer
- (id) initWithWidth:(float) aWidth
  {
  return [self initWithWidth:aWidth andColor:nil];
  }

  // designated initializer.  This is the one that subclasses should override.
- (id) initWithWidth: (float) aWidth andColor: (NSColor *) aColor 
  {
  if (self = [super init])
   {
   self.width = aWidth;
   self.color = aColor ? aColor : [[self class] defaultColor];
   }
  return self;
  }
Amalea answered 8/1, 2010 at 14:33 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.