I am using a UICollectionView with a custom layout that lays out cells in a grid format. There can be well over 50 rows and 50 columns. Scrolling occurs both vertically and horizontally. Currently, I am doing all of the layout setup in prepareLayout and storing it in arrays:
- (void)prepareLayout {
NSMutableArray *newLayoutInfo = [[NSMutableArray alloc] init];
NSMutableArray *newLinearLayoutInfor = [[NSMutableArray alloc] init];
NSInteger sectionCount = [self.collectionView numberOfSections];
NSIndexPath *indexPath = [NSIndexPath indexPathForItem:0 inSection:0];
self.heightForRows = [delegate collectionViewHeightForAllRows];
self.totalWidthsForRows = [[NSMutableArray alloc] init];
for (int i = 0; i < sectionCount; i++) {
[self.totalWidthsForRows addObject:[NSNumber numberWithInt:0]];
}
for (NSInteger section = 0; section < sectionCount; section++) {
NSMutableArray *cellLayoutInfo = [[NSMutableArray alloc] init];
NSInteger itemCount = [self.collectionView numberOfItemsInSection:section];
for (NSInteger item = 0; item < itemCount; item++) {
indexPath = [NSIndexPath indexPathForItem:item inSection:section];
UICollectionViewLayoutAttributes *itemAttributes = [UICollectionViewLayoutAttributes layoutAttributesForCellWithIndexPath:indexPath];
itemAttributes.frame = [self frameForCellAtIndexPath:indexPath];
[cellLayoutInfo addObject:itemAttributes];
[newLinearLayoutInfor addObject:itemAttributes];
}
[newLayoutInfo addObject:cellLayoutInfo];
}
self.layoutInfo = newLayoutInfo;
self.linearLayoutInfo = newLinearLayoutInfor;
}
Then in layoutAttributesForElementsInRect I have:
- (NSArray*)layoutAttributesForElementsInRect:(CGRect)rect {
NSArray *rows = [self.linearLayoutInfo filteredArrayUsingPredicate:[NSPredicate predicateWithBlock:^BOOL(UICollectionViewLayoutAttributes *evaluatedObject, NSDictionary *bindings) {
return CGRectIntersectsRect(rect, [evaluatedObject frame]);
}]];
This works okay, but it is laggy and jumpy when I have over 50 columns and 50 rows. The problem I now have is that I must set
-(BOOL)shouldInvalidateLayoutForBoundsChange {
return YES;
}
This makes it prepare the entire layout every time the bounds change, which, needless to say, has a huge impact on performance and you can barely scroll. The cells consist of just text with an opaque background, so there is no issue there.
I am sure I am not doing this right and that there must be a better way. Thanks for the help in advance.
In custom flow layout I do this and it seems to help:
- (BOOL)shouldInvalidateLayoutForBoundsChange:(CGRect)newBounds {
return !(CGSizeEqualToSize(newBounds.size, self.collectionView.frame.size));
}
-(BOOL)shouldInvalidateLayoutForBoundsChange {
return YES;
}
Causes the layout to do prepareLayout() every time it scrolls, which means anything of heavy computing in prepare will lead to a laggy practice, so one possible direction to solve this is to check what's really taking much time. One possibility is what's inside
for (NSInteger section = 0; section < sectionCount; section++)
{
// generate attributes ...
}
in order to generate attributes for the layout. Every time it scrolls, every time this generalization reruns, so that it impacts on the scroll appear to be jumpy and clumsy. So in order to solve this issue, or at least sort out that this is not the really trouble, I suggest setting a flag in this layout algorithm, say, isScrolling, standing for the situation where the layout needs to prepare. Every time in prepareLayout() check the flag, if it is YES, then we'll know there's no need to do for loop to regenerate all the attributes, which alreay exsit ever since the first time the layout is initialised.
ok--I understand now. Here's what I recommend: create 3 collection views... one for the column headers (where each cell is column header), one for the row leaders (each cell = 1 row leader) and one collection view for your cells. Then when the scroll position of any collection view is changed by the user, update the scroll positions for the other 2 collection views as appropriate.
Related
I've made a quiz and would like for it to shuffle the answers to make it more challenging. This means that when there should be a method that can check which button is the answer. If answer is button 1 then button 1 contains the right answer. If answer is button 4 then button 4 contains the right answer etc.
Right now the code shuffles the answer each time I play a new game, but it sets the titles of all 4 buttons with the same title and the correct answer is always button 1.
Any ideas?
Philip
This is what I have.
- (IBAction)PlayHistory_Easy:(id)sender {
// The questions
history_Easy = [[NSMutableArray alloc] initWithObjects:kHisQ_E1, kHisQ_E2, kHisQ_E3, kHisQ_E4,nil];
numberOfQuestions = [history_Easy count];
index = arc4random() % numberOfQuestions--;
NSString *dd = [history_Easy objectAtIndex:index];
questionLabel.text = dd;
// Setup code to shuffle answers
NSMutableArray *randomizeAnswer = [[NSMutableArray alloc] initWithObjects:btnOne, btnTwo, btnThree, btnFour, nil];
NSMutableArray *ranAns = [[NSMutableArray alloc] initWithObjects:#"Answer 1", #"Answer 2", #"Answer 3", #"Answer 4", nil];
if ([questionLabel.text isEqualToString:kHisQ_E1]) { // Answer 1st button
for (UIButton *btn in randomizeAnswer) {
int i = arc4random() % ranAns.count;
NSString *str = [ranAns objectAtIndex:i];
[btnOne setTitle:[ranAns objectAtIndex:i] forState:UIControlStateNormal];
[btnTwo setTitle:[ranAns objectAtIndex:i] forState:UIControlStateNormal];
[btnThree setTitle:[ranAns objectAtIndex:i] forState:UIControlStateNormal];
[btnFour setTitle:[ranAns objectAtIndex:i] forState:UIControlStateNormal];
}
}
Check the answer. Here checking the answer is done manually, but in order to shuffle the answer, the code be different obviously.
- (IBAction)button1:(id)sender {
if ([questionLabel.text isEqualToString:kHisQ_E1]) {
[self playRIGHTAnswer_EASY];
}
}
- (IBAction)button2:(id)sender {
if ([questionLabel.text isEqualToString:kHisQ_E1]) {
[self playWRONGAnswer_EASY];
}
}
- (IBAction)button3:(id)sender {
if ([questionLabel.text isEqualToString:kHisQ_E1]) {
[self playWRONGAnswer_EASY];
}
}
- (IBAction)button4:(id)sender {
if ([questionLabel.text isEqualToString:kHisQ_E1]) {
[self playWRONGAnswer_EASY];
}
}
To all who is interested in this I provide an answer to my own question.
In trying to figure this out I thought that it would be easier to simply randomly reposition the buttons with each question. So I googled that and found this link:
how to random placement of UIButton and value
I only used part of the code since I already had created my buttons in IB (with outlets and actions properly connected) and so only needed the code below. Whenever you call this code it replaces the buttons according to integer values you put in the mutable array.
- (void)randomlyPositionAnswerButtons {
//Create an array with the rectangles used for the frames
NSMutableArray *indexArray = [NSMutableArray arrayWithObjects:
[NSValue valueWithCGRect:CGRectMake(28, 230, 260, 42)],
[NSValue valueWithCGRect:CGRectMake(28, 296, 260, 42)],
[NSValue valueWithCGRect:CGRectMake(28, 359, 260, 42)],
[NSValue valueWithCGRect:CGRectMake(28, 422, 260, 42)], nil];
//Randomize the array
NSUInteger count = [indexArray count];
for (NSUInteger i = 0; i < count; ++i) {
int nElements = count - i;
int n = (arc4random() % nElements) + i;
[indexArray exchangeObjectAtIndex:i withObjectAtIndex:n];
}
//Assign the frames
btnOne.frame = [((NSValue *)[indexArray objectAtIndex:0]) CGRectValue];
btnTwo.frame = [((NSValue *)[indexArray objectAtIndex:1]) CGRectValue];
btnThree.frame = [((NSValue *)[indexArray objectAtIndex:2]) CGRectValue];
btnFour.frame = [((NSValue *)[indexArray objectAtIndex:3]) CGRectValue];
}
You seem to have both logical errors and copy/paste errors.
Take a step back and separate your data store (the questions and answers) from the logic (the shuffling) and from the UI (the buttons). At the moment you're mixing all 3 of these things together and it's really messy.
Your data should be stored so that you have a list of items containing:
Question
Possible answers
Correct answer
When you shuffle, just change the orders of the possible answers (maybe in a mutable copy of the source list). There are many algorithms for shuffling, but when you iterate over the list you should generally be swapping two items each time.
Now, taking your shuffled answers you can set the value of each button. This should be trivial. Don't think about shuffling buttons...
When a button is pressed, get the answer associated with it and compare it to the correct answer. Done.
If you want to shuffle the questions as a whole, that should be done as a single stage before any of the above is started.
I have a MKMapView (obviously), that shows housing locations around the user.
I have a Radius tool that when a selection is made, the annotations should add/remove based on distance around the user.
I have it add/removing fine but for some reason the annotations won't show up until I zoom in or out.
This is the method that adds/removes the annotations based on distance. I have tried two different variations of the method.
Adds the new annotations to an array, then adds to the map by [mapView addAnnotations:NSArray].
Add the annotations as it finds them using [mapView addAnnotation:MKMapAnnotation];
1.
- (void)updateBasedDistance:(NSNumber *)distance {
//Setup increment for HUD animation loading
float hudIncrement = ( 1.0f / [[[[self appDelegate] rssParser]rssItems] count]);
//Remove all the current annotations from the map
[self._mapView removeAnnotations:self._mapView.annotations];
//Hold all the new annotations to add to map
NSMutableArray *tempAnnotations;
/*
I have an array that holds all the annotations on the map becuase
a lot of filtering/searching happens. So for memory reasons it is
more efficient to load annoations once then add/remove as needed.
*/
for (int i = 0; i < [annotations count]; i++) {
//Current annotations location
CLLocation *tempLoc = [[CLLocation alloc] initWithLatitude:[[annotations objectAtIndex:i] coordinate].latitude longitude:[[annotations objectAtIndex:i] coordinate].longitude];
//Distance of current annotaiton from user location converted to miles
CLLocationDistance miles = [self._mapView.userLocation.location distanceFromLocation:tempLoc] * 0.000621371192;
//If distance is less than user selection, add it to the map.
if (miles <= [distance floatValue]){
if (tempAnnotations == nil)
tempAnnotations = [[NSMutableArray alloc] init];
[tempAnnotations addObject:[annotations objectAtIndex:i]];
}
//For some reason, even with ARC, helps a little with memory consumption
tempLoc = nil;
//Update a progress HUD I use.
HUD.progress += hudIncrement;
}
//Add the new annotaitons to the map
if (tempAnnotations != nil)
[self._mapView addAnnotations:tempAnnotations];
}
2.
- (void)updateBasedDistance:(NSNumber *)distance {
//Setup increment for HUD animation loading
float hudIncrement = ( 1.0f / [[[[self appDelegate] rssParser]rssItems] count]);
//Remove all the current annotations from the map
[self._mapView removeAnnotations:self._mapView.annotations];
/*
I have an array that holds all the annotations on the map becuase
a lot of filtering/searching happens. So for memory reasons it is
more efficient to load annoations once then add/remove as needed.
*/
for (int i = 0; i < [annotations count]; i++) {
//Current annotations location
CLLocation *tempLoc = [[CLLocation alloc] initWithLatitude:[[annotations objectAtIndex:i] coordinate].latitude longitude:[[annotations objectAtIndex:i] coordinate].longitude];
//Distance of current annotaiton from user location converted to miles
CLLocationDistance miles = [self._mapView.userLocation.location distanceFromLocation:tempLoc] * 0.000621371192;
//If distance is less than user selection, add it to the map.
if (miles <= [distance floatValue])
[self._mapView addAnnotation:[annotations objectAtIndex:i]];
//For some reason, even with ARC, helps a little with memory consumption
tempLoc = nil;
//Update a progress HUD I use.
HUD.progress += hudIncrement;
}
}
I have also attempted at the end of the above method:
[self._mapView setNeedsDisplay];
[self._mapView setNeedsLayout];
Also, to force a refresh (saw somewhere it might work):
self._mapView.showsUserLocation = NO;
self._mapView.showsUserLocation = YES;
Any help would be very much appreciated and as always, thank you for taking the time to read.
I'm going to guess that updateBasedDistance: gets called from a background thread. Check with NSLog(#"Am I in the UI thread? %d", [NSThread isMainThread]);. If it's 0, then you should move the removeAnnotations: and addAnnotation: to a performSelectorOnMainThread: invocation, or with GCD blocks on the main thread.
I have already posted another question about this, but no one seemed to know how to do this.
I want my app to pick a random XIB file for me, but dont use the ones that have already been randomly picked.
So heres what i have set up as of right now, it seems to work, but i have to keep pressing the button over and over until it finds one that hasnt be used.
-(IBAction)continueAction:(id)sender{
random = arc4random() % 2;
if (random == 0 && usedQ2 == 0) {
Question_2 *Q2 = [[Question_2 alloc] initWithNibName:#"Question 2" bundle:nil];
Q2.modalTransitionStyle = UIModalTransitionStyleFlipHorizontal;
[self presentModalViewController:Q2 animated:YES];
[Q2 release];
}
else if (random == 1 && usedQ3 == 0) {
Question_3 *Q3 = [[Question_3 alloc] initWithNibName:#"Question 3" bundle:nil];
Q3.modalTransitionStyle = UIModalTransitionStyleFlipHorizontal;
[self presentModalViewController:Q3 animated:YES];
[Q3 release];
}
}
So as you can see i have it pick from a random number, and from their find the one that it matches.
Then you can see i have another part of my if statement that is checking to make sure it hasn't been used before.
each NIB file has its own usedQ(whatever Q it is), and when that Nib file is loaded it puts that usedQ as 1.
I think i could get by doing this, but in order to get rid of the constant button pushing, i will have to put loads of else statements with more else statements in them.
I have also tried running the
random = arc4random() % 2;
in a while statement and a for statement, i hoped that it would keep looking for a number until one that hasn't be used was found with no luck.
Any help? thanks!
Why don't you make a mutable array
and populate it with the names of all
your nibs.
Then read the count of the array and
generate a random number in that
range.
Extract the nib name at that index
and remove it from the array.
repeat steps 2-3.
//Setup your list at an appropriate place
NSMutableArray *nibs
= [[NSMutableArray alloc] initWithObjects: #"One Nib", #"Another
Nib", #"Last Nib", nil];
self.unusedNibs = nibs; //This should be a property you declare in
your header.
[nibs release];
-(IBAction)continueAction:(id)sender{
int random = arc4random() % [self.unusedNibs count];
NSString
*nibName = [self.unusedNibs objectAtIndex: random];
[self.unusedNibs removeObjectAtIndex:
random];
//Load nib here.
}
I have an IKImageBrowser setup which appears to be working well. I have set it up to allow reordering and also set animation to YES (in my awakeFromNib), however whenever I select and try and reorder the images I get strange behaviour:
1) They don't reorder most of the time
2) If they do they don't animate
3) Sometimes the images land on each other, if I scroll away and back they are back where they started.
If I highlight an image and delete it, it animates and disappears as expected...
Is this a problem with Core Animation? Do I need to add a core animation layer to the object in interface builder? I followed this tutorial from Apple to get these results: http://developer.apple.com/library/mac/#documentation/GraphicsImaging/Conceptual/ImageKitProgrammingGuide/ImageBrowser/ImageBrowser.html#//apple_ref/doc/uid/TP40004907-CH5-SW1
Here's the code in question:
- (BOOL) imageBrowser:(IKImageBrowserView *) aBrowser moveItemsAtIndexes: (NSIndexSet *)indexes toIndex:(NSUInteger)destinationIndex{
int index;
NSMutableArray *temporaryArray;
temporaryArray = [[[NSMutableArray alloc] init] autorelease];
for(index=[indexes lastIndex]; index != NSNotFound;
index = [indexes indexLessThanIndex:index])
{
if (index < destinationIndex)
destinationIndex --;
id obj = [mImages objectAtIndex:index];
[temporaryArray addObject:obj];
[mImages removeObjectAtIndex:index];
}
// Insert at the new destination
int n = [temporaryArray count];
for(index=0; index < n; index++){
[mImages insertObject:[temporaryArray objectAtIndex:index]
atIndex:destinationIndex];
}
return YES;
}
Interestingly, this line throws a warning
for(index=[indexes lastIndex]; index != NSNotFound;
comparison is always true due to
limited range of data type
As mentioned above by NSGod, changing int index to NSUInteger index solved the problem
I'm working on a roguelike using Objective-C/Cocoa to learn more. I've gotten most of the basic functionality out of the way, but I still have one problem I've been trying to figure out.
Here's a breakdown of the process:
First, the map is loaded:
NSString* mapPath = [[NSBundle mainBundle] pathForResource:mapFileName ofType:mapFileType];
NSURL* mapURL = [NSURL fileURLWithPath: mapPath];
currentMap_ = [[Map alloc] initWithContentsOfURL: mapURL];
worldArray = [[NSMutableArray alloc] init];
itemArray = [[NSMutableArray alloc] init];
[self populateMap];
return;
Then, in the populateMap function, it goes through each cell of the loaded map, using NSPoints and a loop, and creates objects based on the data from the map in WorldArray. For items, normal floor is put in where the item is, and an item is then made in itemArray. Both arrays are 30x30, as determined by the height of the map.
Here is the populateMap code:
- (void)populateMap
{
NSPoint location;
for ( location.y = 0; location.y < [currentMap_ height]; location.y++ )
{
for ( location.x = 0; location.x < [currentMap_ width]; location.x++ )
{
char mapData = [currentMap_ dataAtLocation: location];
for ( GameObject *thisObject in worldDictionary )
{
//NSLog(#"char: <%c>", [thisObject single]);
if ( mapData == [thisObject single])
{
NSString* world = [thisObject className];
//NSLog(#"(%#) object created",thisObject);
[self spawnObject:world atLocation:location];
}
}
for ( Item *thisObject in itemDictionary )
{
//NSLog(#"char: <%c>", [thisObject single]);
if ( mapData == [thisObject single] )
{
NSString* item = [thisObject className];
NSString* floor = [NormalFloor className];
//NSLog(#"(%#) object created",thisObject);
[self spawnItem:item atLocation:location];
[self spawnObject:floor atLocation:location];
}
}
if ( mapData == '1'
&& [player_ stepsTaken] <= 0)
{
//NSLog(#"player spawned at (%f, %f)",location.x,location.y);
player_ = [[Player alloc] initAtLocation: location];
}
if ( mapData == '1' )
{
//NSLog(#"floor created at (%f, %f)",location.x,location.y);
[worldArray addObject:[[NormalFloor alloc] initAtLocation: location]];
}
}
}
[self setNeedsDisplay:YES];
}
This is what is called when things are spawned:
- (void)spawnObject: (NSString*) object atLocation: (NSPoint) location
{
//NSLog(#"(%#) object created",thisObject);
[worldArray addObject:[[NSClassFromString(object) alloc] initAtLocation: location]];
}
- (void)spawnItem: (NSString*) item atLocation: (NSPoint) location
{
//NSLog(#"(%#) object created",thisObject);
[itemArray addObject:[[NSClassFromString(item) alloc] initAtLocation: location]];
}
worldArray and itemArray are what the game works on from that moment onwards, including the drawing. The player is inside of worldArray as well. I'm considering splitting the player into another array of characterArray, to make it easier when I add things like monsters in the not so distant future.
Now, when I load a new level, I had first considered methods like saving them to data and loading them later, or some sort of savestate function. Then I came to the realization that I would need to be able to get to everything at the same time, because things can still happen outside of the player's current scope, including being chased by monsters for multiple floors, and random teleports. So basically, I need to figure out a good way to store worldArray and itemArray in a way that I will be able to have levels of them, starting from 0 and going onward. I do need a savestate function, but there's no point touching that until I have this done, as you shouldn't actually be allowed to save your game in roguelikes.
So to reiterate, I need to have one set of these arrays per level, and I need to store them in a way that is easy for me to use. A system of numbers going from 0-upward are fine, but if I could use something more descriptive like a map name, that would be much better in the long run.
I've figured out my problem, I'm using an NSMutableDictionary for each and storing them with the keys that correspond to each level. Works like a charm. Bigger problems elsewhere now.
I figured it out, I'm using NSMutableDictionaries, one for each array (objects, items, eventually characters). They're stored using the name of the level. Works like a charm.