I'm new to iOS, coming from a PHP/MYSQL background, and am trying to show a detail view of a table.
I have created a simple NSMutableArray to hold some records as we say in web programming. I am able to display these in a table view and have it working so when you touch a row, you go to the detail page.
But I am totally flummoxed by how you get the detail view to display data using the MVC model and have scoured the web to no avail trying to find a simple example how to do this. I have tried numerous tutorials and viewed numerous videos but can't get the hang of it, because I don't understand the principle involved. I want to pull the data for the record clicked on, but don't know to pass this to the detail view.
The NSMutableArray is created in the tableview controller. Trying to follow MVC, I have also created a model class that lists some properties corresponding to the field names held in the array.
What I can't figure out is how to display this in the detail view.
Does it have something to do with Prepare for Segue? However, that would seem to violate MVC as the data for the detail page is actually in the table view page i.e. a different controllers.
I'm desperate for some guidance...
I don't know what code to try as none has worked for me. I just need a basic example of how to do this seemingly simple routine task.
Create a property in your DetailViewController then set the property in your TableViewController's as follows in its 'didSelectRowAtIndexPath'
DetailViewController *dvc = [DetailViewController alloc] init]; //Can be instantiated via the storyboard too.
OR
DetailViewController *dvc = (DetailViewController *)[self.storyboard instantiateViewControllerWithIdentifier:#"DetailViewControllerIdentifier"]; // Do not forget to set the identifier in the storyboard for the viewcontroller.
then,
dvc.recordsArray = self.recordsArray //setting the recordsArray property of the DetailViewController object to point to the TableViewController recordsArray
[self.navigationController pushViewController:DVC animated:YES];
[self.TableViewController deselectRowAtIndexPath:indexPath animated:YES];
Related
I would like to pass some data from the last Table View to the First View. How can I do grab a hold of the First View object? I'm familiar with delegate pattern.
From the First View, I'm using Style Modal to invoke the Table View.
Create an object to manage your data model outside of your controller structure (singleton or owned by the application delegate). Update it when you have new data and read from it when you want to display something. Then, instead of having to make links between controllers, all you need to do is remove the one or ones you don't want and let the one you go back to decide what to show.
to get your rootViewController of your navigation controller, you can try this
NSArray *viewControllers = self.navigationController.viewControllers;
YourRootViewController *rootViewController = (YourRootViewController *)[viewControllers objectAtIndex:viewControllers.count - 2];
and add your data to rootViewController
QUICK BACKGROUND:
I have an app that shows a list of songs in a searchable, tabbed interface (a.k.a. "Cheers View Controller"). There are two tabs, one for each category of my cheers. I have a sidebar that allows you to select menu options, one of which is called "My Favorites". It uses the SWReveal Controller (a.k.a. "Sidebar"). I am trying to allow the user to mark favorite cheers before game time, then use the sidebar navigation to quickly reference their list of favorite songs when they're in the dugout.
The initial view controller for my application is a Reveal View Controller.
The front view (sw_front) is the tab bar controller that shows the Cheers View Controller when the app first launches. The data is loaded into an array form a CSV file in the ViewDidLoad of the Cheers View Controller.
The rear controller (sw_rear) is the Sidebar that defines the menu options for my application. One of the menu options on my Sidebar is called My Favorites. It's this sidebar that is conceptually throwing me. See below for a snapshot of the relevant area of my storyboard.
STORYBOARD SNAPSHOT:
It turns out that as a new user I cannot post images. :(
GOAL
I am trying to access the array of favorite cheers in the Cheers View Controller from the My Favorites view controller. There is currently no segue from Cheers View Controller to My Favorites and it seems like defining a segue here is wrong conceptually.
I don't want to use a singleton or some other work around, I would rather structure my app the correct way. One easy solution is to move My Favorites over to another tab of the Tab Bar Controller. While I can change the design and things would probably be easier, it feel like I'm missing a valuable concept here that I can use in later apps.
I realize that there are many posts already on the topic of passing data from one controller to another. I've read what I think is all of them but I have not been able to incorporate the solutions into the SWRevealController implementation successfully yet. So clearly I'm confused either on mechanics or on concepts (or both). Most of those posts I read were involving segues or passing fields between two controllers that had segues or relationships of some sort.
MECHANICS QUESTIONS:
I currently prep the favorites array in the ViewDidLoad of the table view controller in the Main Tabbed Interface. I did this immediately after loading the main array with all the songs from a CSV file. I was thinking that I could somehow pass this to the My Favorites table view controller or access it from the My Favorites Table view controller.
//Copy favorite cheers into array
NSPredicate *predicate = [NSPredicate predicateWithFormat:#"SELF.favorite contains[c] %#",#"Yes"];
NSArray *tempArray = [self.cheersArray filteredArrayUsingPredicate:predicate];
self.myFavoriteCheersArray = [NSMutableArray arrayWithArray:tempArray];
Here's where it gets sticky for me - when the user clicks the button to show the SideBar, then clicks on My Favorites:
the prepareForSegue of the Sidebar will execute with this:
- (void) prepareForSegue: (UIStoryboardSegue *) segue sender: (id) sender
{
// Get the selected row and identify the destination controller you are sending to
NSIndexPath *indexPath = [self.tableView indexPathForSelectedRow];
UINavigationController *destViewController = (UINavigationController*)segue.destinationViewController;
// Set the title of navigation bar by using the menu items
destViewController.title = [[_menuItems objectAtIndex:indexPath.row] capitalizedString];
if ( [segue isKindOfClass: [SWRevealViewControllerSegue class]] ) {
SWRevealViewControllerSegue *swSegue = (SWRevealViewControllerSegue*) segue;
swSegue.performBlock = ^(SWRevealViewControllerSegue* rvc_segue, UIViewController* svc, UIViewController* dvc) {
UINavigationController* navController = (UINavigationController*)self.revealViewController.frontViewController;
[navController setViewControllers: #[dvc] animated: NO ];
[self.revealViewController setFrontViewPosition: FrontViewPositionLeft animated: YES];
};
}
And I was trying to add this to the end of the prepareForSegue to somehow allow My Favorites access to the array...but I'm having trouble identifying the property in the Cheers View Controller from the Sidebar...
if([segue.identifier isEqualToString:#"myFavorites"]) {
//Get the Favorites View Controller (which has a navigation controller)
MyFavoritesViewController *myFavoritesViewController = [[MyFavoritesViewController alloc] init];
myFavoritesViewController = (MyFavoritesViewController*)destViewController.topViewController;
//THIS BELOW PART IS SHAKEY AT BEST
//Get the Cheers View Controller (which is part of a tabbed view controller)
SWRevealViewController *frontViewController = [[SWRevealViewController alloc] init];
CheersViewController *cheersViewController = (CheersViewController*)frontViewController.frontViewController;
//Pass the Favorites Array -- The below code gives me an error that the property I set on the CheersViewController is not found.
rankingViewController.favoriteCheers = CheersViewController.myFavoriteCheersArray;
}
}
CONCEPT QUESTIONS I'D LOVE COMMENTARY ON:
What is the best (by best, I mean a balanced approach between implementation reality and academic purity) approach to make the array available to another view controller when a segue is not defined without butchering OO concepts/designs?
The rear view controller (the Sidebar) does not get loaded on app startup - instead it gets loaded on first display. Is it correct that I am therefore loading the data for the first time in the Cheers View Controller (i.e. I could make the Sidebar load on app startup but this seems conceptually wrong as well since the user may never access the Sidebar)?
Is it best to prep the my favorites array when the app first launches in the Cheers View Controller or only when My Favorites is first viewed? The list is rather small but technically if the user never clicked on the sidebar to view their favorites then I would be loading an extra array unnecessarily - probably a nit since the array is small but I am curious.
Is the proper place to load the data where I have it currently?
It seems to me that a delegate conceptually wouldn't make sense since the Cheers View Controller doesn't really need to be aware of a user viewing his/her favorites. All I'm really doing is trying to query a set of master data. Am I correct in thinking this way?
Here is how these were solved. They work - as for whether or not they are the best, I'll let you know as I learn more. grin
QUESTION 1: Load an initial set of data from a plist, CSV or any other source file in AppDelegate didFinishLaunchingWithOptions. For example:
//Read in from JSON file
NSError *error;
NSData *data = [NSData dataWithContentsOfFile:[[NSBundle mainBundle] pathForResource:#"Cheers" ofType:#"json"]];
NSDictionary *json = [NSJSONSerialization JSONObjectWithData:data options:kNilOptions error:&error];
self.cheersArray = json[#"cheers"];
This array can be immutable (i.e. NSArray) and you can cast this to a view controller who can manipulate it as an NSMutableArray if needed. For example:
//Instantiate initial view controller
UIStoryboard *storyboard = [UIStoryboard storyboardWithName:#"Main" bundle:[NSBundle mainBundle]];
CMViewController *cmViewController = [storyboard instantiateInitialViewController];
//Pass array of data into a table view controller
cmViewController.tableData = (NSArray *)self.tableData;
This assumes your View Controller has a property that can be used to pass the data into.
If you had a JSON file and need it to be a PLIST, there's a great utility via Terminal on your Mac
plutil -convert xml1 InputFileName.json -o OutputFileName.plist
As you learn like I did you may realize plists with a smaller number of rows is preferred over JSON files and a tad bit easier (personal opinion) to deal with when it comes time to saving changes to the data (if your app allows that).
QUESTION 2: It is conceptually wrong from an academic point of view to load the Sidebar on app startup. It should only be loaded on first use.
QUESTION 3:Similar to Question 2, It is best to pass the favorites array to the view controller that needs it on first request (i.e. the user presses on the tab "Favorites". Since technically Favorites is just a specific view of a subset of the master data I am loading, I believe this is correct.
QUESTION 4:I moved it form the first view controller to the App Delegate as shown above. This gave me more flexibility to pass data down (conceptually speaking from a parent to a child view controller) rather than across (form child to child view controller). This simplified my code.
QUESTION 5: I don't believe a delegate is appropriate for the answer to the challenge I was facing.
Feel free to comment!
I am working with the project of ios and doing well in it. But now i stuck at one place where i am having three views (Say 1stview, 2ndview, 3rdview). I am navigating to second view from first view and third view using the code line below.
[self.navigationController pushViewController:first view animated:YES];
How can i check on second view wheather i am navigating from first view or third view. So that i can use particular condition on it.
So please help me out regarding this issue. Your help will be much appreciable.
Take a variable in second View controller. When you are creating the object of it, set proper value into it. Later on when it will get pushed, you can use that value to take proper decisions.
In the file of FirstViewController you will write below lines:
SecondController *controller = [[SecondController alloc]init];
controller.flag = 1; //That means you came here from viecontroller 1
[self.navigationController pushViewController:controller animated:YES];
In the file of ThirdViewcontroller you will write below lines:
SecondController *controller = [[SecondController alloc]init];
controller.flag = 3; //That means you came here from viecontroller 3
[self.navigationController pushViewController:controller animated:YES];
Try to arrange thing so that the 2ndView doesn't know about 1stView or 3rdView, but instead just changes it's behavior according to how it was configured. So, let's say that when you're navigating to 2ndView from 1stView, 2nd should display with a green background and when you get there from 3rd it should use blue instead. Rather than telling 2nd which controller preceded it, have the preceding controller just tell 2nd what background color to use. Same goes for any other aspect of 2ndView's behavior.
The benefit of doing it this way is that you can change 1st or 3rd without having to change anything in 2nd, and you can later add a 4thView or 5thView that also use 2ndView without having to change 2ndView.
Implement the method – navigationController:willShowViewController:animated: from the UINavigationControllerDelegate Protocol Reference. Inside this method you can check the navigation stack to get the current view controller using several properties of UINavigationController. An example would be access the visibleViewController property.
As #Apurv pointed out you need some sort of identifier mechanism to be able to know which view controller the call came from. e.g.: viewController.view.tag
I just have a quick question about recommended ways to implement a master-detail view hierarchy in iOS--the kind where selecting a row in a table on one screen pushes a details view for that item onto the navigation stack.
Specifically, should I reuse the same instance of the details view controller and just change its target and reload it each time, or should I instantiate a new instance of the view controller each time?
I'd prefer the first method, as it just seems generally more efficient, but I'm having trouble figuring out how to set the target and do the reload (especially the first time, when the view controller has not yet even been initialized--I'm using storyboards and that pretty much handles all of the initialization itself).
Or perhaps instead of setting the target on the child view controller, I could set it on the parent, such that each time the child view controller is shown, it reloads itself based on the parent selection? That actually sounds like the best bet so far, just looking for tips/warnings from anyone who's run into this before.
First, there's nothing wrong with creating a new view controller each time. If you use segues, that's what you'll get, since segues always instantiate new controllers. The detail controller will be deallocated when you pop or dismiss it anyway, so it won't persist.
If you want to use the same controller, you have to do your push or presentViewController in code. You can still setup the controller in the storyboard. Give it an identifier, but don't connect it up with a segue. In code, you check for the existence of your controller (you'll need a property for it), and if it doesn't exist, create it.
if (! self.detailController) {
DetailController *dvc = [self.storyboard instantiateViewControllerWithIdentifier:#"MyIdentifier"];
}
self.dvc.whateverProperty = self.somePropertyIWantToPass; // pass some date to it
[self.navigationController pushViewController:dvc animated:YES completion:nil];
I have a method that is called when a Settings button is tapped in my root view, that subclasses two UIViewControllers, attaches them to a UITabBarController and pushes the UITabBarController onto a navigation stack:
-(IBAction)onSettings:(id)sender {
// Create the Settings Views
SettingsViewController *vcSettings1 = [[Settings1ViewController alloc] initWithNibName:#"Settings1ViewController" bundle:nil];
Settings2ViewController *vcSettings2 = [[Settings2ViewController alloc] initWithNibName:#"Settings2ViewController" bundle:nil];
// Create the Tab View
UITabBarController *tabController= [[UITabBarController alloc] init];
tabController.viewControllers = #[vcSettings1,vcSettings2];
// Pass the Index of the database on to the views so they can pull the record from the database
vcSettings.recordIndex = recordIndex;
vcSettings2.recordIndex = recordIndex;
// Add the tab bar controller to the navigation stack
[self.navigationController pushViewController:tabController animated:YES];
}
In each of the Settings views, I override the viewWillAppear method to load a row of data from an sqlite database at primary key recordIndex. (Both views pull the same record and display different data from the record, except one field is the same on both.)
I also override viewWillDisappear in each view to save the controls data back to the database.
I can verify that every time I switch views using the tab bar, the viewWillDisappear method is called on one closing view and the viewWillAppear is called on the opening view.
The problem is that when I change data on the first view and switch to the second view, the data is not changed on the second view unless I return to the first view and then back to the second. As best I can tell, here is what seems to be happening:
View 1 is open. I change the data in the field.
I tap on the tab for View 2
viewWillAppear is called for View 2, populating the field in View 2 with the old data in the database.
viewWillDisappear is then called for View 1, saving the changed data to the database.
It seems that the opening view is calling viewWillAppear BEFORE the closing view is calling viewWillDisappear.
I have tried this other ways, such as using a singleton, and simply trying to modify the recordIndex from both views and in all cases it seems that the data is loaded from the opening view before it is saved from the closing view.
Is this a bug in the way UITabBarController works, or am I abusing viewWillAppear and Disappear in a way that I'm not supposed to? Has anyone else run across this behavior?
This is not a good design. There is no guarantee that viewWillAppear of new view should be called after viewWillDisappear from the previous one. Even if that were to work, there would be no guarantee that this would keep working in future iOS versions.
If performance is not impacted, a quick fix could be to save the changes to the database as they occur, you would always have an up to date database that could be accessed from any view in any circumstance.
Best design is to have some model classes, with your model objects accessible through a singleton for example. Those model objects are updated real time as you interact with the UI, they are the ones being accessed by the different views, and they are periodically saved using the method of your choice.
as a cheap solution you could load data in viewDidAppear but in general I agree with JP's answer