How to Return a Custom Objective-C Object from a Class Method? - objective-c

I have a set of Objective-C files in my app labeled Crop.h/Crop.m. Inside is a custom array of different vegetables from a video game - each vegetable has properties like a name, growing season, number of days to grow, etc.
I need to write a function which loops through this array and checks each Crop name value to see if it matches with my view title (self.title). Below is an example of two crop objects:
Crop *turnip = [[Crop alloc] init];
[turnip setName:#"Turnip"];
[turnip setSeason:#"Spring"];
[turnip setBagPrice:120];
[turnip setSaleValue:60];
[turnip setDaysToGrow:5];
[turnip setRenewableDays:0];
Crop *potato = [[Crop alloc] init];
[potato setName:#"Potato"];
[potato setSeason:#"Spring"];
[potato setBagPrice:150];
[potato setSaleValue:80];
[potato setDaysToGrow:8];
[potato setRenewableDays:0];
Then what I'm thinking is calling a new class function titled returnCropData: which takes the self.title as a parameter. I'm just not sure if this is my best method... I'd appreciate any suggestions from iOS devs.
Here's the simple class method I have so far - no data matching yet as I'm still trying to figure out which loop would be best. I'm also struggling to figure out the syntax to add an NSString parameter onto the function
+ (Crop *)returnCropData
{
Crop *turnip = [[Crop alloc] init];
[turnip setName:#"Turnip"];
[turnip setSeason:#"Spring"];
[turnip setBagPrice:120];
[turnip setSaleValue:60];
[turnip setDaysToGrow:5];
[turnip setRenewableDays:0];
// more crops here....
NSMutableArray *cropListing = [[NSMutableArray alloc] init];
[cropListing addObject:turnip];
[cropListing addObject:potato];
// add the other crops here...
return potato;
// return any Crop value for now
}

Apple's Objective-C docs recommend you use objectWithParams: grammar for class methods (constructors). So in your case, you should change your existing method name to
+ (Crop *)cropWithName:(NSString *)name
And call it with Crop *turnip = [Crop cropWithName:#"Turnip];
and then in your for loop you can check if the passed in name equals the name of your object with [name isEqualToString:turnip.name].
This will work, however it seems as though every time that method is called you're creating a ton of Crop objects - time intensive on your part, and memory intensive on the device. Instead, you should look into making a plist file that uses a dictionary to represent each kind of Crop, and then in your creation method you can use your passed in name to look up all the other information about the specified crop. Then you can just return one instance of Crop instead of instantiating a massive amount.
Here's a link to Apple documentation that explains plists:
https://developer.apple.com/library/ios/#documentation/Cocoa/Conceptual/PropertyLists/Introduction/Introduction.html
Specifically, in any objects that I want to pull from a plist I define the following method that takes values from a dictionary and sets an object's properties:
- (id)initWithDictionary:(NSDictionary *)dictionary
{
self = [super init];
[self setValuesForKeysWithDictionary:dictionary];
return self;
}
And then in a class where I want to access an array of data, I would create a synthesized #property and then define this method (in this case, I've got Song objects):
- (NSMutableArray *)songs
{
if (_songs == nil)
{
NSArray *songDicts = [NSArray arrayWithContentsOfFile:[[NSBundle mainBundle] pathForResource:#"ChillMix" ofType:#"plist"]];
_songs = [[NSMutableArray alloc] initWithCapacity:[songDicts count]];
// Fast enumeration //
for (NSDictionary *currDict in songDicts)
{
Song *song = [[Song alloc] initWithDictionary:currDict];
[_songs addObject:song];
}
}
return _songs;
}
Here is a sample project I've developed: https://github.com/Jlawr3nc3/MusicLibraryiOS
[NSArray arrayWithContentsOfFile:] should be your new best friend. It's awesome. If you can format your XML in plist format, you're down to one. single. line. to parse data. (PS normal XML is annoying as hell to parse on iOS so this is a huge time save if you can avoid straight up XML).
Also, I'm not sure what the use case would be for creating a crop object based on a view's title. Since you're already aware of the name of the crop before the view is instantiated, you could create a Crop object and set a Crop property of the view to the crop object you created, and then in viewDidLoad just do self.title = self.crop.name;. Finally, if you've got a UITableView of crop objects you shouldn't populate the table view with static text; instead populate it with objects you create from a plist, and then in didSelectRowAtIndexPath: you can do [self.arrayOfCrops objectAtIndex:indexPath.row] to get the object you tapped on, and pass it in with the view you load. Anyway, check out Apple's sample code for more info on plists and tableviews etc, 'cause they've got a lot of information on this exact stuff.

Related

Using pointers to adjust global objects in objective-c

Ok, so I am working with two sets of data that are extremely similar, and at the same time, these data sets are both global NSMutableArrays within the object.
data_set_one = [[NSMutableArray alloc] init];
data_set_two = [[NSMutableArray alloc] init];
Two new NSMutableArrays are loaded, which need to be added to the old, existing data. These Arrays are also global.
xml_dataset_one = [[NSMutableArray alloc] init];
xml_dataset_two = [[NSMutableArray alloc] init];
To reduce code duplication (and because these data sets are so similar) I wrote a void method within the class to handle the data combination process for both Arrays:
-(void)constructData:(NSMutableArray *)data fromDownloadArray:(NSMutableArray *)down withMatchSelector:(NSString *)sel_str
Now, I have a decent understanding of object oriented programming, so I was thinking that if I were to invoke the method with the global Arrays in the data like so...
[self constructData:data_set_one fromDownloadArray:xml_dataset_one withMatchSelector:#"id"];
Then the global NSMutableArrays (data_set_one) would reflect the changes that happen to "array" within the method. Sadly, this is not the case, data_set_one doesn't reflect the changes (ex: new objects within the Array) outside of the method.
Here is a code snippet of the problem
// data_set_one is empty
// xml_dataset_one has a few objects
[constructData:(NSMutableArray *)data_set_one fromDownloadArray:(NSMutableArray *)xml_dataset_one withMatchSelector:(NSString *)#"id"];
// data_set_one should now be xml_dataset_one, but when echoed to screen, it appears to remain empty
And here is the gist of the code for the method, any help is appreciated.
-(void)constructData:(NSMutableArray *)data fromDownloadArray:(NSMutableArray *)down withMatchSelector:(NSString *)sel_str {
if ([data count] == 0) {
data = down; // set data equal to downloaded data
} else if ([down count] == 0) {
// download yields no results, do nothing
} else {
// combine the two arrays here
}
}
This project is not ARC enabled.
Thanks for the help guys!
Rob
If I understood your problem, you are trying to pass your object as call-by-reference and hoping to work as in C++/C. But Obj-C, although similar but has some different way. You have to dereference the object by using ** (double pointer) as mostly seen in NSError case, which is very rare.
Second ways is: wrap method as a block. Then put the variable in the same lexical scope as the block, and denote it by __block storage type.
Third way can be accessing your object directly by making it an ivar/property or even an singleton.

Saving an NSArray of custom objects

I've created a subclass of UIImage (UIImageExtra) as I want to include extra properties and methods.
I have an array that contains instances of this custom class.However when I save the array, it appears the extra data in the UIImageExtra class is not saved.
UIImageExtra conforms to NSCoding, but neither initWithCoder or encodeWithCoder are called, as NSLog statements I've added aren't printed.
My method to save the array looks like this:
- (void)saveIllustrations {
if (_illustrations == nil) {
NSLog(#"Nil array");
return;
}
[self createDataPath];
//Serialize the data and write to disk
NSString *illustrationsArrayPath = [_docPath stringByAppendingPathComponent:kIllustrationsFile];
NSMutableData *data = [[NSMutableData alloc] init];
NSKeyedArchiver *archiver = [[NSKeyedArchiver alloc] initForWritingWithMutableData:data];
[archiver encodeObject:_illustrations forKey:kIllustrationDataKey];
[archiver finishEncoding];
[data writeToFile:illustrationsArrayPath atomically: YES];
}
And the UIImageExtra has the following delegate methods for saving:
#pragma mark - NSCoding
- (void)encodeWithCoder:(NSCoder *)aCoder {
NSLog(#"Encoding origin data!");
[super encodeWithCoder:aCoder];
[aCoder encodeObject:originData forKey:kOriginData];
}
- (id)initWithCoder:(NSCoder *)aDecoder {
if (self = [super initWithCoder:(NSCoder *) aDecoder]) {
NSLog(#"Decoding origin data");
self.originData = [aDecoder decodeObjectForKey:kOriginData];
}
return self;
}
My code to create the array in the first place looks like this (in case that offers any clues)
for (NSDictionary *illustrationDict in illustrationDicts) {
NSString *illustrationString = [illustrationDict objectForKey:#"Filename"];
NSNumber *xCoord = [illustrationDict objectForKey:#"xCoord"];
NSNumber *yCoord = [illustrationDict objectForKey:#"yCoord"];
UIImageExtra *illustration = (UIImageExtra *)[UIImage imageNamed:illustrationString];
//Scale the illustration to size it for different devices
UIImageExtra *scaledIllustration = [illustration adjustForResolution];
NSValue *originData = [NSValue valueWithCGPoint:CGPointMake([xCoord intValue], [yCoord intValue])];
[scaledIllustration setOriginData:originData];
[self.illustrations addObject:scaledIllustration];
}
Or am I just going about saving this data the wrong way? Many thanks.
Your code to initialize the array is not actually creating instances of your UIImageExtra subclass.
UIImageExtra *illustration = (UIImageExtra *)[UIImage imageNamed:illustrationString];
returns a UIImage. Casting it doesn't do what you were intending.
UIImageExtra *scaledIllustration = [illustration adjustForResolution];
is still just a UIImage.
One straightforward-but-verbose way to approach this would be to make UIImageExtra a wrapper around UIImage. The wrapper would have a class method for initializing from a UIImage:
+ (UIImageExtra)imageExtraWithUIImage:(UIImage *)image;
And then every UIImage method you want to call would have to forward to the wrapped UIImage instance-- also being careful to re-wrap the result of e.g. -adjustForResolution lest you again end up with an unwrapped UIImage instance.
A more Objective-C sophisticated approach would be to add the functionality you want in a Category on UIImage, and then use method swizzling to replace the NSCoding methods with your category implementations. The tricky part of this (apart from the required Objective-C runtime gymnastics) is where to store your "extra" data, since you can't add instance variables in a category. [The standard answer is to have a look-aside dictionary keyed by some suitable representation of the UIImage instance (like an NSValue containing its pointer value), but as you can imagine the bookkeeping can get complicated fast.]
Stepping back for a moment, my advice to a new Cocoa programmer would be: "Think of a simpler way. If what you are trying to do is this complicated, try something else." For example, write a simple ImageValue class that has an -image method and an -extraInfo method (and implements NSCoding, etc.), and store instances of that in your array.
You can't add objects to an NSArray after init. Use NSMutableArray, that might be the issue.

Where do I create global variables for an iOS app?

Here is my code:
I want to be able to create a global NSMutableArray that can store Budget* objects that can then be written to a .pList file... I'm only learning what pLists are, and I am a bit hazy about how to implement them...
Where am I going wrong here?
- (IBAction)btnCreateBudget:(id)sender
{
Budget *budget = [[Budget alloc] init];
budget.name = self.txtFldBudgetName.text;
budget.amount = [self.txtFldBudgetAmount.text intValue];
// Write the data to the pList
NSMutableArray *anArray = [[NSMutableArray alloc] init]; // I want this to be a global variable for the entire app. Where do I put this?
[anArray addObject:budget];
[anArray writeToFile:[self dataFilePath] atomically:YES];
/* As you can see, below is where I test the code. Unfortunately,
every time I run this, I get only 1 element in the array. I'm assuming
that this is because everytime the button is pressed, I create a brand new
NSMutableArray *anArray. I want that to be global for the entire app. */
int i = 0;
for (Budget * b in anArray)
{
i++;
}
NSLog(#"There are %d items in anArray",i);
}
-(NSString *) dataFilePath
{
NSArray *path = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSString *documentDirectory = [path objectAtIndex:0];
return [documentDirectory stringByAppendingPathComponent:#"BudgetData.plist"];
}
edit: I'd like to add that I am creating the anArray array so that it can be accessible by other views. I understand that this can be done with NSNotification? or Should I do this the appDelegate classes? The end goal is to have the anArray object populate a UITableView that is in a separate View.
Just put the declaration outside the method instead of inside it.
NSMutableArray *anArray = nil;
- (IBAction)btnCreateBudget:(id)sender
{
...
if ( anArray == nil )
anArray = [[NSMutableArray alloc] init];
...
}
If it's only used inside the one file, make it "static" instead to prevent name collisions with other files:
static NSMutableArray *anArray = nil;
If it's only used inside the one method, make it "static" and put it inside that method:
- (IBAction)btnCreateBudget:(id)sender
{
static NSMutableArray *anArray = nil;
...
if ( anArray == nil )
anArray = [[NSMutableArray alloc] init];
...
}
Note that people usually use some kind of naming convention for global variables, like "gArray", to easily differentiate them from local variables, instance variables, or method parameters.
Global variable is not necessary in this case. You can do something like this:
Read old data to mutable array (initWithContentsOfFile:).
Add new record to the array.
Save the array to same file.
But the second problem in your code is that if your Budget class is not a property list type (NSString, NSData, NSArray, or NSDictionary objects) writeToFile: will not save it sucessfully.
You need to make sure that your Budget class invokes NSCoder and then the NSCoder initWithCoder: and NSCoder decodeWithCoder: methods. Otherwise, writeToFile: will not work for you NSObject class.
But I digress. The answer to the original question should be the following.
In your .h file you need to do the following.
#interface WhateverClassName : UIViewController
{
NSMutableArray *anArray;
}
#property(nonatomic, retain) NSMutableArray *anArray;
#end
Then, you need to make sure you #synthesize the NSMutableArray so that you don't get any freaky warnings. This is done just after the #implementation line in your .m file.
Then, within the function that you want it to be allocated into memory, simply do the following.
anArray = [[NSMutableArray alloc] initWithObjects:nil];
This is now a global variable. It is global in the sense that it can be used from any function and is not limited to use in one function.
If you would like to have data accessible to the entire application or context ("global"), you can use a singleton. However, do this with care and make sure it is actually necessary and appropriate. I would suggest doing plenty of reading up on it prior to any implementation of a singleton. Carter Allen has a good basic implementation here.
According to "The end goal is to have the anArray object populate a UITableView that is in a separate View" you wouldn't need to write anything to a file, database or singleton. Just set the object. Like stated by Sebastien Peek.
If you wish for offline data storage, look into sqlite, json, plist , etc

rearranging table rows by dragging in Lion

I am having trouble using the new Lion functionality to rearrange rows in my app. I am using outlineView:pasteboardWriterForItem: to store the row indexes so that I can access them later when I validate/accept the drop. I create a new NSPasteboardItem to return, and am attempting to store the row number as so:
[pbItem setData: [NSKeyedArchiver archivedDataWithRootObject: [NSNumber numberWithInteger: [fTableView rowForItem: item]]]
forType: TABLE_VIEW_DATA_TYPE];
TABLE_VIEW_DATA_TYPE is a custom string I'm using to distinguish my custom data in the dragging pasteboard. I don't use it outside of dragging these rows.
When attempting the drag, I receive in Console: 'TableViewDataType' is not a valid UTI string. Cannot set data for an invalid UTI.
Of course I could use some of the built-in UTIs for pasteboards, but none of them apply (and using them causes the drag to accept drags other than the rows, which it shouldn't). Is there something I'm missing, like a way to define a custom UTI just for dragging (without making it a "real" UTI since I have no use for it outside of the internal dragging, so it shouldn't be public).
Thanks for any help!
I had similar requirements except I had a grid of objects that I wanted to rearrange by dragging selected objects to a new location. There are several ways of doing this, including creating a custom object and implementing the NSPasteboardWriting and NSPasteboardReading protocols, (and NSCoding protocols if you will be reading data as NSPasteboardReadingAsKeyedArchive), but this seems to be overkill for dragging of objects that remain internal to the application.
What I did involves using the NSPasteboardItem as a wrapper with a custom UTI type (it already implements the NSPasteboardWriting and NSPasteboardReading protocols). First declare a custom UTI type:
#define kUTIMyCustomType #“com.mycompany.MyApp.MyCustomType”
This needs to be defined in the ‘com.domain.MyApp’ format otherwise you will get errors of the form: “XXX is not a valid UTI string. Cannot set data for an invalid UTI.” Apple mentions this in their documentation.
Then you must register this custom UTI type in the view in which your dragging will occur. This can be done at runtime, and does not require any .plist additions. In your view's init method add the following:
[self registerForDraggedTypes:[NSArray arrayWithObjects:(NSString *)kUTIMyCustomType, nil]];
Now, make sure that the delegate is set for this view, and the delegate object implements the required NSDraggingSource and NSDraggingDestination protocol methods. This will allow you to avoid breaking the MVC design pattern, by allowing the designated controller object to handle placing the data on the pasteboard which will likely involve querying model data (i.e., indexes).
Specifically, for placing on the dragging pasteboard the indexes of objects to be moved when dragging begins as NSPasteboardItem wrappers of your index data:
- (void) draggingSession:(NSDraggingSession *)session willBeginAtPoint:(NSPoint)screenPoint
{
NSPasteboard * pboard = [NSPasteboard pasteboardWithName:NSDragPboard];
[pboard clearContents];
NSMutableArray * selectedIndexes = [NSMutableArray array];
// Add NSString indexes for dragged items to pasteboard in NSPasteboardItem wrappers.
for (MyModel * myModel in [self selectedObjects])
{
NSPasteboardItem * pasteboardItem = [[[NSPasteboardItem alloc] init] autorelease];
[pasteboardItem setString:[NSString stringWithFormat:#"%#", [myModel index]]
forType:kUTIMyCustomType];
[selectedIndexes addObject:pasteboardItem];
}
[pboard writeObjects:selectedIndexes];
}
And when the dragging operation completes, to read the dragged index NSPasteboardItem data:
- (BOOL) performDragOperation:(id <NSDraggingInfo>)sender
{
NSPasteboard * pasteboard = [sender draggingPasteboard];
// Check for custom NSPasteboardItem's which wrap our custom object indexes.
NSArray * classArray = [NSArray arrayWithObject:[NSPasteboardItem class]];
NSArray * items = [pasteboard readObjectsForClasses:classArray options:[NSDictionary dictionary]];
if (items == nil)
return NO;
// Convert array of NSPasteboardItem's with NSString index reps. to array of NSNumber indexes.
NSMutableArray * indexes = [NSMutableArray array];
for (NSPasteboardItem * item in items)
[indexes addObject:[NSNumber numberWithInteger:[[item stringForType:kUTIMyCustomType] integerValue]]];
//
// Handle dragged indexes…
//
return YES;
}
Another technique you can use is to just store the indices of the objects you're dragging in an instance variable on the side. Putting everything on the pasteboard isn't strictly necessary unless you're accepting items from another app or vice versa.
In awakeFromNib, register for the NSStringPboardType.
In …pasteboardWriterForRow, return [NSString string].
In …draggingSession:willBegin…, set your instance variable to the indices you want to track.
In validateDrop, return NSDragOperationNone if your instance variable is nil or the view is not yours.
In …draggingSession:ended…, nil out your instance variable.
Hope that helps… I'm using the technique for a table view, but it should be virtually identical for an outline view.
Instead of using a vanilla NSPasteboardItem, you should create a custom object that conforms to the NSPasteboardWriting protocol.
In your custom object, you can implement writableTypesForPasteboard: to return a list of custom UTIs that your pasteboard item supports. You then implement pasteboardPropertyListForType: to return a plist representation of your object for the appropriate custom UTI when the pasteboard asks for it.
You can create a plist from arbitrary data using the +propertyListWithData:options:format:error: method of NSPropertyListSerialization.
You would then override tableView:pasteboardWriterForRow: in your table view data source to return an instance of your custom object.

Issues declaring already existing NSMutableArray in new class

I have a class (DataImporter) which has the code to download an RSS feed. I also have a view and separate class (TableView) which displays the data in a UITableView and starts the parsing process, storing parsed information in an NSMutableArray (items) which is located in the (TableView) subclass.
Now I wish to add a UIMapView which displays the items in the (items) NSMutableArray. Herein lies the issue - I need to somehow get the data from the (items) NSMutableArray into the new (mapView) subclass which I'm struggling with - and I preferably don't want to have to create a new class to download the data again for the mapView class when it already is in the applications memory. Is there a way I can transfer the information from the NSMutableArray (items) class to the (mapView) class (i.e. how do I declare the NSMutableArray in the (mapView) class)?
Here's a overview of how the system works:
App opened> Data downloaded (using DataImporter class) when (TableView) viewDidLoad runs> Data stored in NSMutableArray accessible by the (TableView) class> And from here I need to access and declare the array from a new (mapView) class.
Any help greatly appreciated, thanks.
Code for viewDidLoad MapKit:
Data *data = nil;
NSString *ilocation = [data locations];
NSString *ilocation2 = #"New Zealand";
NSString *inewlString;
inewlString = [ilocation stringByAppendingString:ilocation2];
NSLog(#"inewlString=%#",inewlString);
if(forwardGeocoder == nil)
{
forwardGeocoder = [[BSForwardGeocoder alloc] initWithDelegate:self];
}
// Forward geocode!
[forwardGeocoder findLocation: inewlString];
Code for parsing data into original NSMutable Array:
- (void)beginParsing {
NSLog(#"Parsing has begun");
//self.navigationItem.rightBarButtonItem.enabled = NO;
// Allocate the array for song storage, or empty the results of previous parses
if (incidents == nil) {
NSLog(#"Grabbing array");
self.datas = [NSMutableArray array];
} else {
[datas removeAllObjects];
[self.tableView reloadData];
}
// Create the parser, set its delegate, and start it.
self.parser = [[DataImporter alloc] init];
parser.delegate = self;
[parser start];
}
Since you have a property self.datas you can get a reference to the data just by calling that from your UIMapView, or by assigning to some target property in your map view from whatever object controls both views:
mapView.someArrayProperty = tableView.datas
However, you might want to rethink your overall program structure. Keeping what is in effect model data inside views is not good practice and tends make things pretty spaghetti-like as you add more objects and functionality.