So I'm making a game that involves wireless communication between multiple iPhones, with one being the host. I am attempting to do so via the MultipeerConnectivity framework, and I've made a MCManager class (an instance of which I put into appDelegate so it's available throughout the app) to handle sending data from one system to another. This is how sending is implemented in my code:
- (void) sendState: (NSString*) str;
//used by the host to send commands to the other connected systems
{
if(appDelegate.mcManager.connected && iAmHost){
NSData *dataToSend = [str dataUsingEncoding: NSUTF8StringEncoding];
NSArray *allPeers = appDelegate.mcManager.session.connectedPeers;
NSError *error;
[appDelegate.mcManager.session sendData:dataToSend
toPeers:allPeers
withMode:MCSessionSendDataReliable
error:&error];
if (error) {
NSLog(#"%#", [error localizedDescription]);
}
}
}
and when the subordinate systems receive the data, MCManager sends the Notification Center a notification and my class, which is looking for that particular notification, grabs it and executes this:
-(void)didReceiveDataWithNotification:(NSNotification *)notification{
if(!iAmHost){
NSData *receivedData = [[notification userInfo] objectForKey:#"data"];
NSString *action = [[NSString alloc] initWithData: receivedData encoding:NSUTF8StringEncoding];
NSLog(#"Recieved:");
NSLog(action); //for debugging purposes, and figuring out timing
//decide how to act depending on the string given
if([action containsString:#"ChangeMaxScore"]){
//the string was formatted as, for example, "ChangeMaxScore105"
NSString* valueStr = [action substringFromIndex:14];
maxScore = (int)[valueStr integerValue];
[self changeMaxScore]; //this method changes the label text that shows the user the value of maxScore
}
else if([action containsString:#"ChangePlayerNo"]){
//strings are formatted as "ChangePlayerNo2" for the second segment in a segmented control with the segments "2", "3", "4"
//so it would be referring to four players
NSString *valueStr = [action substringFromIndex:14];
[playerNumberSegmentedControl setSelectedSegmentIndex: [valueStr integerValue]];
playerNumber = playerNumberSegmentedControl.selectedSegmentIndex + 2;
[self changePlayerNumber];
//Players can either be human or a type of AI (AI-1,AI-2,etc.)
//the segmented control where you choose this is invisible unless that player number is playing
//so this method sets that segmented control visible and interactable (by adding it to the view)
//and removes those segmented controls not in use from the view
}
else if([action containsString:#"ChangeP0State"]){
//changes the value of the first player's segmented control (Human, AI-1, AI-2, etc.
NSString* valueStr = [action substringFromIndex:13];
AIControl0.selectedSegmentIndex = (int)[valueStr integerValue];
}
...
else if([action containsString:#"StartGame"])
[self newGame];
//this method starts the game and, in the process, pushes another view controller
}
}
My issue is that these actions, on the receiving end, are very laggy. For changing the number of players, for instance, the receiver NSLogs "Received: ChangePlayerNo1", and the segmented control on-screen changes its selected segment to the second one, but the stuff that's supposed to show up at that point...doesn't. And when I send the "StartGame" command, the receiver NSLogs that it has received it, I have to wait thirty seconds for it to actually start the game like it was asked.
This delay makes it very hard to test whether my wireless methods are working or not (it works on the host's side, mostly because the host is changing them manually, not responsively - also, all of this works on the other side of my program, which is just the game without wireless support, with several players/AIs on a single screen) and, if not fixed, will definitely prevent the app from being used easily.
I'm curious what causes this and what I can do to fix it. Thank you!
Related
To understand this question, return with me now through the WWDC time machine to the distant past, 2014, when Action extensions were introduced and explained in this video:
https://developer.apple.com/videos/play/wwdc2014/217/
About halfway through, in slide 71, about minute 23:30, the presenter gives instructions for returning a value back to the calling app (the app where the user tapped our Action extension's icon in an activity view):
- (IBAction)done:(id)sender {
NSData *data = self.contents;
NSItemProvider *itemProvider =
[[NSItemProvider alloc] initWithItem:data typeIdentifier:MyDocumentUTI];
NSExtensionItem *item = [[NSExtensionItem alloc] init];
item.attachments = #[itemProvider];
}
A moment later, slide 75, about minute 26, we see how the app that put up the activity view controller is supposed to unwrap that envelope to retrieve the result data:
- (void)setupActivityViewController {
UIActivityViewController *controller;
controller.completionWithItemsHandler =
^(NSString *activityType, BOOL completed,
NSArray *returnedItems, NSError *error) {
if (completed && (returnedItems.count > 0)) {
// process the result items
}
}];
}
So my question is: is that for real? Has anyone within the sound of my voice ever done either of those things? Namely:
Does your app have an Action extension that returns a value to the caller?
Does your app put up an activity view controller that receives the result of some arbitrary unknown Action extension and does something with the value?
I ask because (1) I have never seen (on my iPhone) an Action extension that actually returns a value, and (2) the code elided in "process the result items" seems to me to be complete hand-waving, because how would my app even know what kind of data to expect?
I have come to believe that this code is an aspirational pipe dream with no corresponding reality. But I would be delighted to be told I'm wrong.
I am using an app to lock, unlock, and open the trunk of my car. The only problem is that I can't figure out how to modify the Xcode project so there are 3 buttons. Basically right now if I type "U" then enter- the car unlocks, "L" then enter- the car locks, and "T" then enter- the trunk opens. I want to add three buttons that simulate these three things and eliminate the typing all together. If you want to see my adruino or xcode project code I can upload those. I have put some code about the text box below.
BOOL)textFieldShouldReturn:(UITextField *)textField
{
NSString *text = textField.text;
NSNumber *form = [NSNumber numberWithBool:NO];
NSString *s;
NSData *d;
if (text.length > 16)
s = [text substringToIndex:16];
else
s = text;
d = [s dataUsingEncoding:NSUTF8StringEncoding];
if (bleShield.activePeripheral.state == CBPeripheralStateConnected) {
[bleShield write:d];
NSDictionary *dict = [NSDictionary dictionaryWithObjectsAndKeys:text, TEXT_STR, form, FORM, nil];
[tableData addObject:dict];
[_tableView setContentOffset:CGPointMake(0, CGFLOAT_MAX)];
NSLog(#"%f", _tableView.contentOffset.y);
[self.tableView reloadData];
}
textField.text = #"";
return YES;
Thanks for the help!
Your view controller probably has a textFieldShouldReturn method which is taking the string value from the text field and building a parameter to a call that initiates sending the command. If not this method then perhaps its action method linked to the text field.
You'll need to duplicate parts of that code into a method that receives a string parameter instead of taking it from the text field, say named sendLockCommand:(NSString *)commandString (assuming you're coding in Objective-C, also like that repo).
Make action methods for your buttons, something like lockDoors, unlockDoors, openTrunk, in each call [self sendLockCommand:#"L"], each with the appropriate string. Wire up the buttons to those actions and you're good to go.
When I go from my home segue into another I enter data into text fields. I have a navigation bar at the top and if I hit the back button to go to the home segue I lose all the data I entered. What is an efficient way of keeping the data in those fields?
Thank you.
Edit:
Maybe this helps, but when I load the screen it calls the method-
- (void)viewDidLoad {
NSLog(#"Testing viewDidLoad");
[super viewDidLoad];
// Do any additional setup after loading the view.
_YourName.delegate = self;
_Notes.delegate = self;
// Set up the scroll view.
_scroller.delegate = self;
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(keyboardWillHide:) name:UIKeyboardWillHideNotification object:nil];}
but when I go back to the previous viewcontroller or hit the home button on the ipad it does not call:
- (void)viewDidUnload{
NSLog(#"Testing viewDidUnload");
[self setYourName:nil];
[self setPartnersName:nil];
[self setStartTime:nil];
[self setEndTime:nil];
[self setCurrentTemp:nil];
[self setCurrentWeather:nil];
[self setProjectName:nil];
[self setInstructorName:nil];
[self setClassNum:nil];
[self setWaypoints:nil];
[self setNotes:nil];
[super viewDidUnload];}
Also the data remains in the UITextFields and UITextViews if I hit the home button and then reopen the app. It just goes away if I segue back to the previous UIViewController.
- (IBAction)saveCurrent:(id)sender {
if([self checkFields]){
[self getEndingTime]; //gets the time when saved
// save all data to string using csv formatting
NSString *resultLine = [self getCSVformat];
NSArray *paths = NSSearchPathForDirectoriesInDomains
(NSDocumentDirectory, NSUserDomainMask, YES);
NSString *documentsDirectory = [paths objectAtIndex:0];
// get today's date for file name ****DO DATE FORMATTER ONLY ONCE
NSDate *today = [NSDate date];
NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init];
[dateFormatter setDateFormat:#"yyyy-MM-dd"];
NSString *todayDate = [dateFormatter stringFromDate:today];
//make a file name to write the data to using the documents directory:
NSString *fileName = [NSString stringWithFormat:#"%#/%#_%#_%#.csv",documentsDirectory,
todayDate,[[self ProjectName] text],[[self YourName] text]];
//save content to the documents directory
[resultLine writeToFile:fileName
atomically:NO
encoding:NSUTF8StringEncoding
error:nil];
}}
- (IBAction)retractKeyboard:(id)sender {
[self resignFirstResponder];}
When you push your second view controller, a new instance of the second view controller is created and this is what is shown. When you press the back button this instance is destroyed and the original view controller pops back. When you access the second view controller again, it is another new instance so all of the fields are empty.
If you press the home button your app is suspended, but the second view controller instance is still in memory, so if you return immediately to your app, the values are still there. If you ran some other apps then eventually iOS would terminate your app to releae the memory and you would see your app start from the beginning if you open it.
There are a couple of different approaches you can take to persist the data, depending on the type of data and what you need to do with it in your app. The simplest way is to store data in NSUserDefaults - This works for small amounts of data such as simple strings. For example
[[NSUserDefaults standardUserDefaults] setObject:self.projectName forKey:#"projectKey"];
will save a string. and
self.projectName = [[NSUserDefaults standardUserDefaults] stringForKey:#"projectKey"];
would read it back again. You could use the first line in an action handler for a "save" button and the second in viewDidLoad
If you need to store more complex data then you should look at Core-Data which provides database-style storage mapped to data classes or even online solutions like Parse.com
The other location where you can handle data save/load is in prepareForSegue:sender: - you can examine the segue name and access properties of the source and destination view controller
You will never lose your entered data unless you remove it or the container is removed from the superview.
Importing multiple photos from album, one of the delegate method is
// Here info is array of dictionary containing FileName/AssetURL etc
- (void)somePicker(SomePicker*)somePicker didFinishPickingMediaWithInfo:(NSArray *)info {
_importStatusView.center = self.view.center;
[self.view addSubview:_importStatusView];
[self dismissViewControllerAnimated:YES completion:^{
NSNumber *total = [NSNumber numberWithInteger:info.count];
[info enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {
NSDictionary *imageInfo = (NSDictionary*)obj;
NSString *fileName = [imageInfo objectForKey:#"UIImagePickerControllerFileName"];
NSURL *imageURL = [imageInfo objectForKey:UIImagePickerControllerReferenceURL];
ALAssetsLibrary *assetLibrary=[[ALAssetsLibrary alloc] init];
[assetLibrary assetForURL:imageURL resultBlock:^(ALAsset *asset) {
NSLog(#"start");
ALAssetRepresentation *rep = [asset defaultRepresentation];
Byte *buffer = (Byte*)malloc(rep.size);
NSUInteger buffered = [rep getBytes:buffer fromOffset:0.0 length:rep.size error:nil];
NSData *data = [NSData dataWithBytesNoCopy:buffer length:buffered freeWhenDone:YES];
NSString *filePath = [_currentPath stringByAppendingPathComponent:fileName];
[data writeToFile:filePath atomically:YES];
//This also has no effect
//dispatch_async(dispatch_get_main_queue(), ^{
//_lblImportCountStatus.text = [NSString stringWithFormat:#"%d of %d",idx+1,[total integerValue]];
//NSLog(#"Label value->%#",_lblImportCountStatus.text); //This prints values but after everything is finished it prints all line at once i.e. at the end of the enumeration of all items
//});
//Update UI
NSNumber *current = [NSNumber numberWithInteger:idx+1];
NSDictionary *status = [NSDictionary dictionaryWithObjectsAndKeys:current,#"current", total,#"totalCount", nil];
[self performSelectorOnMainThread:#selector(updateImportCount:) withObject:status waitUntilDone:YES];
//_lblImportCountStatus.text = [NSString stringWithFormat:#"%d of %d",idx+1,[total integerValue]];
if(idx==info.count-1){
[_importStatusView removeFromSuperview];
}
NSLog(#"Finish");
} failureBlock:^(NSError *error) {
NSLog(#"Error: %#",[error localizedDescription]);
}];
}];
}];
}
My declaration for status view and label is
#property (strong, nonatomic) IBOutlet UIView *importStatusView; //View containing label
#property (weak, nonatomic) IBOutlet UILabel *lblImportCountStatus; //Label
Everything in above code is working fine and as expected, but the problem is a importStatusView is being added to the screen but lblImportCountStatus value is not displaying, though If I log the values it shows updated.
When enumeration is finished at the end all the NSLog gets printed for e.g. If I have imported 10 photos than at last it prints, i.e. dispatch_async(dispatch_get_main_queue() this function has no effect at all while enumeration is in progress.
Label value->1 of 10
Label value->2 of 10
Label value->3 of 10
Label value->4 of 10
Label value->5 of 10
Label value->6 of 10
Label value->7 of 10
Label value->8 of 10
Label value->9 of 10
Label value->10 of 10
What could be the issue ?
Update:
-(void)updateImportCount:(NSDictionary*)info{ //(NSNumber*)current forTotalItems:(NSNumber*)totalCount{
NSNumber *current = [info objectForKey:#"current"];
NSNumber *totalCount = [info objectForKey:#"totalCount"];
_lblImportCountStatus.text = [NSString stringWithFormat:#"%d of %d",[current integerValue],[totalCount integerValue]];
[_lblImportCountStatus setNeedsDisplay];
NSLog(#"Updating ui->%#",_lblImportCountStatus.text);
}
Above function works on main thread and updates but stil label is not shown it prints following NSLog
start
Updating ui->1 of 10
Finish
start
Updating ui->2 of 10
Finish
start
Updating ui->3 of 10
Finish
start
Updating ui->4 of 10
Finish
start
Updating ui->5 of 10
Finish
start
Updating ui->6 of 10
Finish
start
Updating ui->7 of 10
Finish
start
Updating ui->8 of 10
Finish
start
Updating ui->9 of 10
Finish
start
Updating ui->10 of 10
Finish
I have uploaded project at this location, please feel free to help.
All those blocks are executing on the main thread (the easiest way to verify this is using NSThread's +currentThread to get the current thread, and -isMainThread to check if it's the main thread. Anywhere you have code that you want to see what thread it's on, do something like this:
NSLog( #"enumeration block on main thread: %#",
[[NSThread currentThread] isMainThread] ? #"YES" : #"NO" );
I think the problem is, since this is all executing on the main thread, you're locking up the runloop, not giving the UI a chance to update.
The right way to fix this is probably to really do this processing on a separate thread (calling the code to update the UI via performSelectorOnMainThread:, as you're doing now). But, a quick hack to make it work would be to allow the runloop to run. At the end of updateImportCount:, do something like this:
[[NSRunLoop currentRunLoop] runUntilDate:[NSDate date]];
It ain't pretty, but it will work.
Update:
In Cocoa (Mac OS and iOS) there a concept of a runloop. The main thread of your application drives an NSRunLoop which serves as the event loop. User actions -- taps, etc. -- are processed through this loop, as are things like timers, network connections, and other things. See the NSRunLoop Reference for more information on that.
In iOS, drawing also happens on the runloop. So when you call setNeedsDisplay on a view, that view is not redrawn immediately. Rather, it's simply flagged as needing redraw, and then on the next drawing cycle (the next trip through the runloop) the actual drawing takes place. The UIView reference has a brief description of this (see the section "The View Drawing Cycle". Quoting from that section:
When the actual content of your view changes, it is your
responsibility to notify the system that your view needs to be
redrawn. You do this by calling your view’s setNeedsDisplay or
setNeedsDisplayInRect: method of the view. These methods let the
system know that it should update the view during the next drawing
cycle. Because it waits until the next drawing cycle to update the
view, you can call these methods on multiple views to update them at
the same time.
When you return from whatever method was called in response to some user action (in this case, somePicker:didFinishPickingMediaWithInfo:, control returns to the runloop, and any views that need redrawing, are. However, if you do a bunch of processing on the main thread without returning, the runloop is basically stalled, and drawing won't take place. The code above basically gives the runloop some time to process, so drawing can take place. [NSDate date] returns the current date and time, right now, so that basically tells the runlooop "run until right now", which ends up giving it one cycle through the loop, giving you one drawing cycle, which is an opportunity for your label to be redrawn.
I have a simple iPhone app that is parsing data (titles, images etc.) from rss feed and showing in the tableview.
The viewDidLoad has an initial counter value to reach the first page of the feed and load in the tableview by calling the fetchEntriesNew method:
- (void)viewDidLoad
{
[super viewDidLoad];
counter = 1;
[self fetchEntriesNew:counter];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:#selector(dataSaved:)
name:#"DataSaved" object:nil];
}
- (void) fetchEntriesNew:(NSInteger )pageNumber
{
channel = [[TheFeedStore sharedStore] fetchWebService:pageNumber withCompletion:^(RSSChannel *obj, NSError *err){
if (!err) {
int currentItemCount = [[channel items] count];
channel = obj;
int newItemCount = [[channel items] count];
NSLog(#"Total Number Of Entries Are: %d", newItemCount);
counter = (newItemCount / 10) + 1;
NSLog(#"New Counter Should Be %d", counter);
int itemDelta = newItemCount - currentItemCount;
if (itemDelta > 0) {
NSMutableArray *rows = [NSMutableArray array];
for (int i = 0; i < itemDelta; i++) {
NSIndexPath *ip = [NSIndexPath indexPathForRow:i inSection:0];
[rows addObject:ip];
}
[[self tableView] insertRowsAtIndexPaths:rows withRowAnimation:UITableViewRowAnimationBottom];
[aiView stopAnimating];
}
}
}];
[[self tableView] reloadData];
}
When the user reaches the bottom of the tableview, i am using the following to reach the next page of the feed and load at the bottom of the first page that was loaded first:
- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView
{
float endScrolling = scrollView.contentOffset.y + scrollView.frame.size.height;
if (endScrolling >= scrollView.contentSize.height)
{
NSLog(#"Scroll End Called");
NSLog(#"New Counter NOW is %d", counter);
[self fetchEntriesNew:counter];
}
}
UPDATE2: Here is a more easy to understand description of whats wrong that i am unable to solve: For example there are 10 entries in the each page of the rss feed. The app starts, titles and other labels are loaded immediately and images starts loading lazily and finally gets finished. So far so good. The user scrolls to reach the bottom, reaching the bottom will use the scroll delegate method and the counter gets incremented from 1 to 2 telling the fetchEntriesNew method to reach the second page of the rss feed. The program will start loading the next 10 entries at the bottom of first 10 previously fetched. This can go on and the program will fetch 10 more entries every time the user scrolls and reaches bottom and the new rows will be placed below the previously fetched ones. So far so good.
Now let us say the user is on page 3 currently which has been loaded completely with the images. Since page 3 is loaded completely that means currently there are 30 entries in the tableview. The user now scrolls to the bottom, the counter gets incremented and the tableview begins populating the new rows from page 4 of the rss feed at the bottom of the first 30 entries. Titles get populated quickly thus building the rows and while the images are getting downloaded (not downloaded completely yet), the user quickly moves to the bottom again, instead of loading the 5th page at the bottom of the 4th, it will destroy the 4th ones that is currently in the middle of getting downloaded and starts loading the 4th one again.
What it should do is that it should keep on titles etc from next pages when user reaches the bottom of the tableview regardless of whether the images of the previous pages are in the middle of getting downloaded or not.
There is NO issue with the downloading and persisting data in my project and all the data is persisted between the application runs.
Can someone help to point me out to the right direction. Thanks in advance.
UPDATE 3: Based on #Sergio's answer, this is what i did:
1) Added another call to archiveRootObject [NSKeyedArchiver archiveRootObject:channelCopy toFile:cachePath]; after [channelCopy addItemsFromChannel:obj];
At this point, its not destroying and reloading the same batch again and again, exactly what i wanted. However, it doesn't persist images if i scroll multiple times to reach the next page without the images of the previous page were loaded completely.
2) I am not sure how to use Bool as he explained in the answer. This is what i did: Added #property Bool myBool; in TheFeedStore, synthesised it and set it to NO after newly added archiveRootObject:channelCopy and set it to YES in ListViewController at the very start of fetchEntries method. It didn't work.
3) I also realised the way i am dealing with the whole issue is performance vice not better. Although i don't know how to use images outside the cache and handle them as sort of cache. Are you suggesting to use a separate archiving file for images?
Thanks a lot to all people who have contributed in trying to solve my issue.
Your issue can be understood if you consider this older question of yours and the solution I proposed.
Specifically, the critical bit has to do with the way you are persisting the information (RSS info + images), which is through archiving your whole channel to a file on disk:
[channelCopy addItemsFromChannel:obj];
[NSKeyedArchiver archiveRootObject:channelCopy toFile:pathOfCache];
Now, if you look at fetchEntriesNew:, the first thing that you do there is destroying your current channel. If this happens before the channel has been persisted to disk you enter a sort of endless loop.
I understand you are currently persisting your channel (as per my original suggestion) at the very end of image download.
What you should do is persisting the channel just after the feed has been read and before starting downloading the images (you should of course also persist it at the end of image downloads).
So, if you take this snippet from my old gist:
[connection setCompletionBlock:^(RSSChannel *obj, NSError *err) {
if (!err) {
[channelCopy addItemsFromChannel:obj];
// ADDED
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
dispatch_group_wait(obj.imageDownloadGroup, DISPATCH_TIME_FOREVER);
[NSKeyedArchiver archiveRootObject:channelCopy toFile:cachePath];
});
}
block(channelCopy, err);
what you should do is adding one more archiveRootObject call:
[connection setCompletionBlock:^(RSSChannel *obj, NSError *err) {
if (!err) {
[channelCopy addItemsFromChannel:obj];
[NSKeyedArchiver archiveRootObject:channelCopy toFile:cachePath];
// ADDED
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
dispatch_group_wait(obj.imageDownloadGroup, DISPATCH_TIME_FOREVER);
[NSKeyedArchiver archiveRootObject:channelCopy toFile:cachePath];
});
}
block(channelCopy, err);
This will make things work as long as you do not scroll fast enough so that the channel is destroyed before the feed (without images) is ever read. To fix this you should add a bool to your TheFeedStore class that you set to YES when you call fetchWebService and reset just after executing the newly added archiveRootObject:channelCopy.
This will fix your issues.
Let me also say that from a design/architecture point of view, you have a big issue with the way you manage persistence. Indeed, you have a single file on disk that you write atomically using archiveRootObject. This architecture is intrinsically "risky" from a multi-threading point of view and you should also devise a way to avoid that concurrent accesses to the shared stored have no destructive effects (e.g.: you archive your channel to disk for page 4 at the same time as the images for page 1 have been fully downloaded, hence you try to persist them as well to the same file).
Another approach to image handling would be storing the images outside of your archive file and treat them as a sort of cache. This would fix the concurrency issues and will also get rid of the performance penalty you get from archiving the channel twice for each page (when the feed is first read and later when the images have come in).
Hope this helps.
UPDATE:
At this point, its not destroying and reloading the same batch again and again, exactly what i wanted. However, it doesn't persist images if i scroll multiple times to reach the next page without the images of the previous page were loaded completely.
This is exactly what I meant saying that your architecture (shared archive/concurrent access) would probably lead to problems.
You have several options: use Core Data/sqlite; or, more easily, store each image in its own file. In the latter case, you could do following:
on retrieval, assign to each image a filename (this could be the id of the feed entry or a sequential number or whatever) and store the image data there;
store in the archive both the URL of the image and the filename where it should be stored;
when you need accessing the image, you don't get it from the archived dictionary directly; instead, you get the filename from the it then read the file from disk (if available);
this change would not affect otherwise your current implementation of rss/image retrieval, but only the way you persist the images and you access them when required (I mean, it seems a pretty easy change).
2) I am not sure how to use Bool as he explained in the answer.
add a isDownloading bool to TheFeedStore;
set it to YES in your fetchWebService: method, just before doing [connection start];
set it to NO in the completion block you pass to the connection object (again in fetchWebService:) right after archiving the feed the first time (this you are already doing);
in your scrollViewDidEndDecelerating:, at the very beginning, do:
if ([TheFeedStore sharedStore].isDownloading)
return;
so that you do not refresh the rss feed while a refresh is ongoing.
Let me know if this helps.
NEW UPDATE:
Let me sketch how you could deal with storing images in files.
In your RSSItem class, define:
#property (nonatomic, readonly) UIImage *thumbnail;
#property (nonatomic, strong) NSString *thumbFile;
thumbFile is the the path to the local file hosting the image. Once you have got the image URL (getFirstImageUrl), you can get, e.g., and MD5 hash of it and use this as your local image filename:
NSString* imageURLString = [self getFirstImageUrl:someString];
....
self.thumbFile = [imageURLString MD5String];
(MD5String is a category you can google for).
Then, in downloadThumbnails, you would store the image file locally:
NSMutableData *tempData = [NSData dataWithContentsOfURL:finalUrl];
[tempData writeToFile:[self cachedFileURLFromFileName:self.thumbFile] atomically:YES];
[[NSNotificationCenter defaultCenter] postNotificationName:#"DataSaved" object:nil];
Now, the trick is, when you access the thumbnail property, you read the image from file and return it:
- (UIImage *)thumbnail
{
NSData* d = [NSData dataWithContentsOfURL:[self cachedFileURLFromFileName:self.thumbFile]];
return [[UIImage alloc] initWithData:d];
}
in this snippet, cachedFileURLFromFileName: is defined as:
- (NSURL*)cachedFileURLFromFileName:(NSString*)filename {
NSFileManager *fileManager = [[NSFileManager alloc] init];
NSArray *fileArray = [fileManager URLsForDirectory:NSCachesDirectory inDomains:NSUserDomainMask];
NSURL* cacheURL = (NSURL*)[fileArray lastObject];
if(cacheURL)
{
return [cacheURL URLByAppendingPathComponent:filename];
}
return nil;
}
Of course, thumbFile should be persisted for this to work.
As you see, this approach is pretty "easy" to implement. This is not an optimized solution, just a quick way to make your app work with its current architecture.
For completeness, the MD5String category:
#interface NSString (MD5)
- (NSString *)MD5String;
#end
#implementation NSString (MD5)
- (NSString *)MD5String {
const char *cstr = [self UTF8String];
unsigned char result[16];
CC_MD5(cstr, strlen(cstr), result);
return [NSString stringWithFormat:
#"%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X",
result[0], result[1], result[2], result[3],
result[4], result[5], result[6], result[7],
result[8], result[9], result[10], result[11],
result[12], result[13], result[14], result[15]
];
}
#end
What you are actually trying to do, is implement paging in a UITableView
Now this is very straightforward and the best idea is to implement the paging in your UITableView delegate cellForRowAtIndexPath method, instead of doing this on the UIScrollView scrollViewDidEndDecelerating delegate method.
Here is my implementation of paging and I believe it should work perfectly for you too:
First of all, I have an implementation constants related to the paging:
//paging step size (how many items we get each time)
#define kPageStep 30
//auto paging offset (this means when we reach offset autopaging kicks in, i.e. 10 items before the end of list)
#define kPageBegin 10
The reason I'm doing this is to easily change the paging parameters on my .m file.
Here is how I do paging:
- (UITableViewCell*)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
NSUInteger row = [indexPath row];
int section = indexPath.section-1;
while (section>=0) {
row+= [self.tableView numberOfRowsInSection:section];
section--;
}
if (row+kPageBegin>=currentItems && !isLoadingNewItems && currentItems+1<maxItems) {
//begin request
[self LoadMoreItems];
}
......
}
currentItems is an integer that has the number of the tableView datasource current items.
isLoadingNewItems is a boolean that marks if items are being fetched at this moment, so we don't instantiate another request while we are loading the next batch from the server.
maxItems is an integer that indicates when to stop paging, and is an value that I retrieve from our server and set it on my initial request.
You can omit the maxItems check if you don't want to have a limit.
and in my paging loading code I set the isLoadingNewItems flag to true and set it back to false after I retrieve the data from the server.
So in your situation this would look like:
- (UITableViewCell*)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
NSUInteger row = [indexPath row];
int section = indexPath.section-1;
while (section>=0) {
row+= [self.tableView numberOfRowsInSection:section];
section--;
}
if (row+kPageBegin>=counter && !isDowloading) {
//begin request
isDowloading = YES;
[self fetchEntriesNew:counter];
}
......
}
Also there is no need to reload your whole table after adding the new rows.
Just use this:
for (int i = 0; i < itemDelta; i++) {
NSIndexPath *ip = [NSIndexPath indexPathForRow:i inSection:0];
[rows addObject:ip];
}
[self.tableView beginUpdates];
[self.tableView insertRowsAtIndexPaths:rows withRowAnimation:UITableViewRowAnimationBottom];
[self.tableView endUpdates];
A simple BOOL is enough to avoid repetitive calls:
BOOL isDowloading;
When the download is done, set it to NO. When it enters here:
if (endScrolling >= scrollView.contentSize.height)
{
NSLog(#"Scroll End Called");
NSLog(#"New Counter NOW is %d", counter);
[self fetchEntriesNew:counter];
}
put it to YES. Also don't forget to set it to NO when the requests fails.
Edit 1:
- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView
{
float endScrolling = scrollView.contentOffset.y + scrollView.frame.size.height;
if (endScrolling >= scrollView.contentSize.height)
{
if(!isDowloading)
{
isDownloading = YES;
NSLog(#"Scroll End Called");
NSLog(#"New Counter NOW is %d", counter);
[self fetchEntriesNew:counter];
}
}
}
And when you finish fetching, just set it to NO again.