I have a UITableView with some names in it. I have built my app from the MasterViewController template that Apple provides. I'm trying to store the name of the selected cell in a NSString and then access it in the other class that handles the new ViewController that appears when the cell is tapped. In there I use that string as the title of the view.
In MasterViewController.h
#property (nonatomic, retain) NSString *theTitle;
In MasterViewController.m
#synthesize theTitle;
- (void)tableView: (UITableView*)tableview didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
UITableViewCell *cell = [tableView cellForRowAtIndexPath:indexPath animated:YES];
theTitle = cell.textLabel.text;
}
In the new ViewController.m
#import "MasterViewController.m"
- (void)viewDidLoad
{
MasterViewController* MasterViewControllerAccess = [[MasterViewController alloc] init];
self.title = MasterViewControllerAccess.theTitle;
NSLog("%#", [NSString stringWithFormat:#"%#", MasterViewControllerAccess.theTitle]);
}
The new ViewController is linked to the cell in the IB. When I press the cell theTitle returns NULL. But if I log it directly in the didSelectRowAtIndexPath:method it returns the real names. This means that something wrong occurs between the different classes. What's wrong?
You are instantiating a new instance of MasterViewController, instead you need to access the MasterViewController instance that already exists. Consider following Apple's example of setting the detail item (ie from master to detail). I can't see any reason to set it the way you are doing it. In any case, if you are using a navigation controller:
#import "MasterViewController.h" // don't import .m files. Always import .h files
- (void)viewDidLoad
{
MasterViewController* MasterViewControllerAccess = (MasterViewController*)self.navigationController.viewControllers[0]
self.title = MasterViewControllerAccess.theTitle;
NSLog("%#", [NSString stringWithFormat:#"%#", MasterViewControllerAccess.theTitle]);
}
In viewDidLoad of the pushed view controller, you allocate a new instance
of MasterViewController, which is completely different and unrelated to the
existing master view controller (which has been loaded from the storyboard or nib file).
Therefore MasterViewControllerAccess.theTitle is nil.
As said in the above comments, it is usually easier to pass the information the other way
around (from master to detail view controller), e.g. in prepareForSegue as in
the template application.
Related
I would like to ask some tips for a project I'm working on. I'm a Xcode beginner, so maybe is more easy than what I'm thinking.
So, the application I want to create shows a collection of data between two TableViews and it shows an image in a view controller at the end.
I've implemented a Property Lists to manage the data between the TableViews and the ViewController.
Now, here my problem, I would like to show (in the last ViewController) an HTML file (stored in my resource folder) rather than just an image. Can someone help me to write down the code for that? I've been able to write the code for the image so far, which is:
ViewController.h
#import <UIKit/UIKit.h>
#interface MethodsViewController : UIViewController
#property UIImage *bookCover;
#property IBOutlet UIImageView *bookCoverView;
#end
ViewController.m
#pragma mark -
#pragma mark View Life Cycle
- (void)viewDidLoad {
[super viewDidLoad];
if (self.bookCover) {
[self.bookCoverView setImage:self.bookCover];
}
}
#end
SecondTableView.m
prepare for segue
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
[tableView deselectRowAtIndexPath:indexPath animated:YES];
// Fetch Book Cover
NSDictionary *book = [self.books objectAtIndex:[indexPath row]];
self.bookCover = [UIImage imageNamed:[book objectForKey:#"Cover"]];
// Perform Segue
[self performSegueWithIdentifier:#"MethodsViewController" sender:self];
}
#end
Well, you need to deal with UIWebView for this. Add UIWebView property to your class:
#property (nonatomic, strong) UIWebView *aWebView;
Next, you need to represent content of your HTML file as an NSString, and it will be a content for the main page.
Lets just call this parameter as webText, in future we will load it into the web view, but first, - we need to get it.
To get webText parameter as an NSString, try the following code:
NSError* error = nil;
NSString *path = [[NSBundle mainBundle] pathForResource: #"nameOfYourHTMLfile" ofType: #"html"];
NSString *webText = [NSString stringWithContentsOfFile: path encoding:NSUTF8StringEncoding error: &error];
Set frame to aWebView in storyboard, then you can load HTML like this:
[self.aWebView loadHTMLString: webText baseURL: nil];
Posting a specific question/answer to a specific problem (unlike the general problems with this method I've seen elsewhere):
I have a UITableView, which is a part of a custom UITableViewController, which I setup and then add to a different view controller.
My UITableView is being loaded, calls all the appropriate setup methods (e.g. numberOfRowsInSection, numberOfSectionsInTableView, etc), but cellForRowAtIndexPath is never called.
I've confirmed that the dataset is being loaded - numberOfRowsInSection is not always zero.
What gives??
The goal appears to be the reuse of a table view's datasource. This can be accomplished by separating the datasource from the view controller. In outline, as follows:
// MyTableViewDatasource.h
#interface MyTableViewDatasource : NSObject <UITableViewDatasource>
#property(strong,nonatomic) NSMutableArray *array;
#end
// MyTableViewDatasource.m
#import "MyTableViewDatasource.h"
#implementation MyTableViewDatasource
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)s {
return self.array.count;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
// your cell config logic from the original view controller
// replace any mention of that vc's model array with self.array
}
#end
Now, say ViewControllerA has a tableView, and we want its datasource to be our newly defined datasource...
// ViewControllerA.m
#import "ViewControllerA.h"
#import "MyTableViewDatasource.h"
#interface ViewControllerA ()
#property(strong,nonatomic) MyTableViewDatasource *datasource;
#end
-(void)viewDidLoad {
// create our data and our datasource
// don't have to do this in viewDidLoad, but it needs to be done
// before the table can be seen, anytime after the model is ready
// this "model" in your case is whatever array that holds the data for the table
NSMutableArray *model = [#[#"Moe", #"Larry", #"Curly"] mutableCopy];
MyTableViewDatasource *datasource = [[MyTableViewDatasource alloc] init];
datasource.array = model;
self.tableView.datasource = datasource;
}
Now ViewControllerA, wherever it once modified its model array, should do the same this way...
[self.datasource.array addObject:#"Shemp"];
[self.tableView reloadData];
Hopefully it's clear that ViewControllerB and C and so on can do the same thing, replacing the code that you posted in your answer.
If you are using ARC, then it's very likely that your custom view controller, which is the ultimate owner of your UITableView, it being trashed as soon as you add the tableView to another view.
Try adding the UITableView's view controller to the master/other view controller's, either via a property or through the view hierarchy.
In my case, I simply created a new property for it in the view controller that wanted its table:
#property (strong, nonatomic) MyTableViewController *tvc;
and later assigned it to self when creating it:
self.tvc = [[MyTableViewController alloc] init];
[self.someOtherView addSubview:tvc.tableView];
So I have 2 different table views that use the same array (the array is originally created in the Role table view, the below one). How can I connect those two?
(Usually I use prepareForSegue to pass the data but since there is no segue, I'm not sure how can I do this)
EDIT 1: Add the location of the array.
What is a Model and why you need it
In most of the cases it's useless to pass data around if you don't have a Data Model. You can store your data using a technique called Data Persistence.
An example of a pattern you could use is MVC.
MVC or model-view controlelr is an software pattern widely using when making iOS Apps. In this architectural pattern your Controllers are a bridge between your View and your Model.
In this specific scenario both UITableViewControllers would use the same Model but they would display this data differently.
Persisting your Model
There are several ways to do that, the way I like the most is a little framework called CoreData, you can see this question for some reference on that.
You can also refer to this question to see the use of Singletons. But keep in mind that singletons alone do not persist the data. You'll have to add some sort of mechanism if you want the data to remain there between app sessions.
Persisting user preferences
The simplest way to store small chunks of data is using NSUserDefaults (but it's only meant to store defaults):
Let's assume you have an array
NSArray* testArray = #[#"first", #"second", #"third"];
You can set it to a key by using
[[NSUserDefaults standardUserDefaults] setObject:testArray forKey:#"myArray"];
You can sync NSUserDefaults using
[[NSUserDefaults standardUserDefaults] synchronize];
Then, anywhere in your app you can read it doing
[[NSUserDefaults standardUserDefaults] objectForKey:#"myArray"]
Passing data through the app
On the other hand you have to pass your data around somehow. To do so you can use formal protocols, specifically delegates.
As per the Apple documentation:
In a delegate-based model, the view controller defines a protocol for
its delegate to implement. The protocol defines methods that are
called by the view controller in response to specific actions, such as
taps in a Done button. The delegate is then responsible for
implementing these methods. For example, when a presented view
controller finishes its task, it sends a message to the presenting
view controller and that controller dismisses it.
Using delegation to manage interactions with other app objects has key
advantages over other techniques:
The delegate object has the opportunity to validate or incorporate
changes from the view controller.
The use of a delegate promotes
better encapsulation because the view controller does not have to know
anything about the class of the delegate. This enables you to reuse
that view controller in other parts of your app.
For more information on passing data through view controllers (the main point of this question) take a look at this SO answer.
You should never use data persistence just to pass data through the app. Neither user defaults nor core data.
Also using singletons is not good choice. All will mess up your memory.
Instead use call backs — either as delegates or blocks.
Or use unwind segues.
I explain delegates and unwind segues here: Passing row selection between view controllers
this example passes index paths, as it is appropriate in that situation, but the passed object might be of any type or size, as only pointers are passes.
if you use the NSUserDefaults on the other side, data is copied and written to the disk — there for data is copied and slowly processed — without any use.
I created a sample app how to pass data from one view controller to another view controller in another tab bar branch.
click to enlarge
TabBarController
We need to intercept the section of view controllers to set up some callback mechanism. In this case I am using blocks, but delegate would work as-well.
UITabController has a purely optional delegate. I create a subclass of UITabBarController to serv as it's own delegate, but actually a separate delegate should work in the same way.
#import "GameTabBarController.h"
#import "RoleViewController.h"
#interface GameTabBarController () <UITabBarControllerDelegate>
#end
#implementation GameTabBarController
-(void)viewDidLoad
{
[super viewDidLoad];
self.delegate = self;
}
-(BOOL)tabBarController:(UITabBarController *)tabBarController shouldSelectViewController:(UIViewController *)viewController
{
if ([viewController isKindOfClass:[UINavigationController class]]) {
UINavigationController *navController = (UINavigationController *)viewController;
if ([navController.topViewController isKindOfClass:[RoleViewController class]]) {
RoleViewController *rvc = (RoleViewController *)[navController topViewController];
[rvc setSelectedRole:^(Role *role) {
UIViewController *viewController = self.viewControllers[0];
[viewController setValue:role forKey:#"role"];
[self setSelectedIndex:0];
}];
}
}
return YES;
}
#end
I set the initial tab bar controller to this sub class
Role, RoleDatasource and RoleViewController
The RoleViewController displays a list of Roles, but the datasource and delegate for it's table view are a separate class that I add to the role view controller scene in the storyboard, where i also were it up.
Role
#interface Role : NSObject
#property (nonatomic,copy, readonly) NSString *name;
-(instancetype)initWithName:(NSString *)name;
#end
#import "Role.h"
#interface Role ()
#property (nonatomic,copy) NSString *name;
#end
#implementation Role
- (instancetype)initWithName:(NSString *)name
{
self = [super init];
if (self) {
_name = name;
}
return self;
}
#end
RoleDatasource
#import <UIKit/UIKit.h>
#class Role;
#interface RoleDatasource : NSObject <UITableViewDataSource, UITableViewDelegate>
#property (nonatomic, copy) void(^roleSelector)(Role *role);
#end
#import "RoleDatasource.h"
#import "Role.h"
#interface RoleDatasource ()
#property (nonatomic,strong) NSArray *roles;
#end
#implementation RoleDatasource
- (instancetype)init
{
self = [super init];
if (self) {
_roles = #[[[Role alloc] initWithName:#"Magician"], [[Role alloc] initWithName:#"Soldier"], [[Role alloc] initWithName:#"Maid"]];
}
return self;
}
-(NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
{
return 1;
}
-(NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
return self.roles.count;
}
-(UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
static NSString *identifier = #"RoleCell";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:identifier forIndexPath:indexPath];
cell.textLabel.text = [self.roles[indexPath.row] name];
return cell;
}
-(void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
self.roleSelector(self.roles[indexPath.row]);
}
#end
RoleViewController
#import <UIKit/UIKit.h>
#class Role;
#interface RoleViewController : UIViewController
#property (nonatomic, copy) void(^selectedRole)(Role *role);
#end
#import "RoleViewController.h"
#import "RoleDatasource.h"
#interface RoleViewController ()
#property (weak, nonatomic) IBOutlet UITableView *tableView;
#end
#implementation RoleViewController
- (void)viewDidLoad {
[super viewDidLoad];
RoleDatasource *roleDataSource = (RoleDatasource *)[self.tableView dataSource];
[roleDataSource setRoleSelector:^(Role *role) {
self.selectedRole(role);
}];
}
#end
PlayViewController
As soon as a role is selected on the role view controller we want to tell our tab bar controller to switch to the game view controller and show the selected role there, see the code for the tab bar controller.
The GameViewController is just a simple view controller subclass that has a property to hold a role and if a role is set, it will displays it name.
#import <UIKit/UIKit.h>
#class Role;
#interface PlayViewController : UIViewController
#property (nonatomic, strong) Role *role;
#end
#import "PlayViewController.h"
#import "Role.h"
#interface PlayViewController ()
#property (weak, nonatomic) IBOutlet UILabel *roleNameLabel;
#end
#implementation PlayViewController
- (void)viewWillAppear:(BOOL)animated {
[super viewWillAppear:animated];
self.roleNameLabel.text = (self.role) ? self.role.name : self.roleNameLabel.text;
}
#end
You'll find an example on github.
I think that I should put the array in the Tab bar Controller and connect it to the Role Table view (in order to maintain the behaviour like it is before) and connect it to my new Table view to do what I want to do.
The only problem I can think of is that since my program is small, adding this will not be a big problem. But if I have more vc, it's going to be so much pain.
I notice that Apple has what seems to be duplicate variable names:
2 properties and two ivars. Why does Apple do this?
//.h file
#interface TypeSelectionViewController : UITableViewController {
#private
Recipe *recipe;
NSArray *recipeTypes;
}
#property (nonatomic, retain) Recipe *recipe;
#property (nonatomic, retain, readonly) NSArray *recipeTypes;
And they then update the recipe instance below. Why have two variable with the same name?
Will one affect the recipe variable of the parentViewController since that recipe variable was set when presenting this view controller the code was in from the parentViewController?
//.m file
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
// If there was a previous selection, unset the accessory view for its cell.
NSManagedObject *currentType = recipe.type;
if (currentType != nil) {
NSInteger index = [recipeTypes indexOfObject:currentType];
NSIndexPath *selectionIndexPath = [NSIndexPath indexPathForRow:index inSection:0];
UITableViewCell *checkedCell = [tableView cellForRowAtIndexPath:selectionIndexPath];
checkedCell.accessoryType = UITableViewCellAccessoryNone;
}
// Set the checkmark accessory for the selected row.
[[tableView cellForRowAtIndexPath:indexPath] setAccessoryType:UITableViewCellAccessoryCheckmark];
// Update the type of the recipe instance
recipe.type = [recipeTypes objectAtIndex:indexPath.row];
// Deselect the row.
[tableView deselectRowAtIndexPath:indexPath animated:YES];
}
UPDATE 1
This code is from Apple's iPhoneCoreDataRecipes core data example:
First have a look at the RecipeViewController's didSelect delegate method, which will present the TypeSelectionViewController (child) view controller. Then have a look at that viewcontroller's didSelect delegate method where you will find the code implementation.
The reason I started looking at this is because I was interested how the parent's tableView cell got updated based on the selection in the ChildViewController in editing mode.
To see this for yourself, do the following:
Run the application
Select the Recipes tab
Click on a recipe - Chocolate Cake.
Click the edit button on the top right
Make note of the current category - should be on desert - then click on it.
Then you will be taken to the child view controller
Click on a different category, then click back and you will notice that the category button for that recipe has magically been updated. And I don't know how that's happening.
Does it have something to do with the private ivars and properties? which affects the parentViewController's cell?
My question i Guess is, how does selecting a category type in the child view controller's table affect the cell.text in the Parent View Controller's table? I can't see where the managedObjectcontext is saved in the child view controller for it to automatically update the parent View controller's cell text.
What you're seeing here is relatively old code, and there's not much need to do this anymore, thanks to Objective-C auto-synthesis.
Nowadays, when you issue a #property (nonatomic) NSArray *foo;, you implicitly get a #synthesize foo = _foo; in your implementation file and an instance variable declaration in your header. You don't see this, the compiler "inserts" it automatically. foo is the property and _foo is the instance variable. (In your above example, the #property and backing instance variable are both the same name, which could get confusing very quickly. With the foo property, you couldn't accidentally say self._foo, that doesn't exist. There's self.foo and _foo. With your example recipe is the ivar and self.recipe is the property. Very easy for one to quickly confuse the two when reading code.
Before the auto-synthesis, there was an intermediate step where you still needed a #synthesize, but you the backing instance variable was generated for you. These new features help you remove boilerplate code.
Answering Update 1
The code doing what you're wondering is in tableView:cellForRowAtIndexPath. There's nothing magical here. When you selected a new Category via the TypeSelectionViewController, the NSManagedObject is updated. Back in the RecipeDetailViewController, cellForRowAtIndexPath pulls the lasted information from CoreData. text = [recipe.type valueForKey:#"name"];
You might be getting confused about what an #property really is. It's just syntactic sugar. A #property these days automatically creates accessor and mutator methods and a backing ivar. Properties themselves aren't areas to store data, it's just a quick way of generating some methods and backing ivars.
Example
#interface MyClass
{
NSUInteger _foo;
}
#end
#implementation MyClass
- (NSUInteger)foo
{
return (_foo)
}
- (void)setFoo:(NSUInteger)newFoo
{
_foo = newFoo;
}
#end
is equivalent to:
#interface MyClass
#property (nonatomic, assign) NSUInteger foo;
#end
You save a lot of typing. When you get into things like NSString properties and different property modifiers like strong or copy, the amount of code you save (and memory management mistakes you avoid) in the mutators becomes much greater.
Your .h file should be your public api. You can re-declare your properties in your .m, implementation file, which are also considered private. For example
.h
#interface MyViewController : UITableViewController
#property (readonly) NSString *name;
#end
.m
#implementation MyViewController
#property (readwrite) NSString *name
#end
Here we are declaring a public name property that is readonly and in your implementation you 're re-declaring the property so that you can use the setter accessor.
UPDATE: I decided to start over since I was still in the early stages of this app. I repeated everything and for whatever reason, the custom cell took the second time around. I will keep the old files around to confirm another answer, as I imagine I am not the only one who will have this problem.
I am building a tabbed application that requires custom cells in its table views. I have done this a few times and I always seem to hit a speed bump when hooking up these custom cells. The app booted up fine until I started using the custom cell in my table view controller titled SongsTVC. I am receiving a termination with a reason of:
[<SongsTVC 0x6831330> setValue:forUndefinedKey:]:
this class is not key value coding-compliant for the key albumLabel.
I am using this tutorial and have used it before (changed a few things for ARC and iOS 5) with success. In fact, the code and IB layout I am using is based off of an already working project I have. I am aware of this error commonly presenting itself when you hook up your outlets to the file's owner and not the cell itself. I am not making this mistake but it is still giving me this error. So far, I have removed the label it has a problem with and even deleted the cell's files entirely in order to start over. Any help would be appreciated.
SongCell.h
#import <UIKit/UIKit.h>
#interface SongCell : UITableViewCell{
}
#property(nonatomic, assign) IBOutlet UILabel *titleLabel;
#property(nonatomic, assign) IBOutlet UILabel *artistLabel;
#property(nonatomic, assign) IBOutlet UILabel *albumLabel;
#end
SongCell.m
#import "SongCell.h"
#interface SongCell ()
#end
#implementation SongCell
#synthesize titleLabel, artistLabel, albumLabel;
#end
SongsTVC.h - Header of the TableViewController
#import <UIKit/UIKit.h>
#interface SongsTVC : UITableViewController{
UINib *cellLoader;
}
#end
SongsTVC.m - Relevant TableViewController methods
#import "SongsTVC.h"
#import "SongCell.h"
#interface SongsTVC ()
#end
static NSString *CellClassName = #"SongCell";
#implementation SongsTVC
- (id)initWithStyle:(UITableViewStyle)style
{
self = [super initWithStyle:style];
if (self) {
// Custom initialization
cellLoader = [UINib nibWithNibName:CellClassName bundle:[NSBundle mainBundle]];
}
return self;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
SongCell *cell = (SongCell *)[tableView dequeueReusableCellWithIdentifier:CellClassName];
if (!cell)
{
//CRASH ERROR POINTS HERE
NSArray *topLevelItems = [cellLoader instantiateWithOwner:self options:nil];
cell = [topLevelItems objectAtIndex:0];
}
// Configure the cell...
cell.titleLabel.text = #"SONG";
cell.artistLabel.text = #"Artist";
cell.albumLabel.text = #"Album";
return cell;
}
Interface Builder
NOTE: The cell identifier has been set to "SongCell" in IB and the file owner is UITableViewController because multiple tables will be using this cell. As long as the view is a table, it should work (it has in the past).
UPDATE: The xib file in XML format has been pasted here.
I had this exact problem today, while making a sample project for another SO answer! It looks like your xib is trying to connect an outlet to your view controller instead of your cell. In your screenshot, the outlets look correctly defined, but occasionally an old outlet definition can get left in the underlying XML and cause this type of crash.
If you've changed the files owner class after connecting some outlets, for example, this could confuse it.
You may be able to find it by opening the xib as "source code", look for the element and check there are only the entries you expect. Perhaps search the XML file for albumLabel as well.
If that doesn't work, you may have to scrap the xib and start again.
Unfortunately, the only solution that I have found was to start over. I did everything exactly as I did before and it worked the second time around. It was quite a chore having to scrap the entire thing and start over but it was the only way I could get it to work. I'm leaving this as the answer unless somebody can figure out what happened. (See original post and its update)
I had a similar issue and was able to solve it by rebuilding the Storyboard/NIB.