Trying to learn by digging through some code.
Have an accessory button, when its pressed it loads up a menu so the user can pick either a picture from photo library or video (works perfect) have also a sticker menu that pops up similarly to the photo library. The sticker pops up accordingly, only have one if that matters. However when I select it nothing happens. The view is dismissed and it should be sent however it is not.v THE NSLog prints my check statement, I have even added the JSQ sound to play and it does it as well. Working with Firebase/Parse.
It seems to call everything just doesn't attach the PNG from the grid view to the message.
StickerView.m
- (void)collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPath:(NSIndexPath *)indexPath
{
[collectionView deselectItemAtIndexPath:indexPath animated:YES];
NSString *file = stickers2[indexPath.item];
NSString *sticker = [file stringByReplacingOccurrencesOfString:#"#2x.png" withString:#""];
if (delegate != nil) [delegate didSelectSticker:sticker];
NSLog(#"Sticker Sent");
[self dismissViewControllerAnimated:YES completion:nil];
}
Chat.m
- (void)didSelectSticker:(NSString *)sticker
{
[self messageSend:sticker Video:nil Picture:nil Audio:nil];
}
messageSend
- (void)messageSend:(NSString *)text Video:(NSURL *)video Picture:(UIImage *)picture Audio:(NSString *)audio
{
Outgoing *outgoing = [[Outgoing alloc] initWith:groupId View:self.navigationController.view];
[outgoing send:text Video:video Picture:picture Audio:audio Sticker:sticker];
[JSQSystemSoundPlayer jsq_playMessageSentSound];
[self finishSendingMessage];
}
LOG
NSString *file = stickers2[indexPath.item];
NSLog (#"%d",indexPath.item);
NSLog (#"%#",file);
NSString *sticker = [file stringByReplacingOccurrencesOfString:#"#2x.png" withString:#"#2x.png"];
NSLog (#"%#",sticker);
if (delegate != nil) [delegate didSelectSticker:sticker];
NSLog(#"Sticker Sent");
[self dismissViewControllerAnimated:YES completion:nil];
indexPath returns a numerical value based on which sticker is selected. These are loaded into the mutableArray based on file name. If the file name contains a string of #"foo" it is loaded into the array. it could be one or one million value based on how many are loaded in. However it does correspond correctly to the selections. One is properly returned when object one is selected and 2 is properly selected and so on.
File name is returned as the correct file name.
Sticker was returned incorrectly based on your suspicious line of code, I have replaced it to not removed the extensions but did not delete it as of yet. Now returning the same value as File.
Outgoing
- (void)send:(NSString *)text Video:(NSURL *)video Picture:(UIImage *)picture Audio:(NSString *)audio Sticker:(NSString *)sticker
{
NSMutableDictionary *item = [[NSMutableDictionary alloc] init];
item[#"userId"] = [PFUser currentId];
item[#"name"] = [PFUser currentName];
item[#"date"] = Date2String([NSDate date]);
item[#"status"] = TEXT_DELIVERED;
item[#"video"] = item[#"thumbnail"] = item[#"picture"] = item[#"audio"] = item[#"latitude"] = item[#"longitude"] = #"";
item[#"video_duration"] = item[#"audio_duration"] = #0;
item[#"picture_width"] = item[#"picture_height"] = #0;
if (text != nil) [self sendTextMessage:item Text:text];
else if (video != nil) [self sendVideoMessage:item Video:video];
else if (picture != nil) [self sendPictureMessage:item Picture:picture];
else if (audio != nil) [self sendAudioMessage:item Audio:audio];
else if (sticker !=nil) [self sendSticker:item Sticker:sticker];
else [self sendLoactionMessage:item];
}
sendSticker
- (void)sendSticker:(NSMutableDictionary *)item Sticker:(NSString *)sticker
{
item[#"sticker"] = sticker;
NSLog(#"%#",sticker);
[self sendMessage:sticker]
}
NSLog never gets called so I know that this is not getting called. I must be missing something, after the user selects the sticker.
Sticker.h
#protocol StickersDelegate
- (void)didSelectSticker:(NSString *)sticker;
#end
#interface StickersView : UIViewController <UICollectionViewDataSource, UICollectionViewDelegate>
#property (nonatomic, assign) IBOutlet id<StickersDelegate>delegate;
#end
I see a couple of potential problem areas, but nothing definitive. I'd check the following either using a debugger or by adding additional logging:
What's the actual numerical value of indexPath.item? Does it represent a "valid" index into your array stickers2?
What is the value of file after you look it up? Is it a valid string, and does it correctly represent the name of an actual file on your file system?
What does the Outgoing class expect for the value of text in its -send:Video:Picture:Audio: method, a fully-qualified path name, or just a simple file name with no path? If the former, you'll either need to look up the system directory and construct the full file path before passing it in, or it would have needed to be constructed that way originally and inserted as such into stickers2, and if the latter, I'm assuming you will have needed to provided a "base" directory to Firebase/Parse somewhere.
This line is suspicious:
NSString *sticker = [file stringByReplacingOccurrencesOfString:#"#2x.png" withString:#""];
Do you mean to strip off the extension entirely? In other words, if you have a base filename of "foo#2x.png", then this will change "foo#2x.png" to "foo", is that what you really want. I'm guessing you might want this to be "foo.png" instead:
NSString *sticker = [file stringByReplacingOccurrencesOfString:#"#2x.png" withString:#".png"];
?
Hope this helps you track it down.
Related
I'm using OCMockito and I want to test a method in my ViewController that uses a NetworkFetcher object and a block:
- (void)reloadTableViewContents
{
[self.networkFetcher fetchInfo:^(NSArray *result, BOOL success) {
if (success) {
self.model = result;
[self.tableView reloadData];
}
}];
}
In particular, I'd want to mock fetchInfo: so that it returns a dummy result array without hitting the network, and verify that the reloadData method was invoked on the UITableView and the model is what it should be.
As this code is asynchronous, I assume that I should somehow capture the block and invoke it manually from my tests.
How can I accomplish this?
This is quite easy:
- (void) testDataWasReloadAfterInfoFetched
{
NetworkFetcher mockedFetcher = mock([NetowrkFetcher class]);
sut.networkFetcher = mockedFetcher;
UITableView mockedTable = mock([UITableView class]);
sut.tableView = mockedTable;
[sut reloadTableViewContents];
MKTArgumentCaptor captor = [MKTArgumentCaptor new];
[verify(mockedFetcher) fetchInfo:[captor capture]];
void (^callback)(NSArray*, BOOL success) = [captor value];
NSArray* result = [NSArray new];
callback(result, YES);
assertThat(sut.model, equalTo(result));
[verify(mockedTable) reloadData];
}
I put everything in one test method but moving creation of mockedFetcher and mockedTable to setUp will save you lines of similar code in other tests.
(Edit: See Eugen's answer, and my comment. His use of OCMockito's MKTArgumentCaptor not only eliminates the need for the FakeNetworkFetcher, but results in a better test flow that reflects the actual flow. See my Edit note at the end.)
Your real code is asynchronous only because of the real networkFetcher. Replace it with a fake. In this case, I'd use a hand-rolled fake instead of OCMockito:
#interface FakeNetworkFetcher : NSObject
#property (nonatomic, strong) NSArray *fakeResult;
#property (nonatomic) BOOL fakeSuccess;
#end
#implementation FakeNetworkFetcher
- (void)fetchInfo:(void (^)(NSArray *result, BOOL success))block {
if (block)
block(self.fakeResult, self.fakeSuccess);
}
#end
With this, you can create helper functions for your tests. I'm assuming your system under test is in the test fixture as an ivar named sut:
- (void)setUpFakeNetworkFetcherToSucceedWithResult:(NSArray *)fakeResult {
sut.networkFetcher = [[FakeNetworkFetcher alloc] init];
sut.networkFetcher.fakeSuccess = YES;
sut.networkFetcher.fakeResult = fakeResult;
}
- (void)setUpFakeNetworkFetcherToFail
sut.networkFetcher = [[FakeNetworkFetcher alloc] init];
sut.networkFetcher.fakeSuccess = NO;
}
Now your success path test needs to ensure that your table view is reloaded with the updated model. Here's a first, naive attempt:
- (void)testReloadTableViewContents_withSuccess_ShouldReloadTableWithResult {
// given
[self setUpFakeNetworkFetcherToSucceedWithResult:#[#"RESULT"]];
sut.tableView = mock([UITablewView class]);
// when
[sut reloadTableViewContents];
// then
assertThat(sut.model, is(#[#"RESULT"]));
[verify(sut.tableView) reloadData];
}
Unfortunately, this doesn't guarantee that the model is updated before the reloadData message. But you'll want a different test anyway to ensure that the fetched result is represented in the table cells. This can be done by keeping the real UITableView and allowing the run loop to advance with this helper method:
- (void)runForShortTime {
[[NSRunLoop currentRunLoop] runUntilDate:[NSDate date]];
}
Finally, here's a test that's starting to look good to me:
- (void)testReloadTableViewContents_withSuccess_ShouldShowResultInCell {
// given
[self setUpFakeNetworkFetcherToSucceedWithResult:#[#"RESULT"]];
// when
[sut reloadTableViewContents];
// then
[self runForShortTime];
NSIndexPath *firstRow = [NSIndexPath indexPathForRow:0 inSection:0];
UITableViewCell *firstCell = [sut.tableView cellForRowAtIndexPath:firstRow];
assertThat(firstCell.textLabel.text, is(#"RESULT"));
}
But your real test will depend on how your cells actually represent the fetched results. And that shows that this test is fragile: if you decide to change the representation, then you have to go fix up a bunch of tests. So let's extract a helper assertion method:
- (void)assertThatCellForRow:(NSInteger)row showsText:(NSString *)text {
NSIndexPath *indexPath = [NSIndexPath indexPathForRow:row inSection:0];
UITableViewCell *cell = [sut.tableView cellForRowAtIndexPath:indexPath];
assertThat(cell.textLabel.text, is(equalTo(text)));
}
With that, here's a test that uses our various helper methods to be expressive and pretty robust:
- (void)testReloadTableViewContents_withSuccess_ShouldShowResultsInCells {
[self setUpFakeNetworkFetcherToSucceedWithResult:#[#"FOO", #"BAR"]];
[sut reloadTableViewContents];
[self runForShortTime];
[self assertThatCellForRow:0 showsText:#"FOO"];
[self assertThatCellForRow:1 showsText:#"BAR"];
}
Note that I didn't have this end in my head when I started. I even made some false steps along the way which I haven't shown. But this shows how I try to iterate my way to test designs.
Edit: I see now that with my FakeNetworkFetcher, the block get executed in the middle of reloadTableViewContents — which doesn't reflect what will really happen when it's asynchronous. By shifting to capturing the block then invoking it according to Eugen's answer, the block will be executed after reloadTableViewContents completes. This is far better.
- (void)testReloadTableViewContents_withSuccess_ShouldShowResultsInCells {
[sut reloadTableViewContents];
[self simulateNetworkFetcherSucceedingWithResult:#[#"FOO", #"BAR"]];
[self runForShortTime];
[self assertThatCellForRow:0 showsText:#"FOO"];
[self assertThatCellForRow:1 showsText:#"BAR"];
}
I am trying to update the score of one of the labels on my custom cell after returning from a push navigation.
In my parent UITableViewController I have the code:
In ViewDidLoad
//These are mutable strings
self.disciplineScoreString1 = [NSMutableString stringWithFormat:#""];
self.disciplineScoreString2 = [NSMutableString stringWithFormat:#""];
self.disciplineScoreString3 = [NSMutableString stringWithFormat:#""];
self.disciplineScoreArray = [[NSMutableArray alloc] init];
[self.disciplineScoreArray addObject:self.disciplineScoreString1];
[self.disciplineScoreArray addObject:self.disciplineScoreString2];
[self.disciplineScoreArray addObject:self.disciplineScoreString3];
So far so good.
In cellForRowAtIndexPath
//this is the custom subclass of UITableViewCell
DayTableViewCell *cell = (DayTableViewCell *) [tableView dequeueReusableCellWithIdentifier:CellIdentifier forIndexPath:indexPath];
//this is the custom label
cell.disciplineScoreLabel.text = [self.disciplineScoreArray objectAtIndex:indexPath.row];
return cell;
Still so far so good. Right now the label in each of the 3 cells is blank.
I now segue to a UIViewController from the first cell and I return from the child UIViewController successfully setting the string value of self.disciplineScoreString1 to #"10"which I NSLog'ged in the parent UITableViewController.
How do I update the label for the first cell now? I have tried reload data in ViewWillAppear but its not working.
Thankyou
EDIT
This is the code in the Child ViewController
In viewWillDisappear:
[super viewWillDisappear:animated];
[self calculateDisciplineScore];
NSInteger currentVCIndex = [self.navigationController.viewControllers indexOfObject:self.navigationController.topViewController];
DisciplineTableViewController *parent = (DisciplineTableViewController *)[self.navigationController.viewControllers objectAtIndex:currentVCIndex];
parent.disciplineScoreString1 = self.disciplineScoreText;
You must be changing the string, instead of modifying it... consider this example
NSMutableArray *array = [NSMutableArray array];
NSMutableString *disciplineScoreString1 = [NSMutableString stringWithFormat:#"original string"];
[array addObject:disciplineScoreString1];
[disciplineScoreString1 appendString:#" hello"]; // OK.. you're modifying the original string
NSLog(#"%#", [array objectAtIndex:0]);
disciplineScoreString1 = [NSMutableString stringWithFormat:#"new string"]; // WRONG!!
NSLog(#"%#", [array objectAtIndex:0]);
The ouput is:
2014-02-15 06:36:46.693 Hello[19493:903] original string hello
2014-02-15 06:36:46.694 Hello[19493:903] original string hello
The second example is wrong because you're creating a new string, and the object in the array is still pointing to the old original string.
EDIT:
// try this
[parent.disciplineScoreString1 replaceCharactersInRange:NSMakeRange(0, parent.disciplineScoreString1.length) withString:self.disciplineScoreText];
// instead of
parent.disciplineScoreString1 = self.disciplineScoreText;
Your current approach is quite rigid and not very standard. You would normally set up some kind of delegate to pass data from a child to a parent. It is very rarely correct that a child should know how to reach into the parent's innards and change it's data.
I would personally start with something like this
#interface ChildViewController : UIViewController
#property (nonatomic, copy) void (^onCompletion)(ChildViewController *viewController, NSString *disciplineText);
#end
Now I'm assuming you are dismissing by just calling [self.navigationController popViewControllerAnimated:YES] from within the child? This is a little odd as it means that the childViewController can now only ever be presented in a navigationController, this makes it less reusable.
What I would do is at the point where you normally call popViewControllerAnimated: I would call the completion instead
if (self.onCompletion) {
self.onCompletion(self, self.disciplineText);
}
Now the object who provides the onCompletion can decide how this controller get's removed and it's told about the data that we finished with, which enabled the controller to do what it wants with it.
In your case the parent controller would provide the completion as it knows how the child is presented so it will know how to dismiss it. It also know that it may want to do something with the data the child finished with
// Where you present the child
childViewController.disciplineText = self.self.disciplineScoreArray[index];
__weak __typeof(self) weakSelf = self;
childViewController.onCompletion = ^(ChildViewController *viewController, NSString *disciplineText){
weakSelf.disciplineScoreArray replaceObjectAtIndex:index withObject:disciplineText];
[self.navigationController popViewControllerAnimated:YES];
[weakSelf.tableView reloadRowsAtIndexPaths:#[ [NSIndexPath indexPathForRow:index inSection:0] ]
withRowAnimation:UITableViewRowAnimationFade];
};
I know that there are tutorials everywhere, but I can't figure this out for some reason. I have a tab bar controller. Each tab links to a navigation controller, which is segued to a view controller. So, 2 main view controllers (StatusVC and TransactionsVC).
In StatusVC, I have a text field. In TransVC, I have a table view. A person adds a cell to the table. Math is done behind the scenes. The cell values are added together (numbers). This information is sent back to StatVC for calculations and displaying of the data. I've already got the math part down. My question: how do I transfer the data between view controllers, and better yet, how do I store this data so that it doesn't get deleted on quit (NSUserDefaults probably)?
This can be broken down I suppose, the transferring of data, the saving of data, and the displaying of data when the tab is pressed and view is shown.
I'm hoping this is making sense. Anyway, here's the code I've got. You're looking at TranVC. User enters data into the table with an alert view. You are looking at part of the Alert View delegate methods. This is when the user enters data into a cell (presses done). Look for key areas with the ******* comments.
StatusViewController *statVC = [[StatusViewController alloc]init]; //*******init
// Set the amount left in the budget
NSString *amountToSpend = statVC.amountLeftInBudget.text;
double budgetLabel = [amountToSpend doubleValue];
NSString *lastItem = [transactions objectAtIndex:0];
double lastLabel = [lastItem doubleValue];
double totalValue = budgetLabel - lastLabel;
NSString *amountToSpendTotal = [NSString stringWithFormat: #"%.2f", totalValue];
statVC.amountLeftInBudget.text = amountToSpendTotal; //*******set text (but not save), either way, this doesn't work
// Set the amount spent
NSString *sum = [transactions valueForKeyPath:#"#sum.self"];
double sumLabel = [sum doubleValue];
NSString *finalSum = [NSString stringWithFormat:#"%.2f", sumLabel];
//Set the amountSpent label
statVC.amountSpent.text = finalSum; //*******set text (but not save), either way, this doesn't work
// The maxed out budget section
if ([statVC.amountLeftInBudget.text isEqualToString: #"0.00"]) //*******set color (but not save), either way, this doesn't work
{
statVC.amountLeftInBudget.textColor = statVC.currencyLabel.textColor = [UIColor redColor];
} else if ([statVC.amountLeftInBudget.text compare:#"0.00"] == NSOrderedAscending)
{
statVC.amountLeftInBudget.textColor = statVC.currencyLabel.textColor = [UIColor redColor];
} else if ([statVC.amountLeftInBudget.text compare:#"0.00"] == NSOrderedDescending)
{
statVC.amountLeftInBudget.textColor = statVC.currencyLabel.textColor = [UIColor colorWithRed:23.0/255.0 green:143.0/255.0 blue:9.0/255.0 alpha:1.0];
}
if ([statVC.amountLeftInBudget.text compare:#"0.00"] == NSOrderedAscending)
{
// Create our Installation query
UIAlertView *exceed;
exceed = [[UIAlertView alloc]
initWithTitle: #"Budget Exceeded"
message: #"You have exceeded your budget amount"
delegate: self
cancelButtonTitle: #"Okay"
otherButtonTitles: nil];
[exceed show];
}
Any help with this would be amazing.
This is indeed a common question.
There are various solutions. The one I recommend is to use a data container singleton. Do a google search on the singleton design pattern in Objective C. You'll even find examples of it here on SO.
Create a singleton with properties for the values that you want to share. Then teach your singleton to save it's data. You can use user defaults, you can use NSCoding, you can extract the data to a dictionary and save it to a plist file in your documents directory, or various other schemes as well.
Like Duncan suggested, a Singleton pattern might be the best route to go. If you place the shared data into a model class, you can create a class method that can be used to acquire a singleton object.
MyModel.m
#implementation MyObject
- (id) init
{
return nil; // We force the use of a singleton. Probably bad practice?
}
// Private initializer used by the singleton; not included in the header file.
- (id)initAsSingleton {
self = [super init];
if (self) {
// Initialize your singleton instance here.
}
return self;
}
+ (MyModel *)sharedMyModel {
static MyModel *myModel = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
myModel = [[MyModel alloc] initAsSingleton];
});
return myModel;
}
MyModel.h
#interface MyModel : NSObject
+ (MyModel *)sharedMyModel; // Singleton instance.
#end
This does not protect against you using [[MyModel alloc] init];. It returns a nil object which is probably poor programming on my end, but it does force you to use the singleton object instead. To use in each one of your view controllers, you just use the following line to grab the singleton instance.
MyModel *model = [MyModel sharedMyModel];
Store the data into it, and return to your other view controller and grab the singleton again. You'll have all of your data.
After thinking about it, you could also force the default initializer to just return your singleton instance like:
- (id)init {
return [MyModel sharedMyModel];
}
I'm new to IOS dev and am making simple programs this one is a hangman game.
I wanted to pick a random string from a plist file (completed).
I now want to compare the user input text (from a text field) and compare it to the string we have randomly picked from our plist.
Here is my code for MainViewController.m as it is a utility. Only the MainView is being used currently.
#import "MainViewController.h"
#import "WordListLoad.h"
#interface MainViewController ()
#end
#implementation MainViewController
#synthesize textField=_textField;
#synthesize button=_button;
#synthesize correct=_correct;
#synthesize UsedLetters=_UsedLetters;
#synthesize newgame=_newgame;
- (IBAction)newg:(id)sender
{
[self start];
}
- (void)start
{
NSMutableArray *swords = [[NSMutableArray alloc] initWithContentsOfFile:[[NSBundle mainBundle] pathForResource:#"swords" ofType:#"plist"]];
NSLog(#"%#", swords);
NSInteger randomIndex = arc4random() % [swords count];
NSString *randomString = [swords objectAtIndex:randomIndex];
NSLog(#"%#", randomString);
}
This is where i would like to implement the checking
I have tried characterAtIndex and I can't seem to get it to work for hard coded placed in the string let along using a for statement to systematic check the string.
- (void)check: (NSString *) randomString;
{
//NSLogs to check if the values are being sent
NSLog(#"2 %#", self.textField.text);
}
- (IBAction)go:(id)sender
{
[self.textField resignFirstResponder];
NSLog(#"1 %#", self.textField.text);
[self check:(NSString *) self.textField];
_textField.text = nil;
}
- (void)viewDidLoad
{
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
[self start];
}
To compare 2 strings: [string1 equalsToString:string2]. This will return true if string1 is equal to string2. To get the string contained in a UITextfield: textfield.text.
Given that it's a hangman game, I assume you are trying to see if a single letter is contained by a given string - so equalsToString: wouldn't be what you want.
Instead, probably better to use rangeOfString:options:
if ([randomString rangeOfString:self.textfield.text options:NSCaseInsensitiveSearch].location != NSNotFound){
// Do stuff for when the letter was found
}
else {
// Do stuff for when the letter wasn't found
}
Also, as was pointed out by Patrick Goley, you need to make sure you're using the textfield.text value to get the string from it. Same with storing the initial word you'll be using as the hidden word.
There are also a couple of other minor code issues (semicolon in the function header, for example) that you'll need to clean up to have a functioning app.
Edit: Made the range of string call actually use the textfield's text, and do so case-insensitive (to prevent false returns when a user puts a capital letter when the word is lower case, or vice-versa). Also included link to documentation of NSString's rangeOfString:options:
For your check method you are sending the UITextfield itself, instead of its text string. Instead try:
[self check: self.textfield.text];
You'll also need to create an NSString property to save your random string from the plist, so you can later access to compare to the textfield string like so:
declare in the interface of the class:
#property (nonatomic,strong) NSString* randomString;
in the start method:
self.randomString = [swords objectAtIndex:randomIndex];
in the check method:
return [self.randomString isEqualToString:randomString];
(Note: You can probably even answer this without reading any of my code, if you don't feel like reading this novel of a question, I've explained at the bottom)
I have some code that's throwing me a bit off course. It seems like my code is calling [tableView reloadData] before the data is ready, although I set the data before I call reloadData. This is probably a newbie-question, but I have never dealt with this kind of code before, and I'd be very happy if someone could explain to me exactly what is going on, and why some of my code is allowed to jump out of the chronological logic..
My question has nothing to do with GameCenter, I think this is a general Objective-C thing, but I am experiencing it with GameCenter, so even if you don't know anything about GameCenter, you'll probably be able to understand what I'm talking about.
I have a tableViewController, and I am filling the tableView with matches(games) from GameCenter, and showing the names of the opponent(always one opponent in my turn-based-game).
I have put this in my viewDidAppear:
[GKTurnBasedMatch loadMatchesWithCompletionHandler:^(NSArray *matches, NSError *error) {
//Gets an array (*matches) of GKTurnBasedMatches(All matches the local player is involved in)
if(error)
{
NSLog(#"Load matches error: %#", error.description);
return;
}
//putting them in an array
matchList = [[NSArray alloc] initWithArray:matches];
//So far only for testing purposes: I am creating an array for player names:
playerNames = [[NSMutableArray alloc] init];
for(GKTurnBasedMatch* mat in matchList)
{//For every match in the array of matches, find the opponent:
GKTurnBasedParticipant *otherParticipant; //These games only have 2 players. Always.
for(GKTurnBasedParticipant *part in [mat participants])
{ //Loop through both participants in each match
if(NO == [part.playerID isEqualToString:[GKLocalPlayer localPlayer].playerID])
{//If the participant *part is NOT the local player, then he must be the opponent:
otherParticipant = part; //Save the opponent.
}
}
if(otherParticipant != nil && otherParticipant.playerID != nil)
{ //If the opponent exists(like he does)
[GKPlayer loadPlayersForIdentifiers:#[otherParticipant.playerID] withCompletionHandler:^(NSArray *players, NSError *error) {
//Returns an array with only the opponent in it
if([(GKPlayer*)[players objectAtIndex:0] displayName]!=nil)
{//If he has a name:
NSString *nam = [[NSString alloc] initWithString:[(GKPlayer*)[players objectAtIndex:0] displayName]];
//Add the name to the name-array
[playerNames addObject:nam];
}
else
{//If he doesn't have a name, put "No name" into the name array(is not happening)
NSString *nam = [[NSString alloc]initWithString:#"No name"];
[playerNames addObject:nam];
}
}];
}
else
{//If for some reason the entire opponent is nil or he doesn't have a playerID, which isn't happening, add "No name";(This is not happening)
[playerNames addObject:[NSString stringWithFormat:#"No name"]];
}
}
//And THEN reload the data, now with the NSMutableArray 'playerNames' ready and filled with opponent's names.
[[self tableView] reloadData];
}];
This is my CellForRowAtIndexPath:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
static NSString *CellIdentifier = #"matchCell";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier forIndexPath:indexPath];
NSString *name = #"Nobody";
if(playerNames!=nil && [playerNames count]>indexPath.row)
name = [playerNames objectAtIndex:indexPath.row];
[[cell textLabel] setText:[NSString stringWithFormat:#"Game vs. %#", name]];
return cell;
}
The thing is: when the view controller appears: as expected, the tableView is initially empty, but after about half a second, it fills with "Game vs. Nobody" in the correct number of games. If I start two games, and re-enter the view controller, then it's blank at first, and then show "Game vs. Nobody" on two cells. BUT: if I scroll the rows outside the screen, and force them to call for cellForRowAtIndexPath again, then the correct name shows up. So it seems like the [tableView reloadData] in viewDidAppear is called before I ask it to, before the array with names has been filled.
I assume this has something to do with the whole CompletionHandler-thing, as I have never used this before, but I thought everything inside the CompletionHandler was supposed to happen chronologically? Is there something I can call, like onCompletion{} or something?
Ideally, I want an UIAlertView or something to show up with a spinning wheel, the UIActivityIndicationView, and I want it to show until all the data has been loaded correctly. I don't know how to find out WHEN the completionHandler is done, and what it really is..
The playerNames array is being filled within the completion handler for loadPlayersForIdentifiers, which is (I assume) asynchronous. The names array is not populated until the completion handler runs, but you are calling [tableView reloadData] within the other block.
To fix, just move the reload call within the completion block for loadPlayersForIdentifiers (at the end)