I've been following a tutorial on iOS development - specifically drill-down UITableViews. I have my own custom plist established, but I can't seem to get the DetailViewController to populate with my plist information. I could really use some help here, I'm a bit over my head!
edit: Here's some details...
The app works through a plist-populated RootViewController, which is a UITableView. When there aren't any children left in the plist, it changes to a Detail view:
AppDelegate.m
NSDictionary *tempDict = [NSDictionary dictionaryWithContentsOfFile:[[NSBundle mainBundle] pathForResource:#"Data" ofType:#"plist"]];
self.data = tempDict;
RootViewController.m
- (void)viewDidLoad
{
[super viewDidLoad];
if(CurrentLevel == 0) { // At the 'root' of the plist
//Initilalize our table data source
NSArray *tempArray = [[NSArray alloc] init];
self.tableDataSource = tempArray;
AppDelegate *AppDelegate = (AppDelegate *)[[UIApplication sharedApplication] delegate];
self.tableDataSource = [AppDelegate.data objectForKey:#"Rows"];
self.navigationItem.title = #"PedalTome";
}
else
self.navigationItem.title = CurrentTitle;
}
later on...
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
//Get the dictionary of the selected data source.
NSDictionary *dictionary = [self.tableDataSource objectAtIndex:indexPath.row];
//Get the children of the present item.
NSArray *Children = [dictionary objectForKey:#"Children"];
if([Children count] == 0) {
DetailViewController *dvController = [[DetailViewController alloc] initWithNibName:#"DetailView" bundle:[NSBundle mainBundle]];
[self.navigationController pushViewController:dvController animated:YES];
}
else {
//Prepare to tableview.
RootViewController *rvController = [[RootViewController alloc] initWithNibName:#"RootViewController" bundle:[NSBundle mainBundle]];
//Increment the Current View
rvController.CurrentLevel += 1;
//Set the title;
rvController.CurrentTitle = [dictionary objectForKey:#"Title"];
//Push the new table view on the stack
[self.navigationController pushViewController:rvController animated:YES];
rvController.tableDataSource = Children;
}
}
My DetailViewController.m is empty, with the exception of a placeholder self.navigationController.title.
If I'm understanding correctly, I need to pass information from RootViewController to DetailViewController - the location and implementation of the plist, the index level (is that what it's called) in the plist, and the string inside that index level (under the key Detail).
At this point, any progress is amazing progress. Thanks in advance.
You can pass whatever information you need to your DetailViewController by setting up a synthesized property in your DetailViewController, and then passing your data to it inside your if-block.
For example, in your DetailViewController.h you would have the following (without ARC):
#property (retain, nonatomic) NSDictionary *myAwesomeDictionary;
Or, with ARC enabled:
#property (strong, nonatomic) NSDictionary *myAwesomeDictionary;
Then in DetailViewController.m you would have the following:
#synthesize myAwesomeDictionary;
Then you would change your code block to the following:
if([Children count] == 0) {
DetailViewController *dvController = [[DetailViewController alloc] initWithNibName:#"DetailView" bundle:[NSBundle mainBundle]];
[dvController setMyAwesomeDictionary:dictionary];
[self.navigationController pushViewController:dvController animated:YES];
}
This assumes that the NSDictionary called dictionary that you created a few lines above is the data that you'd like to show in your DetailViewController.
Then in your DetailViewController's viewDidLoad: method you can access that dictionary using self.myAwesomeDictionary and do whatever you need to do with it.
Disclaimer:
Two things seem to go against Apple's code style standards in your code:
Your AppDelegate stores your model (your plist). - Apple says that you shouldn't crowd your AppDelegate with global data/logic. In general, only write code that pertains specifically to a class, in that specific class.
You aren't parsing your plist into custom objects. - This makes it hard to code because you constantly have to figure out what your generic Array and Dictionary objects represent, and make your code totally unreadable for other people.
Some of your instance variable names are capitalized. For example, NSArray *Children should be NSArray *children and CurrentLevel should be currentLevel. Only Class names have the first letter capitalized.
Check out http://jlawr3nc3.github.com - specifically my CompanyRecords example code for information on how to make a class and FunWithArrays for how to parse a plist into custom objects. MusicLibraryiOS then delves into how to take a plist, parse it into custom objects, and then display it in a UITableView along with a detail view.
Table View Specifier May do what you need.
Specified Table View is an iOS table view that has its contents specified
via a plist file. Its purpose is largely demonstrative but is also designed
to be used in a live product (useful for credits pages). Can be used with
iOS version 4.2 and above.
A dig through their code will most likely be enlightening.
Related
I am trying to update the score of one of the labels on my custom cell after returning from a push navigation.
In my parent UITableViewController I have the code:
In ViewDidLoad
//These are mutable strings
self.disciplineScoreString1 = [NSMutableString stringWithFormat:#""];
self.disciplineScoreString2 = [NSMutableString stringWithFormat:#""];
self.disciplineScoreString3 = [NSMutableString stringWithFormat:#""];
self.disciplineScoreArray = [[NSMutableArray alloc] init];
[self.disciplineScoreArray addObject:self.disciplineScoreString1];
[self.disciplineScoreArray addObject:self.disciplineScoreString2];
[self.disciplineScoreArray addObject:self.disciplineScoreString3];
So far so good.
In cellForRowAtIndexPath
//this is the custom subclass of UITableViewCell
DayTableViewCell *cell = (DayTableViewCell *) [tableView dequeueReusableCellWithIdentifier:CellIdentifier forIndexPath:indexPath];
//this is the custom label
cell.disciplineScoreLabel.text = [self.disciplineScoreArray objectAtIndex:indexPath.row];
return cell;
Still so far so good. Right now the label in each of the 3 cells is blank.
I now segue to a UIViewController from the first cell and I return from the child UIViewController successfully setting the string value of self.disciplineScoreString1 to #"10"which I NSLog'ged in the parent UITableViewController.
How do I update the label for the first cell now? I have tried reload data in ViewWillAppear but its not working.
Thankyou
EDIT
This is the code in the Child ViewController
In viewWillDisappear:
[super viewWillDisappear:animated];
[self calculateDisciplineScore];
NSInteger currentVCIndex = [self.navigationController.viewControllers indexOfObject:self.navigationController.topViewController];
DisciplineTableViewController *parent = (DisciplineTableViewController *)[self.navigationController.viewControllers objectAtIndex:currentVCIndex];
parent.disciplineScoreString1 = self.disciplineScoreText;
You must be changing the string, instead of modifying it... consider this example
NSMutableArray *array = [NSMutableArray array];
NSMutableString *disciplineScoreString1 = [NSMutableString stringWithFormat:#"original string"];
[array addObject:disciplineScoreString1];
[disciplineScoreString1 appendString:#" hello"]; // OK.. you're modifying the original string
NSLog(#"%#", [array objectAtIndex:0]);
disciplineScoreString1 = [NSMutableString stringWithFormat:#"new string"]; // WRONG!!
NSLog(#"%#", [array objectAtIndex:0]);
The ouput is:
2014-02-15 06:36:46.693 Hello[19493:903] original string hello
2014-02-15 06:36:46.694 Hello[19493:903] original string hello
The second example is wrong because you're creating a new string, and the object in the array is still pointing to the old original string.
EDIT:
// try this
[parent.disciplineScoreString1 replaceCharactersInRange:NSMakeRange(0, parent.disciplineScoreString1.length) withString:self.disciplineScoreText];
// instead of
parent.disciplineScoreString1 = self.disciplineScoreText;
Your current approach is quite rigid and not very standard. You would normally set up some kind of delegate to pass data from a child to a parent. It is very rarely correct that a child should know how to reach into the parent's innards and change it's data.
I would personally start with something like this
#interface ChildViewController : UIViewController
#property (nonatomic, copy) void (^onCompletion)(ChildViewController *viewController, NSString *disciplineText);
#end
Now I'm assuming you are dismissing by just calling [self.navigationController popViewControllerAnimated:YES] from within the child? This is a little odd as it means that the childViewController can now only ever be presented in a navigationController, this makes it less reusable.
What I would do is at the point where you normally call popViewControllerAnimated: I would call the completion instead
if (self.onCompletion) {
self.onCompletion(self, self.disciplineText);
}
Now the object who provides the onCompletion can decide how this controller get's removed and it's told about the data that we finished with, which enabled the controller to do what it wants with it.
In your case the parent controller would provide the completion as it knows how the child is presented so it will know how to dismiss it. It also know that it may want to do something with the data the child finished with
// Where you present the child
childViewController.disciplineText = self.self.disciplineScoreArray[index];
__weak __typeof(self) weakSelf = self;
childViewController.onCompletion = ^(ChildViewController *viewController, NSString *disciplineText){
weakSelf.disciplineScoreArray replaceObjectAtIndex:index withObject:disciplineText];
[self.navigationController popViewControllerAnimated:YES];
[weakSelf.tableView reloadRowsAtIndexPaths:#[ [NSIndexPath indexPathForRow:index inSection:0] ]
withRowAnimation:UITableViewRowAnimationFade];
};
I have tried to push to a UIViewController using an Array:
self.cities=#[#"New York", #"Chicago", #"Los Angeles", #"Miami"];
and then in my didSelectRowAtIndexPath:
[self.navigationController pushViewController:NewYorkViewController animated:YES];
but now all the items in the array push to NewYorkViewController. I believe an NSDictionary would work
NSDictionary *cities=#[#{#"city":#"NewYork",#"viewController":NewYorkViewController}, #{#"city":#"Chicago",#"viewController":ChicagoViewController}, #{#"city":#"LosAngeles",#"viewController":LosAngelesViewController}, #{#"city":#"Miami",#"viewController":MiamiViewController}];
However I'm a noob and have never worked with NSDictionary. If you would be so kind to explain how i would go about doing this. If you know a way using array's that would work too.
ps.
I don't use storyboards and could you also explain what I would do in my cellForRowAtIndexPath: and numberOfRowsInSection:
Thanks
I am guessing you want to transfer your data from the UITableViewController to NewYorkViewController. The usual way to do this is having a property in your NewYorkViewController say an array or whatver you want.
#property (nonatomic,retain) NSMutableArray *itemsArray;
and in didSelectRowAtIndexPath while creating NewYorkViewController's object
NewYorkViewController *newyorkObject = [NewYorkViewController alloc]init];
newyorkObject.itemsArray = self.cities;
[self.navigationController pushViewController:newyorkObject animated:YES];
and in viewDidLoad of NewYorkViewController add this:
NSLog(#"items array: %#",self.itemsArray); //to check whether you got what you passed from the previous class.
I am using the Windows System now, so the following code may contain errors.
Use reflection to get the IOS Class
And then use this to alloc an object and operate it
You can use [Object class] or NSClassFromString ("Class Name") to get type of class
like this:
NSString * ClassName = # "yourClassName";
Class * tempClass = NSClassFromString (ClassName);
//if your class is subclass of UIViewController
UIViewController * tempObj = [[tempClass alloc] init];
//operating it
[self.navigationController pushViewController: tempObj animated:YES];
[tempObj release];
For your code you can change it like this:
//NSDictionary =#{#"key",#"value",....}
NSDictionary *citieone=#{#"NewYork",[NewYorkViewController Class]}
NSDictionary *citietwo = #{#"Chicago",NSClassFromString(#"ChicagoViewController")};
//other citys
NSArray* cities = #{citieone,citietwo,...};
//in UITableView
NSString* cityName = [[[citis objectForIndex:index] allKeys] objectAtIndex:0];
Class* controllerClass = [[[citis objectForIndex:index] allValues] objectAtIndex:0];
// operating it
UIViewController * controller= [[controllerClass alloc] init];
[self.navigationController pushViewController: controller animated:YES];
[controller release];
I did something similar with my app, but I was using NSArray instead of NSDictionary, but the logic is similar IMHO.
I created a NSArray with my objects, and a method to return their count and also the name at index. This is so that when I want to get a particular object from the array I can access it quicky. I have changed my code to accept the NSDictionary so that you can use.
In order to create the number of rows in the UITableView, I created a method that returns the number of rows when called.
-(NSInteger) numberOfCities
{
NSArray * allKeys = [mutableDictionary allKeys];
return [allKeys count];
}
This will be called in the
- (NSInteger) tableView: (UITableView*) tableView numberOfRowsInSection: (NSInteger) section
{
return [self numberOfCities];
}
so that the UITableView will have the exact number of rows as the NSDictionary
In order to return the name at a particular index so that you can display that name in the UITableViewCell, I created this method
-(NSString*) cityNameAtIndex: (NSInteger) index
{
return NSString* cityName = [[[mutableDictionary objectForIndex:index] allKeys] objectAtIndex:index]; ;
}
This method can be called in the
- (UITableViewCell*) tableView: (UITableView*) tableView cellForRowAtIndexPath: (NSIndexPath*) indexPath
to assign the different city names to different cells in the table.
Then in the delegate
- (void) tableView: (UITableView*) tableView didSelectRowAtIndexPath: (NSIndexPath*) indexPath
you can call this method and push to the different viewControllers
UIViewController* selectedController = nil ;
NSString* nameOfCity = [self cityNameAtIndex: indexPath.row] ;
if( [nameOfCity isEqualToString:#"New York"] )
selectedController = [[NewYorkViewController alloc] init];
else if( [nameOfCity isEqualToString:#"Chicago"] )
selectedController = [[ChicagoViewController alloc] init];
else if( [nameOfCity isEqualToString:#"Miami"] )
selectedController = [[MiamiViewController alloc] init];
else if( [nameOfCity isEqualToString:#"Los Angeles"] )
selectedController = [[LosAngelesViewController alloc] init];
To do this, you will have to create the different viewControllers (Chicago, New York, Los Angeles, Miami) beforehand, but if you have already created them and left them as an object in the NSDictionary, you can use the nameOfCity to grab the viewController object within the NSDictionary that belongs to that city, and then assign the selectedController to that viewController.
But if you are only using the NSDictionary to store the names of the cities, I think that NSArray will be a better option. I created mine in the appDelegate and the names within the NSArray are #define in my class called global.h as I needed the NSArray and names in many different viewControllers, so if you need that as well, you can do the same.
Hope this helps! :)
I am new to iOS.I am recently stuck with a problem.
I have a view A and View B. View A has a navigation controller. view A has a button to switch to B.When i am clicking this button every time B creates a new object. how can i track this object to share data between this two view.
Thanks
There are several ways to do this.
You could have a property of B, that A sets before you push. (NSDictionary, Array, String etc)
This not the best way however it would work.
UIViewController *viewB = [[UIViewController alloc]init];
[viewB setMyProperty:#"some data!"];
[self.navigationController pushViewController:viewB animated:YES];
You could also use NSNotificationCenter to pass the object to the next view.
NSDictionary *dictionary = [NSDictionary dictionaryWithObject:
[NSNumber numberWithInt:index]
forKey:#"index"];
[[NSNotificationCenter defaultCenter] postNotificationName:#"myNotification"
object:self
userInfo:dictionary];
The way I usually handle this is to setup and object that holds my data with an associated protocol initialized in my AppDelegate. Then any view that needs to read/write something just grabs a Pointer to that object and runs with it.
#class AppData;
#protocol AppDataProtocol
- (AppData*)theAppData;
#end
in the View you can grab the pointer with this.
-(AppData*)theAppData {
id<AppDataProtocol> theDelegate = (id<AppDataProtocol>)[UIApplication sharedApplication].delegate;
AppData* theData = (AppData*)theDelegate.theAppData;
return theData;
}
and this.
appData = [self theAppData];
You are then able to easily access any property of appData.
-(void)fnButtonA{
ViewB *vcB = [[ViewB alloc] initWithData:DataToB];
[[self navigationController] pushViewController:vcB animated:Yes];
}
In ViewB.m edit the init function to
-(UIViewController *)initWithData:(NSMutableDictionary*)data
I have a project I am working on, to learn some more about JSON and restkit. It all is working great, however I am having trouble with an array losing it's values.
This is the last method that is executed in my network request.
SHRetrieveStoresWS.m
- (void)objectLoader:(RKObjectLoader *)objectLoader didLoadObjects:(NSArray *)objects
{
self.stores = [[NSArray alloc] initWithArray:objects];
StoresViewController *viewController = [[StoresViewController alloc] init];
[viewController didLoadObjects:objects];
for (Store *aStore in stores) {
NSLog(#"%#", [aStore longName]);
}
}
Which calls this method in my view controller.
StoresViewController.m
#property (strong, nonatomic) NSArray *data;
- (void)didLoadObjects:(NSArray *)aArray
{
NSLog(#"%d", aArray.count);
self.data = [[NSArray alloc] initWithArray:aArray];
NSLog(#"%d", data.count);
[self.tableView reloadData];
}
The values are correct when I ask for the values within this method, but the array shows 0 objects immediately afterwards. Am I missing something here?
I am later checking the value with this method.
- (IBAction)pushMe:(id)sender
{
NSLog(#"Data: %d", self.data.count);
}
You should pass the data in the segue...
-(void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender{
if ([segue.identifier isEqualToString: #"MY_IDENTIFIER"]){
StoresViewController *viewController = segue.destinationViewController;
[viewController didLoadObjects: objects];
}
}
That should work for you! Just change MY_IDENTIFIER to whatever the identifier of your segue is.
StoresViewController is initialised as a local variable which is only accessible in the method that it was declared (aka objectLoader). After objectLoader has completed, the local variable is no longer valid.
The problem is most likely that you're creating multiple instances of StoresViewController instead of giving your network controller a reference to the original instance.
You can demonstrate it to yourself by printing out self in -viewDidLoad and again in didLoadObjects:. You'll see that the pointer addresses are different.
This line is the culprit:
StoresViewController *viewController = [[StoresViewController alloc] init];
Instead of instantiating StoresViewController again, add a property to your SHRetrieveStoresWS class and use it to hold a reference to your view controller.
#property (strong) StoresViewController *viewController;
You'll need to set that property before -didLoadObjects: is invoked.
BookingDocumentsViewController *bdVc = [self.storyboard instantiateViewControllerWithIdentifier:#"BookingDocs"];
bdVc.orId = rl_id;
bdVc.docsArray = self.documentsArray;
[self.navigationController pushViewController:bdVc animated:YES];
I have Above code snippet. I'm trying to load a new viewcontroller and assign its Mutable Array (docsArray) object to current view's mutableArray (documentsArray <=this is not nil)
Whenever I execute above code I get EXC_BAD_ACCESS error.
but if I comment the 3rd line. It works but I can't get my array to the new view. I even tried with [[NSMutableArray alloc] initWithArray:self.documentsArray]; this doesnt work either.
But if I use bdVc.docsArray =[[[NSMutableArray alloc] init]; it works but again I can't get my mutable array to the new view.
Edit:
However 2nd line has NSString values. And they can be passed without a problem.
What am I doing wrong here?
I'm not getting any errors in console, instead I get this.
Maybe consider using a Segue. It instantiates the destination viewcontroller for you. Then in your source view controller implement
- (void) prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender
Get a reference to your destination viewcontroller and set its data.
BookingDocumentsViewController *bdVc = [segue destinationViewController];
bdVc.docsArray = self.documentsArray;
in BookingDocumentsViewController.h
#property(nonatomic, retain)NSmutableArray *docsArray;
You do can do in your BookingDocumentsViewController.m:
#synthesize docsArray;
- (void)viewDidLoad
{
NSmutableArray *array = [NSmutableArray alloc]initWithArray:docsArray];
[super viewDidLoad];
}
then when you are pushing the view
BookingDocumentsViewController *bdVc = [self.storyboard instantiateViewControllerWithIdentifier:#"BookingDocs"];
bdVc.orId = rl_id;
bdVc.docsArray = self.documentsArray;
[self.navigationController pushViewController:bdVc animated:YES];
[bdVc Release];
I think I found the issue. A very basic mistake. In bdVc's viewDidLoad I had the following line,
NSLog(#"Booking Documents viewDidLoad : %#",self.docsArray.count);
This was causing the error. that %# instead of %d. I wonder why xcode didn't show proper reason for the error.
Thank you all for the help. :)