I have a UISegmentControl with my UITableView that sorts the data. I'd like to be able to do things:
(1) default sort (so when the user turns on the app for the first time, it would select the first segment, and sort by that action)
(2) remember where the user was between table loads. What I mean by this is, similar to Apple's coverflow, when I go to a different cover, the UITableView repopulates. So if the last time the user was there, the sort was on the 3rd segment, then it would remember that.
I'm a bit new to object-oriented design, and this was my best guess to not have the same redundant code everywhere: (MarkersList is a NSMutableArray)
- (NSArray *)sortByName:(NSArray *)sortDescriptors {
return [self.MarkersList sortedArrayUsingDescriptors:sortDescriptors];
}
- (NSArray *)sortByRSID:(NSArray *)sortDescriptors {
return [self.MarkersList sortedArrayUsingDescriptors:sortDescriptors];
}
- (void)setSortedMarkersList:(NSArray *)sortedArray {
if (self.MarkersList != nil) {
[self.MarkersList removeAllObjects];
}
[self.MarkersList addObjectsFromArray:sortedArray];
}
- (IBAction)sortButtonPressed:(UISegmentedControl *)segmentControl {
// Create sort descriptors
NSSortDescriptor *nameDescriptor = [[[NSSortDescriptor alloc] initWithKey:#"Name" ascending:YES selector:#selector(localizedCaseInsensitiveCompare:)] autorelease];
NSSortDescriptor *rsID = [[NSSortDescriptor alloc] initWithKey:#"ID" ascending:YES];
if ([segmentControl selectedSegmentIndex] == NAME) { // this is #define 0
NSArray *sortDescriptors = [[NSArray alloc] initWithObjects:nameDescriptor, rsIDDescriptor, nil];
NSArray *sortedArray = [self sortByGene:sortDescriptors];
[self setSortedMarkersList:sortedArray];
[sortDescriptors release];
}
else if ([segmentControl selectedSegmentIndex] == RS_ID) {
NSArray *sortDescriptors = [[NSArray alloc] initWithObjects:rsIDDescriptor, resultDescriptor, nameDescriptor, nil];
NSArray *sortedArray = [self sortByRSID:sortDescriptors];
[sortDescriptors release];
[self setSortedMarkersList:sortedArray];
}
[self.MarkersTableView reloadData];
}
I haven't implemented the third sort yet since it's not just an NSString or NSNumber like the other two yet. So far, I think it works correctly. However, the problem I have is to implement (1), I would need to call sortByName when my table is loaded. I could just create the NSSortDescriptors again, but that seems redundant. Is there a more OOD way to achieve this?
For (2), I'm guessing I could save that index for the table in a dictionary, and retrieve it when that table is loaded. Or something along those lines, not really sure.
Any help is greatly appreciated. Thanks!
This isn't so much an OO question, as iOS doesn't always let us use best practices to solve a problem. The best bet for your situation, I think, is to store the information on the selected sort in the NSUserDefaults. In your -viewDidLoad method, check if the selected sort object exists in NSUserDefaults, use it if it does, and if not choose a reasonable default value.
Don't worry about creating NSSortDescriptors with each load of the application, unless you have done profiling and determined that a large amount of time is spent building it. Serializing and deserializing the NSSortDescriptors would be far more inefficient than just recreating it when needed. Apple spends a lot of time optimizing frequently used classes like NSSortDescriptor.
Regarding #2, you can use indexPathsOfVisibleRows on the UITableView to get an array of visible indices, store the first one in UserDefaults, then on load (or pop from the next view controller if that occurs) call –scrollToRowAtIndexPath:atScrollPosition:animated:.
Once you're code is functioning, I recommend you watch the various videos on iTunes U regarding profiling and Instruments. It's a wonderful tool that is often overlooked, and really helps concentrate effort where it's needed.
Related
I'm getting through the Fall 2010 version of the Stanford class CS193P, iPhone programming. On assignment 2 I'm improving upon a calculator app created in assignment one. It seems that I'm almost finished but the app crashes when I try to press a variable located on the interface (for these purposes, "x").
Using my limited debugging skills, I managed to track the problem down. The problem comes in the method "(NSSet)variablesInExpression:(id)anExpression".
+ (NSSet *)variablesInExpression:(id)anExpression
{
NSMutableSet *setOfVariables = [[NSSet alloc] init];
for (NSString *str in anExpression) {
if ([str hasPrefix:VARIABLE_PREFIX]) {
[setOfVariables addObject:str];
}
}
[setOfVariables autorelease];
return setOfVariables;
}
When I get to the line
[setOfVariables addObject:str];
the app crashes. I've been trying to figure it out for a couple hours, please help! Is there a way in XCode to see the entire list of values in 'anExpression'?
Although you declare your variable as mutable set you create instance of immutable NSSet class - you must create NSMutableSet instance:
NSMutableSet *setOfVariables = [[NSMutableSet alloc] init];
I have a a game for the iphone where the tags of 32 buttons are vital. I am trying to make a feature where it would save these 32 integers into an array so you can play a different game on the board with the same integers(tags) and then later come back to your game with the integers in the same way.(Not asking about multitasking or anything like that) There are some questions like this but none of their answers seem to work.
Right now I have:
savedGame = [NSArray arrayWithObjects: [NSNumber numberWithInt:a2.tag],
[NSNumber numberWithInt:a4.tag],
[NSNumber numberWithInt:a6.tag],
[NSNumber numberWithInt:a8.tag],
[NSNumber numberWithInt:b1.tag],
[NSNumber numberWithInt:b3.tag],
[NSNumber numberWithInt:b5.tag],
[NSNumber numberWithInt:b7.tag],
[NSNumber numberWithInt:c2.tag],
[NSNumber numberWithInt:c4.tag],
all the way to:
[NSNumber numberWithInt:h7.tag],nil];
for the part where you save the game
and:
a2.tag = [((NSNumber*)[savedGame objectAtIndex:0]) intValue];
all the way up to:
h7.tag = [((NSNumber*)[savedGame objectAtIndex:32]) intValue];
at the part where you resume your game. However, this code is apparently SO BAD that whenever it gets to this code in the ios simulator it crashes XCODE TOO(which gets very annoying because I need to force quit and start the program again)(its xcode 4)
Saved game is created in the .h like:
NSArray *savedGame;
I guess I will have to add something where it checks if there are 32 numbers in that second part--so if you could add that in your answer that would be great--and thanks in advance! I think the problem is with the second part of that code-- the
g2.tag = [((NSNumber*)[savedGame objectAtIndex:25]) intValue];
I have also heard of NS Mutable Arrays, so maybe that could solve my problem?
Thanks for any and all help!! And I am also open to new ways--maybe not arrays--that could solve this problem!! Thankyou in advance!!!
Use an NSMutableArray instead and you can do things like
[savedGame replaceObjectAtIndex:4 withObject:[NSNumber numberWithInt:a8.tag]];
You can also save these in NSUserDefaults:
NSUserDefaults *prefs = [NSUserDefaults standardUserDefaults];
[prefs setInteger:a4.tag forKey#"a4.tag"];
a4.tag = [prefs integerForKey:#"a4.tag"];
This latter approach will also allow you to use a for loop to set and retrieve these values.
You should really use an NSDictionary for this job.
http://developer.apple.com/library/mac/#documentation/Cocoa/Reference/Foundation/Classes/NSDictionary_Class/Reference/Reference.html
This will allow you to query your data using keys.
If you are suspecting that you could be accessing your array beyond its size, then you could check if the array has got the number of objects you expect by inspecting [savedGames count].
Don't know if it helps.
As a general suggestion, I would first run your unmodified program in debugging mode (if you are not doing that already), and possibly step-by-step in the region where it fails. So you will know exactly which is the offending statement.
In order to further try and understand what is happening, I would split your assignments in multiple lines:
NSNumber* number = [savedGame objectAtIndex:0];
a2.tag = [number intValue];
if you set a breakpoint on the first statement you can see what objectAtIndex is returning.
Of course I understand it is big work to make all those changes.
As an alternative approach, since it seems to me that what matters to you are the ints (not the NSNumbers, you are using those only to store the ints somewhere), you could simply resort to using a C array of longs. This would avoid all the conversions back and forth. Of course, I am not sure whether you are really only interested in the ints.
Ummmm, based on the way your tags are situated, i guess its some kind of checkers like game? As in you have a 64 square grid but only half of the spaces are playable.
As for your comment about a mutable array, a mutable array just allows you to dynamically add or remove objects to the array. A regular NSArray has a fixed size and components (you cant add to it or remove from it). So you dont need to do the initWithObjects: part with a mutable array.
But, I really think you should restructure the way you are doing this. In this situation, you would want an object that has a location. Then you can just do
#interface GamePiece : NSObject
{
CGPoint location; //an object with an x and y
}
#property (assign, readwrite) CGPoint location;
-(void)saveTheGame {
[self setSavedGame:[[[NSMutableArray alloc] init] autorelease]];
for(GamePiece *piece in allPieces)
{
[savedGame addObject:piece]
}
}
Then in loadGame
if([savedGame count] == 32)
{
[self setAllPieces:nil]; //allPieces is an array object of a "Game" that contains
//references to all the pieces on the board
for(GamePiece *piece in savedGame)
{
[allPieces addObject:piece];
}
}
I want to provide custom sorting using NSFetchedResultsController and NSSortDescriptor.
As custom sorting via NSSortDescriptor message -(id)initWithKey:ascending:selector: is not possible (see here), I tried to use a NSSortDescriptor derived class in order to override the compareObject:toObject: message.
My problem is that the compareObject:toObject: is not always called. It seems that it is called only when the data are already in memory. There is an optimization of some sort that use a database based sort instead of the compareObject:toObject when the data are retrieved from the store the first time. (see here).
My question is : how to force NSFetchedResultscontroller to use the compareObject:toObject: message to sort the data ? (and will it work with large data set)
One solution is to use a binary store instead of a sqlite store but I don't want to do that.
Another solution is:
-call performFetch to sort data via SQL (compareObject not called)
-make a modification to the data and reverse it.
-call performFetch again (compareObject is called)
It does work in my case but it's a hack and I am not sure it will always work (especially with large data set (greater than the batch size)).
UPDATED:You can reproduce with the CoreDataBooks sample.
In RootViewController.m, add this ugly hack:
- (void)viewWillAppear:(BOOL)animated {
Book* book = (Book *)[NSEntityDescription insertNewObjectForEntityForName:#"Book"
inManagedObjectContext:[self fetchedResultsController].managedObjectContext];
[[self fetchedResultsController] performFetch:nil];
[[self fetchedResultsController].managedObjectContext deleteObject:book];
[self.tableView reloadData];
}
In RootViewController.m, replace the sort descriptor code with:
MySortDescriptor *myDescriptor = [[MySortDescriptor alloc] init];
NSArray *sortDescriptors = [[NSArray alloc] initWithObjects:myDescriptor, nil];
[fetchRequest setSortDescriptors:sortDescriptors];
Add MySortDescriptor class:
#implementation MySortDescriptor
-(id)init
{
if (self = [super initWithKey:#"title" ascending:YES selector:#selector(compare:)])
{
}
return self;
}
- (NSComparisonResult)compareObject:(id)object1 toObject:(id)object2
{
//set a breakpoint here
return [[object1 valueForKey:#"author" ] localizedCaseInsensitiveCompare:[object2 valueForKey:#"author" ] ];
}
//various overrides inspired by [this blog post][3]
- (id)copy
{
return [self copyWithZone:nil ];
}
- (id)mutableCopy
{
return [self copyWithZone:nil ];
}
- (id)mutableCopyWithZone:(NSZone *)zone
{
return [self copyWithZone:zone ];
}
- (id)copyWithZone:(NSZone*)zone
{
return [[MySortDescriptor alloc] initWithKey:[self key] ascending:[self ascending] selector:[self selector]];
}
- (id)reversedSortDescriptor
{
return [[[MySortDescriptor alloc] initWithKey:[self key] ascending:![self ascending] selector:[self selector]] autorelease];
}
#end
In reference to your question and the comments. You are going to need to pull the objects into memory to sort them. Once they are in memory you can use a convenience method to determine distance from a point.
To decrease the number of objects you pull into memory you could calculate max and min values and then filter on those, reducing the radius of your search before you sort.
It is not possible to sort on a calculated value unless it is in memory.
Sorry if I missed anything about this but I have a table view with 2 large sections and an index to navigate between sections:
- (NSArray *)sectionIndexTitlesForTableView:(UITableView *)tableView {
NSMutableArray *listArray = [[NSMutableArray alloc] init];
listArray = [NSArray arrayWithArray:[#"S|H"componentsSeparatedByString:#"|"]];
return listArray;}
Since I only have 2 sections the top index "S" is at the top and "H" is at the extreme bottom of the screen.
Is there any way to reposition those two index letters to be located at the center? (or at least near each other)
Thanks
I don't think so - positioning the labels is really up to the OS, and finally it meets user expectation.
Your code leaks an NSMutableArray though, as you first alloc/init an array, and then reassign the variable to another array.
Also, arrayWithArray: is overkill here.
Make it
NSArray *listArray = [#"S|H"componentsSeparatedByString:#"|"];
or better yet
NSArray *listArray = [NSArray arrayWithObjects:#"S", #"H", nil];
I have a switch statement similar to this one:
switch (number)
{
case 1:
if (imageView1.hidden == NO)
{
imageView1.hidden = YES;
} else
{
imageView1.hidden = NO;
}
break;
case 2:
if (imageView2.hidden == NO)
{
imageView2.hidden = YES;
} else
{
imageView2.hidden = NO;
}
break;
And so forth and so on.
My question is how do I use a string with a value say "imageView1" and use that to access the instance of my imageView class instead of having a different case for each instance of imageView? I know it muse be similar to creating an NSPath from a string or something like that, but I'm just not sure where to look or what it would be called.
Thanks in advance for any help!
I don't disagree with those who are concerned about the design, if this is actually the code. I will assume, however, that you are only posting a generalized version of your question. And since this is an important concept in Objective-C, so we should talk about it.
You can access an object's properties by name using Key Value coding, and the routine -valueWithKey:.
NSString *nameOfView = #"imageView1";
[[self valueForKey:nameOfView] setHidden:YES];
This will, in order, look for a method called -imageView1, an ivar named imageView1 and finally an ivar named _imageView1. This technique is very heavily used in Cocoa, and is important to understand. This is one of the many reasons we name things carefully, and yet another reason that we make accessors that handle memory management for us. Search the docs for "Key-Value Compliance" for more information.
Now for this specific case, I would tend towards something more like JimG's solution, using an NSArray of views, so I can loop through them and turn on or off the ones I want based on their index. But I can imagine a lot of cases where that wouldn't be appropriate, and KVC may be.
Why not put the instances in an NSArray and index into that?
NSArray *views = [NSArray arrayWithObjects: imageView1, imageView2, nil];
NSImageView *iview = [views objectAtIndex: number];
Also, you could consider something like:
iview.hidden = ! iview.hidden;
[Edit: missing asterisks, oops]