I'm trying to mock and test UITableViewCells to make sure my configureCell:forIndexPath works correctly, except I can't get it to work using isKindOfClass but only conformsToProtocol. This would require all of my uitableviewcells to have it's own protocol and does not seem needed.
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
FeedObj *item = [_feedElements objectAtIndex:indexPath.row];
if( item.obj_type == FeedObjTypeFriendAdd ) {
MyTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:MyTableViewCellIdentifier forIndexPath:indexPath];
[self configureCell:cell forIndexPath:indexPath]
return cell;
} else if( item.obj_type = FeedObjTypeSomeOtherType ) {
// do another cell
}
}
- (void)configureCell:(UITableViewCell *)cell forIndexPath:(NSIndexPath *)indexPath
{
// only enters conditional in test if I do [cell conformsToProtocol:#protocol(SomeIndividualProtocolForEachTableViewcell)]
if( [cell isKindOfClass:[MyTableViewCell class]] ) {
// do the configuring
FeedObj *item = [_streamElements objectAtIndex:indexPath.row];
NSString *firstName = [item.obj_data objectForKey:#"first_name"];
NSString *lastName = [item.obj_data objectForKey:#"last_name"];
NSString *name = [NSString stringWithFormat:#"%# %#.", firstName, [lastName substringToIndex:1]];
NSString *text = [NSString stringWithFormat:#"%# has joined", name];
[((MyTableViewCell *)cell).messageLabel setText:text];
} else if( [cell isKindOfClass[SomeOtherTableView class]] ) {
// do other config
}
}
#implementation SampleTests
- (void)setUp
{
_controller = [[MySampleViewController alloc] init];
_tableViewMock = [OCMockObject niceMockForClass:[UITableView class]];
[_tableViewMock registerNib:[UINib nibWithNibName:#"MyTableViewCell" bundle:nil] forCellReuseIdentifier:MyTableViewCellIdentifier];
}
- (void)testFriendAddCell
{
FeedObj *friendAdd = [[FeedObj alloc] init];
friendAdd.obj_type = FeedObjTypeFriendAdd;
friendAdd.obj_data = [NSMutableDictionary dictionaryWithDictionary:#{ #"first_name" : #"firstname", #"last_name" : #"lastname" }];
_mockStreamElements = [NSMutableArray arrayWithObject:friendAdd];
[_controller setValue:_mockStreamElements forKey:#"_feedElements"];
NSIndexPath *indexPath = [NSIndexPath indexPathForRow:0 inSection:0];
[[[_tableViewMock expect] andReturn:[[[NSBundle mainBundle] loadNibNamed:#"MyTableViewCell" owner:self options:nil] lastObject]] dequeueReusableCellWithIdentifier:MyTableViewCellIdentifier forIndexPath:indexPath];
MyTableViewCell *cell = (MyTableViewCell *)[_controller tableView:_tableViewMock cellForRowAtIndexPath:indexPath];
STAssertNotNil( cell, #"should not be nil" );
STAssertTrue( [cell.messageLabel.text isEqualToString:#"firstname l. has joined"], #"should be equal" );
[_tableViewMock verify];
}
#end
I've also tried doing [[[mockCell stub] andReturnValue:OCMOCK_VALUE((BOOL) {YES})] isKindOfClass:[MyTableViewCell class]]] with a mockCell expect and it doesn't work either. Like this:
id mockCell = [OCMockObject partialMockForObject:[[[NSBundle mainBundle] loadNibNamed:#"MyTableViewCell" owner:self options:nil] lastObject]];
[[[mockCell stub] andReturnValue:OCMOCK_VALUE((BOOL) {YES})] isKindOfClass:[OCMConstraint isKindOfClass:[MyTableViewCell class]]];
[[[_tableViewMock expect] andReturn:mockCell] dequeueReusableCellWithIdentifier:MyTableViewCellIdentifier forIndexPath:indexPath];
I even tried with an OCMConstraint listed in http://blog.carbonfive.com/2009/02/17/custom-constraints-for-ocmock/.
Is there anyway to do this or do I have to use protocols for each tableviewcell? Thanks in advance
I'd strongly suggest you rethink how you are building out this implementation. For starters, a view controllers is great at managing a view, but managing your model data is not what it's for. Its good for passing around your model data to the views it manages, so with that in mind, let's build this out like that.
Let's start by introducing a new class, called FeedController. This controller's job is to sit between your VC and your model data backing this screen. Let's assume this public interface:
#interface FeedController : NSObject
- (instancetype)initWithFeedArray:(NSArray *)array;
- (NSString *)firstNameAtIndexPath:(NSIndexPath *)path;
- (NSString *)lastNameAtIndexPath:(NSIndexPath *)path;
- (NSString *)fullNameAtIndexPath:(NSIndexPath *)path;
// This should probably have a better name
- (NSString *)textAtIndexPath:(NSIndexPath *)path;
#end
I'm not going to implement these methods, but they'd look exactly like you'd expect. The initializer would copy the array passed into it, store it in an ivar, and the other methods would take the piece of info out of the array at the specific index and apply any custom transformations you must (like combining the first and last name to get the full name). The main goal here is to transfer the data, not manipulate it. The moment you try and manipulate this data in your view controller, is the moment you'll be back to square one, testing wise.
The object of your configureCell:forIndexPath: is now just to transfer data from the FeedController class, which is infinitely simple to test. No need to set up a responder chain, mock out objects, or anything. Just supply some fixture data and away you go.
You are still testing the pieces that make up your configureCell:forIndexPath: but not directly testing that method anymore. If you want to make sure that the view is being populated correctly, great, you should. However, you'll do this differently, this isn't a job for unit tests. Pull out UIAutomation or your favourite UI testing framework, and test your UI. Use the unit tests on the FeedController to test your data and transformations.
I hope this helps.
Related
When I searching and then select row that opens only the first letter (for example A.Others letters don't open. NSLog and breakpoint not helping. I don't understand what is the problem.
#synthesize propertyList, letters, filteredNames, searchController , arrayPlace;
- (void)viewDidLoad {
[super viewDidLoad];
............
filteredNames = [[NSMutableArray alloc]init];
searchController = [[UISearchController alloc]init];
self.searchController.searchResultsUpdater = self;
NSString *path = [[NSBundle mainBundle] pathForResource:#"names" ofType:#"plist"];
self.propertyList = [NSDictionary dictionaryWithContentsOfFile:path];
self.letters = [[self.propertyList allKeys] sortedArrayUsingSelector:#selector(compare:)];
}
#pragma mark - Table view data source
.......
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
static NSString *CellIdentifier = #"Cell";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier forIndexPath:indexPath];
cell.backgroundColor = [UIColor colorWithPatternImage:[UIImage imageNamed:#"cell bg1.png"]];
cell.accessoryType = UITableViewCellAccessoryDetailDisclosureButton;
if (tableView.tag == 1){
NSString *letter = self.letters[indexPath.section];;
NSArray *keyValues = [[self.propertyList[letter] allKeys] sortedArrayUsingSelector:#selector(compare:)];
cell.textLabel.text = keyValues[indexPath.row];
} else{
cell.textLabel.text = filteredNames[indexPath.row];
}
return cell;
}
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
UITableViewCell *cell = [tableView cellForRowAtIndexPath:indexPath];
NSString *keyTitle = cell.textLabel.text;
NSDictionary *peopleUnderLetter = [self.propertyList objectForKey:self.letters[indexPath.section]];
__block NSDictionary *selectedPerson = nil;
[peopleUnderLetter enumerateKeysAndObjectsUsingBlock:^(id _Nonnull key, id _Nonnull obj, BOOL * _Nonnull stop) {
if ([key isEqualToString:keyTitle]) {
selectedPerson = obj;
*stop = YES;
}
}];
if (selectedPerson) {
DetailViewController *vc = [self.storyboard instantiateViewControllerWithIdentifier:#"DetailViewController"];
// Push the view controller.
[self.navigationController pushViewController:vc animated:YES];
[vc setDictionaryGeter:selectedPerson];
}
}
And :
#pragma mark Search Display Delegate Methods
-(void)searchDisplayController:(UISearchController *)controller didLoadSearchResultsTableView:(UITableView *)tableView {
[tableView registerClass:[UITableViewCell class] forCellReuseIdentifier:#"Cell"];
}
-(BOOL)searchDisplayController:(UISearchController *)controller shouldReloadTableForSearchString:(NSString *)searchString
{
[filteredNames removeAllObjects];
if (searchString.length > 0) {
NSPredicate *predicate = [NSPredicate predicateWithFormat:#"SELF contains [search] %#", self.searchBar.text];
for (NSString *letter in letters) {
NSArray *matches = [[self.propertyList[letter] allKeys]filteredArrayUsingPredicate:predicate];
[filteredNames addObjectsFromArray:matches];
}
}
return YES;
}
Search bar fails and he does select row after searching
If you want more information just say it to me by answers and I will edit my question and then you will edit your answer
Please explain again clearly. You search using any alphabet, it shows the result which has only "A". Is this what you're trying to say ? If so, then remove the above code and try the below approach :-
Drag a search bar into the view controller and set its delegate to self (You'll find its property in the storyboard's delegate property
to the view controller).
Add UISearchBarDelegate in the .h file that will take care of automatically calling the appropriate methods of the search bar of
which the delegate is set to self.
Use the below method to detect the search. You can filter the NSArray here and reload the table.
-(void)searchBar:(UISearchBar *)searchBar textDidChange:(NSString *)searchText{
}
I'd recommend you to go through some basic tutorials about iOS development before getting deeper. All the best and I hope it helps you...
Screenshot
I am trying to build OS-X core data based app. In one of the entities, I am storing an URL ex. (www.somesite.com/somepage/someindex.php)
Using binding, I am successfully displaying the URL in the NSTableView. I would like however that URL to be clickable, and when clicked, browser to fire up and open the page. I have done some research, and I have found some solutions, for example:
Clickable url link in NSTextFieldCell inside NSTableView?
also:
https://developer.apple.com/library/mac/qa/qa1487/_index.html
but they both look outdated, first one is six years old, while the second is last updated on Jan. 2005
Anyone can provide easier & faster way how to achieve this? I didn't expected that I will have to write bunch of code just to make simple link to work to be honest... I am coming from web development world, where those kind of things can be sorted out withing few seconds, while here seems to be totally different story....
Any help will be appreciated.
John
You can use NSTextView and implement its delegate. There is a demo:
// MyCellView.h
#interface MyCellView : NSView
#property (nonatomic, strong) IBOutlet NSTextView *textView;
#end
// ViewController.m
- (void)viewDidLoad {
[super viewDidLoad];
self.tableView.delegate = self;
self.tableView.dataSource = self;
NSNib *nib = [[NSNib alloc] initWithNibNamed:#"MyCellView" bundle:[NSBundle mainBundle]];
[self.tableView registerNib:nib forIdentifier:#"MyCell"];
}
- (NSView *)tableView:(NSTableView *)tableView viewForTableColumn:(NSTableColumn *)tableColumn row:(NSInteger)row {
MyCellView *cell = (MyCellView *)[tableView makeViewWithIdentifier:#"MyCell" owner:self];
cell.textView.delegate = self;
[cell.textView.textStorage setAttributedString:[self makeLinkAttributedString:#"This is a test: www.somesite.com/somepage/someindex.php"]];
return cell;
}
- (NSAttributedString *)makeLinkAttributedString:(NSString *)string {
NSMutableAttributedString *linkedString = [[NSMutableAttributedString alloc] initWithString:string];
NSDataDetector *detector = [NSDataDetector dataDetectorWithTypes:NSTextCheckingTypeLink error:nil];
[detector enumerateMatchesInString:string options:0 range:NSMakeRange(0, string.length) usingBlock:^(NSTextCheckingResult *match, NSMatchingFlags flags, BOOL *stop) {
if (match.URL) {
NSDictionary *attributes = #{ NSLinkAttributeName: match.URL };
[linkedString addAttributes:attributes range:match.range];
}
}];
return [linkedString copy];
}
#pragma mark - NSTextViewDelegate methods
- (BOOL)textView:(NSTextView *)textView clickedOnLink:(id)link atIndex:(NSUInteger)charIndex {
// The click will be handled by you or the next responder.
return NO;
}
You can use TTTAttributedLabel in your tableviewcell. It supports powerful link detection.
I have made a table where depending on which cell you click on you will be sent into a new scene (detailviewcontroller). For example if you click on the cell with the text Thailand you will be sent to ThailandDetailViewController (scene). Everything works until you use the searchbar (look under - (void)tableView).
-When some countries get outfiltered (because of the searchfunction) the reaming countries will go higher in the table and acquire a lower count. Which leads to that they will lead to the wrong detailviewcontroller (scene).
A friend of mine said to me that I should use objectAtIndex within my array, didnt really catch what he meant with that.. And make a switch on the cell.textLabel.text (didnt really follow him)
Here is my .m file:
- (void)viewDidLoad
{
[super viewDidLoad];
self.mySearchBar.delegate = self;
self.myTableView.delegate = self;
self.myTableView.dataSource = self;
totalStrings = [[NSMutableArray alloc] initWithObjects:#"America",#"Austria",#"Canada",#"France",#"Germany",#"Greece",#"Malaysia",#"Mexico",#"Netherlands",#"Poland",#"Russia",#"Singapore",#"Thailand",#"Ukraine", nil];
}
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
switch (indexPath.row) {
case 0: [self performSegueWithIdentifier:#"Segue0" sender:self];
break;
case 1: [self performSegueWithIdentifier:#"Segue1" sender:self];
break;
//and so on
default: break;
}
}
-(void)searchBar:(UISearchBar *)searchBar textDidChange:(NSString *)searchText
{
if(searchText.length == 0){
isFiltered = NO;
}
else
{
isFiltered = YES;
filteredStrings = [[NSMutableArray alloc]init];
for (NSString *str in totalStrings){
NSRange stringRange = [str rangeOfString:searchText options:NSCaseInsensitiveSearch];
if(stringRange.location !=NSNotFound) {
[filteredStrings addObject:str];
}
}
}
[self.myTableView reloadData];
}
-(UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
static NSString *Cellidentifier = #"cell";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:Cellidentifier];
if (!cell) {
cell = [[UITableViewCell alloc]initWithStyle:UITableViewCellStyleDefault reuseIdentifier:Cellidentifier];
}
if (!isFiltered) {
cell.textLabel.text = [totalStrings objectAtIndex:indexPath.row];
}
else //if it's filtered
{
cell.textLabel.text = [filteredStrings objectAtIndex:indexPath.row];
}
return cell;
}
Big thank you in beforehand!!
Well, you can have a custom class to store the area and the segue index like this:
#interface SegueVO : NSObject
#property (nonatomic, strong) NSString *area;
#property int segueIndex;
-(id)initWithArea:(NSString *)area andSegueIndex:(int)index;
#end
#implementation SegueVO
-(id)initWithArea:(NSString *)area andSegueIndex:(int)index
{
self = [super init];
if (self)
{
self.area = area;
self.segueIndex = index;
}
return self;
}
#end
You will then store your ares in the totalStrings array like this:
[[NSMutableArray alloc] initWithObjects:[[SegueVO alloc] initWithArea:#"America" andIndex:0],....
Of course you can create a factory method to cut down on initialisation code.
Now you can work out what segue to activate like this:
NSArray *arrayToUse = totalStrings;
if (isFiltered)
arrayToUse = filteredStrings;
[self performSegueWithIdentifier:[#"Segue"
stringByAppendingString:[NSString stringWithFormat:#"%i",
[arrayToUse[indexPath.row].segueIndex]] sender:self];
Hope this helps.
You could easily solve this problem by storing a custom object in your table's data model instead of an NSString. That object would contain the label to display plus the name of the segue to activate once selected.
It's another question why you'd want a totally different view controller for different data. I suppose these are different kinds of data that need different ways to deal with them.
I have those two methods that provide title and a description for cell, declared in a protocol.
#protocol TableCellProtocol <NSObject>
#optional
#property(readonly,nonatomic,strong) NSString *titleForCell;
#property(readonly,nonatomic,strong) NSString *descriptionForCell;
#end
I have a NSManagedObject implementing that protocol and providing relevant methods:
-(NSString*)titleForCell {
return [NSString stringWithFormat:#"%# - %#",self.myVar1,self.myVar2];
}
-(NSString*)descriptionForCell {
return [NSString stringWithFormat:#"%# - %#",self.myVar3,self.myVar4];
}
where self.myVar<n> are CoreData attributes.
This protocol is meant to be used in a UITableViewCell:
- (UITableViewCell *)tableView:(UITableView *)table cellForRowAtIndexPath:(NSIndexPath *)indexPath {
id<TableCellProtocol> obj = [_fetchedResultsController objectAtIndexPath:indexPath];
MyTableViewCell *cell = [table dequeueReusableCellWithIdentifier:#"tableCell"];
cell.cellTitle.text=obj.titleForCell;
cell.cellDescr.text=obj.descriptionForCell;
return cell;
}
Although I am not having memory leak or allocation problems, I found this not really elegant, and quite poor for performance reason, because every time the cell is displayed a new NSString is created.
Also, the relevant attributes, may change during application lifecycle, so in case of storing title and description somewhere, I am in need to refresh them when needed.
I am using ARC in my project.
With regard to your comment above, I will post the use of transient attribute to store the custom attribute in NSManagedObject subclass. This approach is very efficient in terms of memory as the managed object context hold onto the object until the context is reset and also if some changes occur it automatically changes the attributes accordingly. In your case, you had to create the string each time you created or reused a cell. But, now since we cache the object, once the transient attribute is created, it is always there and it does not have any memory overhead. So it should be much faster.
You could implement the transient attribute as follows:
#implementation SomeManagedObject
+ (NSSet*)keyPathsForValuesAffectingValueForKey:(NSString *)key{
if([key isEqualToString:#"cellTitle"]){
return [NSSet setWithObjects:#"myVar1", #"myVar2", nil];
}else if([key isEqualToString: #"cellDescription"]){
return [NSSet setWithObjects:#"myVar3", #"myVar4", nil];
}
return [super keyPathsForValuesAffectingValueForKey:key];
}
// You could either override the getter for the transient attribute like this
- (NSString*)cellTitle{
if(!_cellTitle){
[sell willChangeValueForKey: #"cellTitle"];
_cellTitle = [NSString stringWithFormat:#"%# - %#",self.myVar1,self.myVar2];
[self didChangeValueForKey: #"cellTitle"];
}
return _cellTitle;
}
// You could also override the awakeFromFetch method to create the transient attribute as
- (void)awakeFromFetch{
[sell willChangeValueForKey: #"cellTitle"];
_cellTitle = [NSString stringWithFormat:#"%# - %#",self.myVar1,self.myVar2];
[self didChangeValueForKey: #"cellTitle"];
[super awakeFromFetch];
}
// Also use the willTurnIntoFault/didTurnIntoFault method as
- (void)didTurnIntoFault{
[super didTurnIntoFault];
[sell willChangeValueForKey: #"cellTitle"];
_cellTitle = [NSString stringWithFormat:#"%# - %#",self.myVar1,self.myVar2];
[self didChangeValueForKey: #"cellTitle"];
}
#end
I hope that helps. Thank you
Try this :
Instead of declaring two properties and defining their getter methods, you could define two methods that return String. And use this to set the title and description for your tableViewCell.
#protocol TableCellProtocol <NSObject>
#optional
-(NSString *) getTitleForCell;
-(NSString *) getDescriptionForCell;
#end
-(NSString*)getTitleForCell {
return [NSString stringWithFormat:#"%# - %#",self.myVar1,self.myVar2];
}
-(NSString*)getDescriptionForCell {
return [NSString stringWithFormat:#"%# - %#",self.myVar3,self.myVar4];
}
Let me know if this works for you.Thanks.
you have to reuse the cells in the tableview by checking if(cell==nil)
-(UITableViewCell *)tableView:(UITableView *)table cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
id<TableCellProtocol> obj = [_fetchedResultsController objectAtIndexPath:indexPath];
MyTableViewCell *cell = [table dequeueReusableCellWithIdentifier:#"tableCell"];
if (cell == nil) {
cell = [[UITableViewCell alloc]initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:#"cell"];
}
cell.cellTitle.text=obj.titleForCell;
cell.cellDescr.text=obj.descriptionForCell;
return cell;
}
So what I am trying to do is create a notepad style addition to my app.
All I want is for it to work exactly like apples existing notepad where you click the "add" button in the top right, then it creates a new note that you can write in and then when you click done it adds the note to a Cell in a UITableView.
I already have the UITableView and everything set up I just need to know how to run this action
-(IBAction)noteAdd:(id)sender{
}
And then when you click that button it does what I described above.
How would I go about doing this? I'm a little lost.
This Is How I am Adding the TableView to the scene, just By the way.
//tableview datasource delegate methods
-(NSInteger)numberOfSectionsInTableView:(UITableView *)tableView{
return 1;
}
-(NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section{
return cameraArray.count;
}
-(UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{
CustomCell *cell = [tableView dequeueReusableCellWithIdentifier:#"Cell"];
if(cell == nil){
cell = [[CustomCell alloc]initWithStyle:UITableViewCellStyleDefault reuseIdentifier:#"Cell"];
}
NSEnumerator *enumerator = [cameraArray objectEnumerator];
id anObject;
NSString *cellName = nil;
while (anObject = [enumerator nextObject]) {
cellName = anObject;
}
//static NSString *cellName = [cameraArray.objectAtIndex];
cell.textLabel.text = [NSString stringWithFormat:cellName];
return cell;
}
In UITableView
- (void)insertRowsAtIndexPaths:(NSArray *)indexPaths withRowAnimation:(UITableViewRowAnimation)animation
So you'd do something like
-(IBAction) noteAdd:(id)sender
{
NSIndexPath *newCellPath = [NSIndexPath indexPathForRow:cameraArray.count
inSection:0];
// I'm assuming cameraArray is declared mutable.
[cameraArray addObject:#"New item"];
[self.tableView insertRowsAtIndexPaths:#[newCellPath]
withRowAnimation:UITableViewRowAnimationFade];
}
While I'm at it, a few comments on your code:
I'm pretty sure this code:
NSEnumerator *enumerator = [cameraArray objectEnumerator];
id anObject;
NSString *cellName = nil;
while (anObject = [enumerator nextObject]) {
cellName = anObject;
}
is a rather roundabout way of getting the last string in the array. You could do that easier with cameraArray.lastObject. But I don't think that's what you want either, I think you're looking for
// XCode >= 4.5:
cellName = cameraArray[indexPath.row];
// XCode < 4.5:
cellName = [cameraArray objectAtIndex:indexPath.row];
And the next line:
cell.textLabel.text = [NSString stringWithFormat:cellName];
Best case, this creates an extraneous string. If the cell name happens to have a % in it, you'll almost certainly either get an error or an EXC_BAD_ACCESS. To fix that error you could use
cell.textLabel.text = [NSString stringWithFormat:#"%#", cellName];
but there's really no reason to. Just assign the string directly:
cell.textLabel.text = cellName;
Or if you insist on a copy:
cell.textLabel.text = [NSString stringWithString:cellName];
// OR
cell.textLabel.text = [[cellName copy] autorelease];
// OR