I'm trying to load a simple plist file (with an array at the root) into a UITableView (in the first view of an XCode 4.2 tabbed app). I've done this before in other (XCode 3) projects but for some reason it seems my lazy initializer for the array is never getting called.
.h file:
#import <UIKit/UIKit.h>
#interface NailPolishFirstViewController : UIViewController {
NSMutableArray *myCollection;
}
#property(nonatomic, retain) NSMutableArray *myCollection;
#end
.m file (relevant parts)
#import "NailPolishFirstViewController.h"
#implementation NailPolishFirstViewController
#synthesize myCollection;
// ...
- (NSMutableArray *) myCollection {
if (myCollection == nil) {
NSString *path = [[NSBundle mainBundle] bundlePath];
NSString *finalPath = [path stringByAppendingPathComponent:#"database.plist"];
self.myCollection = [NSMutableArray arrayWithContentsOfFile:finalPath];
NSLog(#"Collection size: %#", [self.myCollection count]);
}
return myCollection;
}
// ...
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
{
return 1;
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
NSLog(#"Getting rows ... %#", [myCollection count]);
return [myCollection count];
}
// ...
The xib file for this controller has a UITableView attached and the dataSource and delegate are set to File's Owner.
When I build and run, the numberOfRowsInSection is logging "Getting rows ... (null)" but the log in my lazy initializer for myCollection never shows. Why isn't it ever getting called?
You aren't going through the accessor. Using [myCollection count] is accessing the ivar directly, which will be nil. If you're going to use lazy loading you have to always go via self.myCollection otherwise it will never call your accessor, and never populate the records.
Since myCollection is a declared property, it needs to reference its accessor. Try calling it as self.myCollection.
For instance
- (NSMutableArray *) myCollection {
if (myCollection == nil) {
NSString *path = [[NSBundle mainBundle] bundlePath];
NSString *finalPath = [path stringByAppendingPathComponent:#"database.plist"];
myCollection = [NSMutableArray arrayWithContentsOfFile:finalPath];
NSLog(#"Collection size: %#", [self.myCollection count]);
}
return myCollection;
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
NSLog(#"Getting rows ... %#", [self.myCollection count]);
return [self.myCollection count];
}
Related
I trying to learn how to save array of objects using NSKeyedArchiver and I coded a small application to do that and I logged to see if the array was saved but everytime I get 0 for array count and here is the code.
ViewController.h
#interface ViewController : UIViewController
{
IBOutlet UITextField *text;
IBOutlet UITextField *textName;
IBOutlet UITextField *textAge;
IBOutlet UILabel *name;
IBOutlet UILabel *age;
BOOL flag;
BOOL choice;
NSString *documentDirectory;
NSMutableArray *anArray;
Person *p;
NSData *data;
}
-(BOOL) dataFilePath;
-(IBAction)readPlist;
-(IBAction) writePlist;
#property (strong,nonatomic)IBOutlet UITextField *text;
#property (strong,nonatomic)IBOutlet UITextField *textName;
#property (strong,nonatomic)IBOutlet UITextField *textAge;
#property (strong,nonatomic)IBOutlet UILabel *name;
#property (strong,nonatomic)IBOutlet UILabel *age;
#property (strong,nonatomic)NSString *documentDirectory;
#property (strong,nonatomic)NSMutableArray *anArray;
#end
ViewController.m
#interface ViewController ()
#end
#implementation ViewController
#synthesize text,documentDirectory,textAge,textName,name,age,anArray;
- (void)viewDidLoad
{
[super viewDidLoad];
// checking if the file was created and show a message if its created or not.
if ([self dataFilePath]) {
NSLog(#"File Created !");
} else {
NSLog(#"File Not Created !");
}
NSLog(#"File location : %#",documentDirectory);
choice = YES;
// Do any additional setup after loading the view, typically from a nib.
}
- (void)didReceiveMemoryWarning
{
[super didReceiveMemoryWarning];
// Dispose of any resources that can be recreated.
}
-(BOOL) dataFilePath
{
NSArray *path = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
documentDirectory = [path objectAtIndex:0];
documentDirectory = [documentDirectory stringByAppendingPathComponent:#"MilmersÂData.dat"];
return TRUE;
}
- (IBAction)writePlist
{
p.name = textName.text;
p.age = [textAge.text intValue];
[anArray addObject:p];
for (int i=0; i<[anArray count]+1; i++) {
Person *pp = [[Person alloc]init];
pp=[anArray objectAtIndex:i];
NSLog(#"Name: %#",pp.name); // checking the names in pp object but getting null
}
data = [NSKeyedArchiver archivedDataWithRootObject:anArray];
[data writeToFile:documentDirectory options:NSDataWritingAtomic error:nil];
NSLog(#"Array length: %d",[anArray count]); //Always got array count zero.
}
-(IBAction)readPlist
{
NSString *filePath = documentDirectory;
NSMutableArray *array = [NSKeyedUnarchiver unarchiveObjectWithFile:filePath];
NSLog(#"The array is: %#",array); // it shows that there is nothing in the array
}
#end
I wrote the class for writing .plist files originally but I knew later that I cant store objects in .plist file so I tried so that with archive, thats why the method name have plist in it.
Thank you in advance
Looks like you aren't ever creating an instance of p to add to the array. Try:
Person *p = [[Person alloc] init];
p.name = textName.text;
p.age = [textAge.text intValue];
[anArray addObject:p];
your index limit was also wrong in this loop
for (int i=0; i<[anArray count]; i++) {
NSLog(#"Name: %#", [[anArray objectAtIndex:i] name]);
}
you should really have been seeing a couple of different crashes...
Try adding this in viewDidLoad
[[NSFileManager defaultManager] createFileAtPath:documentDirectory contents:nil error:nil];
It looks like you never do this, and using archives to write to files only works if the file already exists (make sure you only do this once, otherwise every time that view is loaded the file will be emptied of all the data in it). And when you do this
if ([self dataFilePath])
It's pointless, because no matter what it always returns yes, whether the file exists or not.
Does your Person class implement NSCoding?
Specifically you need to implement something like the following in Person.m:
- (id)initWithCoder:(NSCoder *)decoder {
self = [super init];
if (!self) {
return nil;
}
self.name = [decoder decodeObjectForKey:#"name"];
self.age = [decoder decodeObjectForKey:#"age"];
return self;
}
- (void)encodeWithCoder:(NSCoder *)encoder {
[encoder encodeObject:self.name forKey:#"name"];
[encoder encodeObject:self.age forKey:#"age"];
}
I have a class that gets data from a repository and stuffs it into an NSArray:
EpisodeRepository Class
-(NSMutableArray *)getEpisodes {
NSMutableArray *episodes = [NSMutableArray array];
NSData *data = [self getDataFromAction:kGetEpisodesAction];
NSError *error = nil;
NSArray *json = [NSJSONSerialization JSONObjectWithData:data options:NSJSONReadingMutableContainers error:&error];
for (NSDictionary *d in json) {
Episode *episode = [self buildEpisodeFromJSONDictionary:d];
[episodes addObject:episode];
}
return episodes;
}
In my view controller, I keep a reference to the repo and the array of episodes, like so:
#interface VideoController : UITableViewController
#property (nonatomic, strong) NSArray *episodes;
#property (nonatomic, strong) EpisodeRepository *episodeRepo;
In the implementation, what I want to happen is that every the view appears, refetch the data from the web services(in the EpisodeRepository) and reload the table view with the updated episodes. This works great the first time the tableview is populated. The problem is that when I call, cellForRowAtIndexPath, the episodes array is completely blank. It's not null, but there is no data whatsoever in it. The weird thing is that all the other tableView delegates realize that there is data in the array and act accordingly. What is special about cellForRowAtIndexPath and why would it possibly delete my entries in the array?
Here is the controller implementation:
-(id) initWithCoder:(NSCoder *)aDecoder {
self = [super initWithCoder:aDecoder];
if(self) {
episodeRepo = [[EpisodeRepository alloc] init];
}
return self;
}
-(void)viewWillAppear:(BOOL)animated
{
[self loadContent];
[self.tableView reloadData];
[super viewWillAppear:animated];
}
-(void) loadContent {
self.episodes = [self.episodeRepo getEpisodes];
self.seasons = [self getSeasons:self.episodes];
[self setUILoaded];
}
-(UITableViewCell *) tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
//episodes is an empty array here???
}
I found the solution! It turns out I had code in my App Delegate under the willSelectViewController for the tab bar that was allocating the array outside of the view controller. Lesson learned: don't do this!
I want to fetch and index all the iPod Library artists in my app, just like the Music app does. The problem I'm having is that I don't know what's the best way to tackle this problem. Any help?
Let's name your UITableViewController class LibraryArtistsBrowserTableViewController
1. Preparation
The interface file LibraryArtistsBrowserTableViewController.h will contain the following variables:
#interface LibraryArtistsBrowserTableViewController : UITableViewController
#property (nonatomic, strong) MPMediaQuery *artistsQuery;
#property (nonatomic, strong) NSArray *artistsArray,*sectionedArtistsArray;
#property (nonatomic, strong) UILocalizedIndexedCollation *collation;
- (NSArray *)partitionObjects:(NSArray *)array collationStringSelector:(SEL)selector;
#end
2. Indexing
In your implementation file LibraryArtistsBrowserTableViewController.m, put the following code in your viewDidLoad method:
- (void)viewDidLoad
{
[super viewDidLoad];
//Make a query for artists
self.artistsQuery = [MPMediaQuery artistsQuery];
//Group by Album Artist
[self.artistsQuery setGroupingType:MPMediaGroupingAlbumArtist];
//Grab the "MPMediaItemCollection"s and store it in "artistsArray"
self.artistsArray = [self.artistsQuery collections];
//We then populate an array "artists" with the individual "MPMediaItem"s that self.artistsArray` contains
NSMutableArray *artists = [NSMutableArray array];
for (MPMediaItemCollection *artist in artistsArray) {
//Grab the individual MPMediaItem representing the collection
MPMediaItem *representativeItem = [artist representativeItem];
//Store it in the "artists" array
[artists addObject:representativeItem];
}
//We then index the "artists" array and store individual sections (which will be the alphabet letter and a numbers section), all containing the corresponding MPMediaItems
self.sectionedArtistsArray = [self partitionObjects:artists collationStringSelector:#selector(albumArtist)];
}
The function we use to section the items is defined below, place this somewhere your implementation file:
- (NSArray *)partitionObjects:(NSArray *)array collationStringSelector:(SEL)selector
{
self.collation = [UILocalizedIndexedCollation currentCollation];
NSInteger sectionCount = [[collation sectionTitles] count];
NSMutableArray *unsortedSections = [NSMutableArray arrayWithCapacity:sectionCount];
for(int i = 0; i < sectionCount; i++)
[unsortedSections addObject:[NSMutableArray array]];
for (id object in array)
{
NSInteger index = [self.collation sectionForObject:object collationStringSelector:selector];
[[unsortedSections objectAtIndex:index] addObject:object];
}
NSMutableArray *sections = [NSMutableArray arrayWithCapacity:sectionCount];
for (NSMutableArray *section in unsortedSections)
[sections addObject:[self.collation sortedArrayFromArray:section collationStringSelector:selector]];
return sections;
}
We then make sure the view controller knows how many sections and rows in each section there are, along with the titles of each section (the indexed letters/numbers):
- (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section {
return [[self.collation sectionTitles] objectAtIndex:section];
}
- (NSArray *)sectionIndexTitlesForTableView:(UITableView *)tableView {
return [self.collation sectionIndexTitles];
}
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
return [[self.collation sectionTitles] count];
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
return [[self.sectionedArtistsArray objectAtIndex:section] count];
}
3. Displaying the artists
We then display the artists as follows:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
static NSString *CellIdentifier = #"Cell";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
if (cell == nil) {
cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier] autorelease];
}
//We grab the MPMediaItem at the nth indexPath.row corresponding to the current section (or letter/number)
MPMediaItem *temp = [[self.sectionedArtistsArray objectAtIndex:indexPath.section] objectAtIndex:indexPath.row];
//Title the current cell with the Album artist
cell.textLabel.text = [temp valueForProperty:MPMediaItemPropertyAlbumArtist];
return cell;
}
You should be good to go. The only issue I have with this is that it fails to escape punctuation (apostrophes etc.) and 'The' prefix of artists. Other than that it works just fine.
You should read the iPod Library Access Guide. You'll need to make a request and fill your table view with the result. Depending on what you want you could use the MPMediaPickerController. If you just want the artists you should use an MPMediaQuery
MPMediaQuery *artistQuery = [MPMediaQuery artistsQuery];
EDIT:
NSString *artistKey = [MPMediaItem titlePropertyForGroupingType:MPMediaGroupingArtist];
MPMediaQuery *artistsQuery = [MPMediaQuery artistsQuery];
NSMutableArray *artists = [NSMutableArray arrayWithCapacity:artistsQuery.collections.count];
for (MPMediaItemCollection *album in artistsQuery.collections) {
MPMediaItem *albumItem = [album representativeItem];
[artists addObject:[albumItem valueForProperty:artistKey]];
}
The Documentation for reprsentativeItem states:
The media items in a collection typically share common property
values, owing to how the collection was built. For example, if you
build a collection based on a predicate that uses the
MPMediaItemPropertyArtist property, all items in the collection share
the same artist name. You can use the representativeItem property to
efficiently obtain values for such common properties—often more
efficiently than fetching an item from the items array.
Might want to look at UILocalizedIndexedCollation
- (NSArray *)partitionObjects:(NSArray *)array collationStringSelector:(SEL)selector
{
UILocalizedIndexedCollation *collation = [UILocalizedIndexedCollation currentCollation];
NSInteger sectionCount = [[collation sectionTitles] count];
NSMutableArray *unsortedSections = [NSMutableArray arrayWithCapacity:sectionCount];
for(int i = 0; i < sectionCount; i++)
{
[unsortedSections addObject:[NSMutableArray array]];
}
for (id object in array)
{
NSInteger index = [collation sectionForObject:object collationStringSelector:selector];
[[unsortedSections objectAtIndex:index] addObject:object];
}
NSMutableArray *sections = [NSMutableArray arrayWithCapacity:sectionCount];
for (NSMutableArray *section in unsortedSections)
{
[sections addObject:[collation sortedArrayFromArray:section collationStringSelector:selector]];
}
return sections;
}
Place that up top, and use it like so:
self.sectionedArray = [self partitionObjects:ArrayOfItemsFromQuery collationStringSelector:#selector(title)];
sectionedArray being an NSArray declared in .h.
I think this only works for a list of songs though.
I'm trying to implement an rss feed into my app and I have created a method to clean the title up.
- (NSString *)cleanTitle:(NSString *)Title {
return [Title stringByReplacingOccurrencesOfString:#"twitterfeed: " withString:#""];
}
The warning occurs on the articleTitle line below:
- (void)parseAtom:(GDataXMLElement *)rootElement entries:(NSMutableArray *)entries {
NSString *blogTitle = [rootElement valueForChild:#"title"];
NSArray *items = [rootElement elementsForName:#"entry"];
for (GDataXMLElement *item in items) {
NSString *articleTitle = [self cleanTitle: [item valueForChild:#"title"]];
Do you know how to get rid of this warning?
Thanks.
Make sure - (NSString *)cleanTitle:(NSString *)Title is also declared in your header file.
The method's signature must be known before it is used if the two methods are not in the same category or class. If it's the same class but -cleanTitle: is in a (Private) category or some such, be sure to declare that category prior to your class' implementation (in your .m file) :
#interface MyClass (Private)
- (NSString *)cleanTitle: (NSString *)title;
#end
Update: Unfortunately the help offered below did not solve my problem of sharing a class property across functions. Can anyone else suggest a possible problem? Here's the latest code:
Header .h:
#interface FirstViewController:UIViewController <UITableViewDataSource, UITableViewDelegate, UITabBarControllerDelegate> {
NSDictionary *sectorDictionary;
NSInteger sectorCount;
}
#property (nonatomic, retain) NSDictionary *sectorDictionary;
- (id)initWithData:(NSMutableDictionary*)inData;
Implementation .m:
#synthesize sectorDictionary;
- (id) testFunction:(NSDictionary*)dictionary {
NSLog(#"Count #1: %d", [dictionary count]);
return nil;
}
- (id)initWithData:(NSMutableDictionary *)inData {
self = [self init];
if (self) {
[self testFunction:inData];
// set the retained property
self.sectorDictionary = inData;
}
return self;
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
NSLog(#"Count #2: %d", [self.sectorDictionaryCopy count]);
return [self.sectorDictionaryCopy count];
}
Console output:
2010-05-05 20:11:13.126 JSONApp[17378:207] Count #1: 9
2010-05-05 20:11:13.130 JSONApp[17378:207] Count #2: 0
Following up on this question about sharing an object between classes, I now need to figure out how to share that object across various functions in a class.
First, the setup: In my App Delegate I load menu information from JSON into a NSMutableDictionary and message that through to a view controller using a function called initWithData. I need to use this dictionary to populate a new Table View, which has methods like numberOfRowsInSection and cellForRowAtIndexPath.
I'd like to use the dictionary count to return numberOfRowsInSection and info in the dictionary to populate each cell. Unfortunately, my code never gets beyond the init stage and the dictionary is empty so numberOfRowsInSection always returns zero.
I thought I could create a class property, synthesize it and then set it. But it doesn't seem to want to retain the property's value. What am I doing wrong here?
In the header .h:
#interface FirstViewController:UIViewController <UITableViewDataSource, UITableViewDelegate, UITabBarControllerDelegate> {
NSMutableDictionary *sectorDictionary;
NSInteger sectorCount;
}
#property (nonatomic, retain) NSMutableDictionary *sectorDictionary;
- (id)initWithData:(NSMutableDictionary*)data;
#end
in the implementation .m:
#synthesize sectorDictionary;
- (id) testFunction:(NSMutableDictionary*)dictionary {
NSLog(#"Count #1: %d", [dictionary count]);
return nil;
}
- (id)initWithData:(NSMutableDictionary *)data {
if (!(self=[super init])) {
return nil;
}
[self testFunction:data];
// this is where I'd like to set a retained property
self.sectorDictionary = data;
return nil;
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
NSLog(#"Count #2: %d", [self.sectorDictionary count]);
return [self.sectorDictionary count];
}
Output from NSLog:
2010-05-04 23:00:06.255 JSONApp[15890:207] Count #1: 9
2010-05-04 23:00:06.259 JSONApp[15890:207] Count #2: 0
Any init function should return self if it completes successfully. Change the return nil to return self at the end of initWithData.
-(id) initWithData:(NSMutableDictionary *)inData {
self = [self init]; // best practice use super for method with same name
if ( self ) {
self.sectorDictionary = inData;
}
return self;
}
The sectorDictionary member should not be mutable. If it changes, you need to call reloadData on the table. Your setter function should make an immutable copy.
-(void) setSectorDictionary:(NSDictionary *)inData {
NSDictionary *old = sectorDictionary;
sectorDicitonary = inData ? [[NSDictionary alloc] intWithDictionary:inData] : nil;
[self.table reloadData];
[old release];
}
I believe your problem lies within the line self.sectorDictionary = data.
You need to firstly allocate some memory for the dictionary you are creating, so a line like
self.sectorDictionary = [[NSMutableDictionary alloc] init]; is going to be necessary.
After you have initiated the new dictionary you need to populate it with the contents of the dictionary that was passed in.
So...
Try the line:
self.sectorDictionary = [[NSMutableDictionary alloc] initWithDictionary:data];
instead of:
self.sectorDictionary = data;
instead of
self.sectorDictionary = data;
try this way...
self.sectorDictionary = [[NSMutableDictionary alloc] init];
self.sectorDictionary = (NSMutableDictionary *) data;