I want to be able to update two sets of identical buttons with one function. Also I don't want to update all the buttons, only some of them.
Can I have a function like this?:
-(void) updateFields{
updateButton1 : (Bool) x
updateButton2 : (Bool) y
updateButton3 : (Bool) z }
The implementation will look like this:
[button1_1 setEnabled:x];
[button1_2 setEnabled:x]; //called only if updateButton1 is given an argument
[button2_1 setEnabled:y];
etc...
What about passing an array of button and an array of boolean wrapped in a NSNumber?
- (void)updateButton:(NSArray *)buttons withArray:(NSArray *)enablers {
// buttons is an array of UIButton
// enablers is an array of NSNumber created from boolean value
// Security check
if(buttons.count != enabler.count) {
NSLog(#"Error: array have different dimensions");
return;
}
// Enable buttons
for(int i=0; i<buttons.count; i++) {
UIButton *button = (UIButton *)[buttons objectAtIndex:i];
BOOL enable = [[enablers objectAtIndex:i] boolValue]
[button setEnabled:enable];
}
}
This may not be possible with primitive data types unless you create objects from them and put them in NSArray or NSDictionary. Other option can be to create a custom object and pass that as argument.
- (void)selectButton:(SelectedButton *)iButton {
if (iButton.type = A) {
// Handle A
} else if (iButton.type = B) {
// Handle B
} else if (iButton.type = C) {
// Handle C
}
}
i think the syntax you're going for makes more sense as a C function
however note that in this example the parameters are NOT optional.
void updateButtons(BOOL btn1, BOOL btn2, BOOL btn3){
button1.enabled = btn1
button2.enabled = btn2
button3.enabled = btn3
}
It's possible to create an Objective-C method with a variable argument list, as Matt Gallagher explains in Variable argument lists in Cocoa. Variable argument lists are even used in the Foundation framework, e.g. +[NSArray arrayWithObjects:...].
That said, it's probably a lot less work to pass the list of buttons in your method as an array, particularly given the ease with which one can now create arrays using object literals:
[foo updateFields:#[button1, button2, button3]];
I'm a newbie in Objective C, used to write C. Anyway, I have a class called DataProcessing:
DataProcessing.m
...
- (BOOL)MyStringTweaker:(NSString *)strIn : (NSString *)strOut {
if(some_thing) {
strOut = [NSString stringWithFormat:#"I_am_tweaked_%#", strIn];
return true;
}
else
return false;
}
...
From the AppDelegate (OSX Application)
AppDelegate.m
...
NSString *tweaked;
DataProcessing *data_proc = [[DataProcessing alloc] init];
if([data_proc MyStringTweaker:#"tweak_me":tweaked])
NSLog([NSString stringWithFormat:#"Tweaked: %#", tweaked]);
else
NSLog(#"Tweaking failed...");
...
This doesn't work, *tweaked is NIL after the call to MyStringTweaker...
What am I missing?
Objective-C, like C, is pass-by-value only. You need to change your method signature to be:
- (BOOL)MyStringTweaker:(NSString *)strIn : (NSString **)strOut
and use:
*strOut = [NSString stringWithFormat:#"I_am_tweaked_%#", strIn];
to do the assignment.
Then, where you call it, you need to pass the address of the pointer you want to fill in:
[data_proc MyStringTweaker:#"tweak_me" :&tweaked]
A good explanation is in the comp.lang.c FAQ.
Editorial aside: Why not label the second argument? It looks weird to have it naked like that.
I'm trying to make a simple Cocoa application where a number is input into a textfield. When a number is typed into one textfield the other textfield will automatically update with another number (and vice versa). The textfield that I've dragged into the window is the "Text Field with Number Formatter" from interface builder. I have the textfields set as delegate which allows them to automatically update. However, the formatting does not seem to work. I can't input decimal numbers. Please help!!!
Header file here:
#import <Cocoa/Cocoa.h>
#interface AppDelegate : NSObject <NSApplicationDelegate> {
IBOutlet NSTextField *number1;
IBOutlet NSTextField *number2;
}
#property (assign) IBOutlet NSWindow *window;
#end
Implementation file here:
#import "AppDelegate.h"
#implementation AppDelegate
#synthesize window = _window;
-(void)controlTextDidChange:(NSNotification *) note {
NSTextField *changedField = [note object];
if (changedField == number1) {
float num1 = [number1 floatValue];
[number2 setFloatValue: (num1*2.0)];
}
if (changedField == number2) {
float num2 = [number2 floatValue];
[number1 setFloatValue: (num2/2.0)];
}
}
#end
This is a bit obscure, but what you are trying to do cannot really work but there is an alternative.
In your code you have:
float num1 = [number1 floatValue];
[number2 setFloatValue: (num1*2.0)];
The first line requests the current value of number1. As you have a number formatter attached to number1 it is called to format the text... and if you've entered, say, 123. at this point the formatted result is 123... and your decimal point is vanished. You'll notice you cannot enter commas (thousands separators) either.
So the fundamental problem is that after every character is typed your code is forcing it to be a valid number at that point.
The alternative is to only do your action when the user has entered a complete number. You can either write code yourself to analyze the input as each character is typed, or you can use the quick and easy method of waiting until the user hits and responding to the action.
To do the latter change your code to:
- (IBAction) textDidChange:(id)sender
{
if (sender == number1)
{
float num1 = [number1 floatValue];
[number2 setFloatValue: (num1*2.0)];
}
else if (sender == number2)
{
float num2 = [number2 floatValue];
[number1 setFloatValue: (num2/2.0)];
}
}
and connect it to the control as the "selector" and remove the "delegate" connection you already have.
This solution is less "dynamic" than what you tried, but it does work. To get the dynamic response back you'll need to write more involved code as stated above.
I know I'm very late to answer but I just ran into the exact same problem and found a nice workaround. Building upon #CRD's answer.
How I accomplished the problem is:
Store the formatter
Wipe the formatter from the textfield
read the value from the textfield
Reapply the formatter to the textfield
An example of how I did it is below:
-(void)controlTextDidChange:(NSNotification *)obj
{
if ([obj object])
{
NSTextField *field = [obj object];
NSNumberFormatter *formatter = [field formatter];
[field setFormatter:nil];
double data = [field doubleValue];
[field setFormatter:formatter];
// ...
// Do other meaningful stuff now
}
}
Not sure if it's the proper way to do it but it worked well for me.
A bit late to the party, but I just ran into this issue myself, and I think I found a concise solution that maintains dynamic updating, yet avoids the formatter kicking in too early, thus preventing the user from entering fractions:
- (void)controlTextDidChange:(NSNotification *)aNotification
{
NSTextView *fieldEditor = [aNotification.userInfo valueForKey:#"NSFieldEditor"];
CGFloat floatValue = fieldEditor.textStorage.string.floatValue;
// Getting the new value from the field editor avoids the formatter kicking in here.
// You can now use the value as you wish. Probably something like:
if (sender == number1)
{
[number2 setFloatValue: (floatValue*2.0)];
}
else if (sender == number2)
{
[number1 setFloatValue: (floatValue/2.0)];
}
}
(I typed this into the browser, so it may not be completely correct, but should be close enough. The essential parts are the first two lines: extract the field editor and extract the new value from it.
I am using IBOutletCollections to group several Instances of similar UI Elements. In particular I group a number of UIButtons (which are similar to buzzers in a quiz game) and a group of UILabels (which display the score). I want to make sure that the label directly over the button updates the score. I figured that it is easiest to access them by index. Unfortunately even if I add them in the same order, they do not always have the same indexes. Is there a way in Interface Builder to set the correct ordering.
EDIT: Several commenters have claimed that more recent versions of Xcode return IBOutletCollections in the order the connections are made. Others have claimed that this approach didn't work for them in storyboards. I haven't tested this myself, but if you're willing to rely on undocumented behavior, then you may find that the explicit sorting I've proposed below is no longer necessary.
Unfortunately there doesn't seem to be any way to control the order of an IBOutletCollection in IB, so you'll need to sort the array after it's been loaded based on some property of the views. You could sort the views based on their tag property, but manually setting tags in IB can be rather tedious.
Fortunately we tend to lay out our views in the order we want to access them, so it's often sufficient to sort the array based on x or y position like this:
- (void)viewDidLoad
{
[super viewDidLoad];
// Order the labels based on their y position
self.labelsArray = [self.labelsArray sortedArrayUsingComparator:^NSComparisonResult(UILabel *label1, UILabel *label2) {
CGFloat label1Top = CGRectGetMinY(label1.frame);
CGFloat label2Top = CGRectGetMinY(label2.frame);
return [#(label1Top) compare:#(label2Top)];
}];
}
I ran with cduhn's answer and made this NSArray category.
If now xcode really preserves the design-time order this code is not really needed, but if you find yourself having to create/recreate large collections in IB and don't want to worry about messing up this could help (at run time). Also a note: most likely the order in which the objects were added to the collection had something to do with the "Object ID" you find in the Identity Inspector tab, which can get sporadic as you edit the interface and introduce new objects to the collection at a later time.
.h
#interface NSArray (sortBy)
- (NSArray*) sortByObjectTag;
- (NSArray*) sortByUIViewOriginX;
- (NSArray*) sortByUIViewOriginY;
#end
.m
#implementation NSArray (sortBy)
- (NSArray*) sortByObjectTag
{
return [self sortedArrayUsingComparator:^NSComparisonResult(id objA, id objB){
return(
([objA tag] < [objB tag]) ? NSOrderedAscending :
([objA tag] > [objB tag]) ? NSOrderedDescending :
NSOrderedSame);
}];
}
- (NSArray*) sortByUIViewOriginX
{
return [self sortedArrayUsingComparator:^NSComparisonResult(id objA, id objB){
return(
([objA frame].origin.x < [objB frame].origin.x) ? NSOrderedAscending :
([objA frame].origin.x > [objB frame].origin.x) ? NSOrderedDescending :
NSOrderedSame);
}];
}
- (NSArray*) sortByUIViewOriginY
{
return [self sortedArrayUsingComparator:^NSComparisonResult(id objA, id objB){
return(
([objA frame].origin.y < [objB frame].origin.y) ? NSOrderedAscending :
([objA frame].origin.y > [objB frame].origin.y) ? NSOrderedDescending :
NSOrderedSame);
}];
}
#end
Then include the header file as you chose to name it and the code can be:
- (void)viewDidLoad
{
[super viewDidLoad];
// Order the labels based on their y position
self.labelsArray = [self.labelsArray sortByUIViewOriginY];
}
Not sure when this changed exactly, but as of Xcode 4.2 at least, this no longer seems to be a problem. IBOutletCollections now preserve the order in which the views were added in Interface Builder.
UPDATE:
I made a test project to verify that this is the case: IBOutletCollectionTest
Not as far as I am aware.
As a workaround, you could assign each of them a tag, sequentially. Have the buttons range 100, 101, 102, etc. and the labels 200, 201, 202, etc. Then add 100 to the button's tag to get its corresponding label's tag. You can then get the label by using viewForTag:.
Alternatively, you could group the corresponding objects into their own UIView, so you only have one button and one label per view.
I found that Xcode sorts the collection alphabetically using the ID of the connection.
If you open the version editor on your nib file you can easily edit the id's (making sure they are unique otherwise Xcode will crash).
<outletCollection property="characterKeys" destination="QFa-Hp-9dk" id="aaa-0g-pwu"/>
<outletCollection property="characterKeys" destination="ahU-9i-wYh" id="aab-EL-hVT"/>
<outletCollection property="characterKeys" destination="Kkl-0x-mFt" id="aac-0c-Ot1"/>
<outletCollection property="characterKeys" destination="Neo-PS-Fel" id="aad-bK-O6z"/>
<outletCollection property="characterKeys" destination="AYG-dm-klF" id="aae-Qq-bam"/>
<outletCollection property="characterKeys" destination="Blz-fZ-cMU" id="aaf-lU-g7V"/>
<outletCollection property="characterKeys" destination="JCi-Hs-8Cx" id="aag-zq-6hK"/>
<outletCollection property="characterKeys" destination="DzW-qz-gFo" id="aah-yJ-wbx"/>
It helps if you first order your object manually in the Document Outline of IB so they show up in sequence in the the xml code.
The extension proposed by #scott-gardner is great & solves problems such as a collection of [UIButtons] not showing in the expected order. The below code is simply updated for Swift 5. Thanks really goes to Scott for this!
extension Array where Element: UIView {
/**
Sorts an array of `UIView`s or subclasses by `tag`. For example, this is useful when working with `IBOutletCollection`s, whose order of elements can be changed when manipulating the view objects in Interface Builder. Just tag your views in Interface Builder and then call this method on your `IBOutletCollection`s in `viewDidLoad()`.
- author: Scott Gardner
- seealso:
* [Source on GitHub](bit dot ly/SortUIViewsInPlaceByTag)
*/
mutating func sortUIViewsInPlaceByTag() {
sort { (left: Element, right: Element) in
left.tag < right.tag
}
}
}
It seems very random how IBOutletCollection is ordered. Maybe I am not understanding Nick Lockwood's methodology correctly - but I as well made a new project, added a bunch of UILabels, and connected them to a collection in the order they were added to the view.
After logging, I got a random order. It was very frustrating.
My workaround was setting tags in IB and then sorting the collections like so:
[self setResultRow1:[self sortCollection: [self resultRow1]]];
Here, resultRow1 is an IBOutletCollection of about 7 labels, with tags set through IB. Here is the sort method:
-(NSArray *)sortCollection:(NSArray *)toSort {
NSArray *sortedArray;
sortedArray = [toSort sortedArrayUsingComparator:^NSComparisonResult(id a, id b) {
NSNumber *tag1 = [NSNumber numberWithInt:[(UILabel*)a tag]];
NSNumber *tag2 = [NSNumber numberWithInt:[(UILabel*)b tag]];
return [tag1 compare:tag2];
}];
return sortedArray;
}
Doing this, I can now access objects by using [resultRow1 objectAtIndex: i] or such. This saves overhead of having to iterate through and compare tags every time I need to access an element.
I needed this ordering for a collection of UITextField objects for setting where the "Next" button on the keyboard would lead to (field tabbing). This is going to be an international app so I wanted the language direction to be ambiguous.
.h
#import <Foundation/Foundation.h>
#interface NSArray (UIViewSort)
- (NSArray *)sortByUIViewOrigin;
#end
.m
#import "NSArray+UIViewSort.h"
#implementation NSArray (UIViewSort)
- (NSArray *)sortByUIViewOrigin {
NSLocaleLanguageDirection horizontalDirection = [NSLocale characterDirectionForLanguage:[[NSLocale currentLocale] objectForKey:NSLocaleLanguageCode]];
NSLocaleLanguageDirection verticalDirection = [NSLocale lineDirectionForLanguage:[[NSLocale currentLocale] objectForKey:NSLocaleLanguageCode]];
UIView *window = [[UIApplication sharedApplication] delegate].window;
return [self sortedArrayUsingComparator:^NSComparisonResult(id object1, id object2) {
CGPoint viewOrigin1 = [(UIView *)object1 convertPoint:((UIView *)object1).frame.origin toView:window];
CGPoint viewOrigin2 = [(UIView *)object2 convertPoint:((UIView *)object2).frame.origin toView:window];
if (viewOrigin1.y < viewOrigin2.y) {
return (verticalDirection == kCFLocaleLanguageDirectionLeftToRight) ? NSOrderedDescending : NSOrderedAscending;
}
else if (viewOrigin1.y > viewOrigin2.y) {
return (verticalDirection == kCFLocaleLanguageDirectionLeftToRight) ? NSOrderedAscending : NSOrderedDescending;
}
else if (viewOrigin1.x < viewOrigin2.x) {
return (horizontalDirection == kCFLocaleLanguageDirectionTopToBottom) ? NSOrderedDescending : NSOrderedAscending;
}
else if (viewOrigin1.x > viewOrigin2.x) {
return (horizontalDirection == kCFLocaleLanguageDirectionTopToBottom) ? NSOrderedAscending : NSOrderedDescending;
}
else return NSOrderedSame;
}];
}
#end
Usage (after layout)
- (void)viewDidAppear:(BOOL)animated {
_availableTextFields = [_availableTextFields sortByUIViewOrigin];
UITextField *previousField;
for (UITextField *field in _availableTextFields) {
if (previousField) {
previousField.nextTextField = field;
}
previousField = field;
}
}
Here's an extension I created on Array<UIView> to sort by tag, e.g., useful when working w/ IBOutletCollections.
extension Array where Element: UIView {
/**
Sorts an array of `UIView`s or subclasses by `tag`. For example, this is useful when working with `IBOutletCollection`s, whose order of elements can be changed when manipulating the view objects in Interface Builder. Just tag your views in Interface Builder and then call this method on your `IBOutletCollection`s in `viewDidLoad()`.
- author: Scott Gardner
- seealso:
* [Source on GitHub](http://bit.ly/SortUIViewsInPlaceByTag)
*/
mutating func sortUIViewsInPlaceByTag() {
sortInPlace { (left: Element, right: Element) in
left.tag < right.tag
}
}
}
I used the extension proposed by #scott-gardner to order Image Views in order to display counters using individual png images of dot-matrix digits.
It worked like a charm in Swift 5.
self.dayDigits.sortUIViewsInPlaceByTag()
func updateDayDigits(countString: String){
for i in 0...4 {
dayDigits[i].image = offDigitImage
}
let length = countString.count - 1
for i in 0...length {
let char = Array(countString)[length-i]
dayDigits[i].image = digitImages[char.wholeNumberValue!]
}
}
I'm doing some Objective-C programming that involves parsing an NSXmlDocument and populating an objects properties from the result.
First version looked like this:
if([elementName compare:#"companyName"] == 0)
[character setCorporationName:currentElementText];
else if([elementName compare:#"corporationID"] == 0)
[character setCorporationID:currentElementText];
else if([elementName compare:#"name"] == 0)
...
But I don't like the if-else-if-else pattern this produces. Looking at the switch statement I see that i can only handle ints, chars etc and not objects... so is there a better implementation pattern I'm not aware of?
BTW I did actually come up with a better solution for setting the object's properties, but I want to know specifically about the if-else vs switch pattern in Objective-C
You should take advantage of Key-Value Coding:
[character setValue:currentElementText forKey:elementName];
If the data is untrusted, you might want to check that the key is valid:
if (![validKeysCollection containsObject:elementName])
// Exception or error
I hope you'll all forgive me for going out on a limb here, but I would like to address the more general question of parsing XML documents in Cocoa without the need of if-else statements. The question as originally stated assigns the current element text to an instance variable of the character object. As jmah pointed out, this can be solved using key-value coding. However, in a more complex XML document this might not be possible. Consider for example the following.
<xmlroot>
<corporationID>
<stockSymbol>EXAM</stockSymbol>
<uuid>31337</uuid>
</corporationID>
<companyName>Example Inc.</companyName>
</xmlroot>
There are multiple approaches to dealing with this. Off of the top of my head, I can think of two using NSXMLDocument. The first uses NSXMLElement. It is fairly straightforward and does not involve the if-else issue at all. You simply get the root element and go through its named elements one by one.
NSXMLElement* root = [xmlDocument rootElement];
// Assuming that we only have one of each element.
[character setCorperationName:[[[root elementsForName:#"companyName"] objectAtIndex:0] stringValue]];
NSXMLElement* corperationId = [root elementsForName:#"corporationID"];
[character setCorperationStockSymbol:[[[corperationId elementsForName:#"stockSymbol"] objectAtIndex:0] stringValue]];
[character setCorperationUUID:[[[corperationId elementsForName:#"uuid"] objectAtIndex:0] stringValue]];
The next one uses the more general NSXMLNode, walks through the tree, and directly uses the if-else structure.
// The first line is the same as the last example, because NSXMLElement inherits from NSXMLNode
NSXMLNode* aNode = [xmlDocument rootElement];
while(aNode = [aNode nextNode]){
if([[aNode name] isEqualToString:#"companyName"]){
[character setCorperationName:[aNode stringValue]];
}else if([[aNode name] isEqualToString:#"corporationID"]){
NSXMLNode* correctParent = aNode;
while((aNode = [aNode nextNode]) == nil && [aNode parent != correctParent){
if([[aNode name] isEqualToString:#"stockSymbol"]){
[character setCorperationStockSymbol:[aNode stringValue]];
}else if([[aNode name] isEqualToString:#"uuid"]){
[character setCorperationUUID:[aNode stringValue]];
}
}
}
}
This is a good candidate for eliminating the if-else structure, but like the original problem, we can't simply use switch-case here. However, we can still eliminate if-else by using performSelector. The first step is to define the a method for each element.
- (NSNode*)parse_companyName:(NSNode*)aNode
{
[character setCorperationName:[aNode stringValue]];
return aNode;
}
- (NSNode*)parse_corporationID:(NSNode*)aNode
{
NSXMLNode* correctParent = aNode;
while((aNode = [aNode nextNode]) == nil && [aNode parent != correctParent){
[self invokeMethodForNode:aNode prefix:#"parse_corporationID_"];
}
return [aNode previousNode];
}
- (NSNode*)parse_corporationID_stockSymbol:(NSNode*)aNode
{
[character setCorperationStockSymbol:[aNode stringValue]];
return aNode;
}
- (NSNode*)parse_corporationID_uuid:(NSNode*)aNode
{
[character setCorperationUUID:[aNode stringValue]];
return aNode;
}
The magic happens in the invokeMethodForNode:prefix: method. We generate the selector based on the name of the element, and perform that selector with aNode as the only parameter. Presto bango, we've eliminated the need for an if-else statement. Here's the code for that method.
- (NSNode*)invokeMethodForNode:(NSNode*)aNode prefix:(NSString*)aPrefix
{
NSNode* ret = nil;
NSString* methodName = [NSString stringWithFormat:#"%#%#:", prefix, [aNode name]];
SEL selector = NSSelectorFromString(methodName);
if([self respondsToSelector:selector])
ret = [self performSelector:selector withObject:aNode];
return ret;
}
Now, instead of our larger if-else statement (the one that differentiated between companyName and corporationID), we can simply write one line of code
NSXMLNode* aNode = [xmlDocument rootElement];
while(aNode = [aNode nextNode]){
aNode = [self invokeMethodForNode:aNode prefix:#"parse_"];
}
Now I apologize if I got any of this wrong, it's been a while since I've written anything with NSXMLDocument, it's late at night and I didn't actually test this code. So if you see anything wrong, please leave a comment or edit this answer.
However, I believe I have just shown how properly-named selectors can be used in Cocoa to completely eliminate if-else statements in cases like this. There are a few gotchas and corner cases. The performSelector: family of methods only takes 0, 1, or 2 argument methods whose arguments and return types are objects, so if the types of the arguments and return type are not objects, or if there are more than two arguments, then you would have to use an NSInvocation to invoke it. You have to make sure that the method names you generate aren't going to call other methods, especially if the target of the call is another object, and this particular method naming scheme won't work on elements with non-alphanumeric characters. You could get around that by escaping the XML element names in your method names somehow, or by building an NSDictionary using the method names as the keys and the selectors as the values. This can get pretty memory intensive and end up taking a longer time. performSelector dispatch like I described is pretty fast. For very large if-else statements, this method may even be faster than an if-else statement.
If you want to use as little code as possible, and your element names and setters are all named so that if elementName is #"foo" then setter is setFoo:, you could do something like:
SEL selector = NSSelectorFromString([NSString stringWithFormat:#"set%#:", [elementName capitalizedString]]);
[character performSelector:selector withObject:currentElementText];
or possibly even:
[character setValue:currentElementText forKey:elementName]; // KVC-style
Though these will of course be a bit slower than using a bunch of if statements.
[Edit: The second option was already mentioned by someone; oops!]
Dare I suggest using a macro?
#define TEST( _name, _method ) \
if ([elementName isEqualToString:# _name] ) \
[character _method:currentElementText]; else
#define ENDTEST { /* empty */ }
TEST( "companyName", setCorporationName )
TEST( "setCorporationID", setCorporationID )
TEST( "name", setName )
:
:
ENDTEST
One way I've done this with NSStrings is by using an NSDictionary and enums. It may not be the most elegant, but I think it makes the code a little more readable. The following pseudocode is extracted from one of my projects:
typedef enum { UNKNOWNRESIDUE, DEOXYADENINE, DEOXYCYTOSINE, DEOXYGUANINE, DEOXYTHYMINE } SLSResidueType;
static NSDictionary *pdbResidueLookupTable;
...
if (pdbResidueLookupTable == nil)
{
pdbResidueLookupTable = [[NSDictionary alloc] initWithObjectsAndKeys:
[NSNumber numberWithInteger:DEOXYADENINE], #"DA",
[NSNumber numberWithInteger:DEOXYCYTOSINE], #"DC",
[NSNumber numberWithInteger:DEOXYGUANINE], #"DG",
[NSNumber numberWithInteger:DEOXYTHYMINE], #"DT",
nil];
}
SLSResidueType residueIdentifier = [[pdbResidueLookupTable objectForKey:residueType] intValue];
switch (residueIdentifier)
{
case DEOXYADENINE: do something; break;
case DEOXYCYTOSINE: do something; break;
case DEOXYGUANINE: do something; break;
case DEOXYTHYMINE: do something; break;
}
The if-else implementation you have is the right way to do this, since switch won't work with objects. Apart from maybe being a bit harder to read (which is subjective), there is no real downside in using if-else statements this way.
Although there's not necessarily a better way to do something like that for one time use, why use "compare" when you can use "isEqualToString"? That would seem to be more performant since the comparison would halt at the first non-matching character, rather than going through the whole thing to calculate a valid comparison result (though come to think of it the comparison might be clear at the same point) - also though it would look a little cleaner because that call returns a BOOL.
if([elementName isEqualToString:#"companyName"] )
[character setCorporationName:currentElementText];
else if([elementName isEqualToString:#"corporationID"] )
[character setCorporationID:currentElementText];
else if([elementName isEqualToString:#"name"] )
There is actually a fairly simple way to deal with cascading if-else statements in a language like Objective-C. Yes, you can use subclassing and overriding, creating a group of subclasses that implement the same method differently, invoking the correct implementation at runtime using a common message. This works well if you wish to choose one of a few implementations, but it can result in a needless proliferation of subclasses if you have many small, slightly different implementations like you tend to have in long if-else or switch statements.
Instead, factor out the body of each if/else-if clause into its own method, all in the same class. Name the messages that invoke them in a similar fashion. Now create an NSArray containing the selectors of those messages (obtained using #selector()). Coerce the string you were testing in the conditionals into a selector using NSSelectorFromString() (you may need to concatenate additional words or colons to it first depending on how you named those messages, and whether or not they take arguments). Now have self perform the selector using performSelector:.
This approach has the downside that it can clutter-up the class with many new messages, but it's probably better to clutter-up a single class than the entire class hierarchy with new subclasses.
Posting this as a response to Wevah's answer above -- I would've edited, but I don't have high enough reputation yet:
unfortunately the first method breaks for fields with more than one word in them -- like xPosition. capitalizedString will convert that to Xposition, which when combined with the format give you setXposition: . Definitely not what was wanted here. Here is what I'm using in my code:
NSString *capName = [elementName stringByReplacingCharactersInRange:NSMakeRange(0, 1) withString:[[elementName substringToIndex:1] uppercaseString]];
SEL selector = NSSelectorFromString([NSString stringWithFormat:#"set%#:", capName]);
Not as pretty as the first method, but it works.
I have come up with a solution that uses blocks to create a switch-like structure for objects. There it goes:
BOOL switch_object(id aObject, ...)
{
va_list args;
va_start(args, aObject);
id value = nil;
BOOL matchFound = NO;
while ( (value = va_arg(args,id)) )
{
void (^block)(void) = va_arg(args,id);
if ( [aObject isEqual:value] )
{
block();
matchFound = YES;
break;
}
}
va_end(args);
return matchFound;
}
As you can see, this is an oldschool C function with variable argument list. I pass the object to be tested in the first argument, followed by the case_value-case_block pairs. (Recall that Objective-C blocks are just objects.) The while loop keeps extracting these pairs until the object value is matched or there are no cases left (see notes below).
Usage:
NSString* str = #"stuff";
switch_object(str,
#"blah", ^{
NSLog(#"blah");
},
#"foobar", ^{
NSLog(#"foobar");
},
#"stuff", ^{
NSLog(#"stuff");
},
#"poing", ^{
NSLog(#"poing");
},
nil); // <-- sentinel
// will print "stuff"
Notes:
this is a first approximation without any error checking
the fact that the case handlers are blocks, requires additional care when it comes to visibility, scope and memory management of variables referenced from within
if you forget the sentinel, you are doomed :P
you can use the boolean return value to trigger a "default" case when none of the cases have been matched
The most common refactoring suggested for eliminating if-else or switch statements is introducing polymorphism (see http://www.refactoring.com/catalog/replaceConditionalWithPolymorphism.html). Eliminating such conditionals is most important when they are duplicated. In the case of XML parsing like your sample you are essentially moving the data to a more natural structure so that you won't have to duplicate the conditional elsewhere. In this case the if-else or switch statement is probably good enough.
In this case, I'm not sure if you can easily refactor the class to introduce polymorphism as Bradley suggests, since it's a Cocoa-native class. Instead, the Objective-C way to do it is to use a class category to add an elementNameCode method to NSSting:
typedef enum {
companyName = 0,
companyID,
...,
Unknown
} ElementCode;
#interface NSString (ElementNameCodeAdditions)
- (ElementCode)elementNameCode;
#end
#implementation NSString (ElementNameCodeAdditions)
- (ElementCode)elementNameCode {
if([self compare:#"companyName"]==0) {
return companyName;
} else if([self compare:#"companyID"]==0) {
return companyID;
} ... {
}
return Unknown;
}
#end
In your code, you could now use a switch on [elementName elementNameCode] (and gain the associated compiler warnings if you forget to test for one of the enum members etc.).
As Bradley points out, this may not be worth it if the logic is only used in one place.
What we've done in our projects where we need to so this sort of thing over and over, is to set up a static CFDictionary mapping the strings/objects to check against to a simple integer value. It leads to code that looks like this:
static CFDictionaryRef map = NULL;
int count = 3;
const void *keys[count] = { #"key1", #"key2", #"key3" };
const void *values[count] = { (uintptr_t)1, (uintptr_t)2, (uintptr_t)3 };
if (map == NULL)
map = CFDictionaryCreate(NULL,keys,values,count,&kCFTypeDictionaryKeyCallBacks,NULL);
switch((uintptr_t)CFDictionaryGetValue(map,[node name]))
{
case 1:
// do something
break;
case 2:
// do something else
break;
case 3:
// this other thing too
break;
}
If you're targeting Leopard only, you could use an NSMapTable instead of a CFDictionary.
Similar to Lvsti I am using blocks to perform a switching pattern on objects.
I wrote a very simple filter block based chain, that takes n filter blocks and performs each filter on the object.
Each filter can alter the object, but must return it. No matter what.
NSObject+Functional.h
#import <Foundation/Foundation.h>
typedef id(^FilterBlock)(id element, NSUInteger idx, BOOL *stop);
#interface NSObject (Functional)
-(id)processByPerformingFilterBlocks:(NSArray *)filterBlocks;
#end
NSObject+Functional.m
#implementation NSObject (Functional)
-(id)processByPerformingFilterBlocks:(NSArray *)filterBlocks
{
__block id blockSelf = self;
[filterBlocks enumerateObjectsUsingBlock:^( id (^block)(id,NSUInteger idx, BOOL*) , NSUInteger idx, BOOL *stop) {
blockSelf = block(blockSelf, idx, stop);
}];
return blockSelf;
}
#end
Now we can set up n FilterBlocks to test for the different cases.
FilterBlock caseYES = ^id(id element, NSUInteger idx, BOOL *breakAfter){
if ([element isEqualToString:#"YES"]) {
NSLog(#"You did it");
*breakAfter = YES;
}
return element;
};
FilterBlock caseNO = ^id(id element, NSUInteger idx, BOOL *breakAfter){
if ([element isEqualToString:#"NO"] ) {
NSLog(#"Nope");
*breakAfter = YES;
}
return element;
};
Now we stick those block we want to test as a filter chain in an array:
NSArray *filters = #[caseYES, caseNO];
and can perform it on an object
id obj1 = #"YES";
id obj2 = #"NO";
[obj1 processByPerformingFilterBlocks:filters];
[obj2 processByPerformingFilterBlocks:filters];
This approach can be used for switching but also for any (conditional) filter chain application, as the blocks can edit the element and pass it on.