Guys I'm working on the Newsstand stuff now. I'm trying to handle network errors.
What you see on the image below is my simple log ("Percentage: %i" is inside connection:didWriteData:totalBytesWritten:expectedTotalBytes:).
My problem is depicted in the last 3 lines of code.
What I've done in this lines:
After that line I've switched on the airplane mode (simulated network error)
I've received connection:didWriteData:totalBytesWritten:expectedTotalBytes: with totalBytesWritten equal to expectedTotalBytes
I've received connectionDidFinishDownloading:(NSURLConnection *)connection destinationURL:(NSURL *)destinationURL.
After that:
Hooray, I've just finished downloading my .zip, I can unpack it, announce the status to my view and so on... :(
My question is what's going on?
I have implemented connection:didFailWithError: but it's not invoked.
I was trying to grab the totalBytesWritten in last invoked didWriteData: and compare it to real file size in DidFinishDownloading:
I have stripped all my project away just to make sure that its not related to my whole design.
I'm thinking about combination of NSTimer and NKIssueContentStatusAvailable to check the real download status.
It's all hacky. Isn't it?
Update:
Reproduced on iOS 6 and 7 with XCode 5
All NewsstandKit methods invoked on the main thread
Same thing when simulating offline mode with Charles proxy (app in foreground)
It's not an issue anymore when switching to Airplane, but still can reproduce the issue when throttling on Charles proxy.
I ended up with this solution (checking if connection:didWriteData:... is telling the truth in connectionDidFinishDownloading:destinationURL:):
- (void)connection:(NSURLConnection *)connection didWriteData:(long long)bytesWritten totalBytesWritten:(long long)totalBytesWritten expectedTotalBytes:(long long)expectedTotalBytes
{
...
self.declaredSizeOfDownloadedFile = expectedTotalBytes;
}
And:
- (void)connectionDidFinishDownloading:(NSURLConnection *)connection destinationURL:(NSURL *) destinationURL
{
NSDictionary* fileAttributes = [[NSFileManager defaultManager] attributesOfItemAtPath:destinationURL.absoluteString error:nil];
NSNumber* destinationFileSize = [fileAttributes objectForKey:NSFileSize];
if (destinationFileSize.intValue != self.declaredSizeOfDownloadedFile)
{
NSError* error = ...;
[self connection:connection didFailWithError:error];
self.declaredSizeOfDownloadedFile = 0;
return;
}
...
}
Related
So i want to build a iOS, i am pretty new to the world of objective-c and one feature i want to implement is the ability to send a API request and do a bit of background processing while the app is not "in focus/in background". I have researched for a couple days about this BGTask API for iOS 13 and have created a projected to see if i can get "background fetch" working. I have not be able to. Im pretty sure i have everything setup correctly but i can not get background fetch functionality to trigger on my iPhone, not even once over the past couple days.
I am using a actual iOS device to test this with iOS 13.4.1
"Permitted background task scheduler identifiers" is setup properly in Info.plist
App is signed
Background processing and Background fetch is checked in Background Modes
I waited the 15 minute interval as per Apples documentation
Here is my code. All this is just a blank iOS project using objective-c. I only edited AppDelegate.m and Info.plist
AppDelegate.m
#import "AppDelegate.h"
#import <BackgroundTasks/BackgroundTasks.h>
static NSString* TaskID = #"com.myapp.task";
#interface AppDelegate ()
#end
#implementation AppDelegate
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
// Override point for customization after application launch.
[[BGTaskScheduler sharedScheduler] registerForTaskWithIdentifier:TaskID
usingQueue:nil
launchHandler:^(BGProcessingTask *task) {
[self handleAppRefreshTask:task];
}];
return YES;
}
#pragma mark - UISceneSession lifecycle
- (UISceneConfiguration *)application:(UIApplication *)application configurationForConnectingSceneSession:(UISceneSession *)connectingSceneSession options:(UISceneConnectionOptions *)options {
// Called when a new scene session is being created.
// Use this method to select a configuration to create the new scene with.
return [[UISceneConfiguration alloc] initWithName:#"Default Configuration" sessionRole:connectingSceneSession.role];
}
- (void)application:(UIApplication *)application didDiscardSceneSessions:(NSSet<UISceneSession *> *)sceneSessions {
// Called when the user discards a scene session.
// If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions.
// Use this method to release any resources that were specific to the discarded scenes, as they will not return.
}
-(void)handleAppRefreshTask:(BGProcessingTask *)task {
//do things with task
NSLog(#"Process started!");
task.expirationHandler = ^{
NSLog(#"WARNING: expired before finish was executed.");
};
NSString *targetUrl = #"https://webhook.site/1b274a6f-016f-4edf-8e31-4ed7058eaeac";
NSMutableURLRequest *request = [[NSMutableURLRequest alloc] init];
[request setHTTPMethod:#"GET"];
[request setURL:[NSURL URLWithString:targetUrl]];
[[[NSURLSession sharedSession] dataTaskWithRequest:request completionHandler:
^(NSData * _Nullable data,
NSURLResponse * _Nullable response,
NSError * _Nullable error) {
NSString *myString = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
NSLog(#"Data received: %#", myString);
}] resume];
task.expirationHandler = ^{
NSLog(#"WARNING: expired before finish was executed.");
};
[task setTaskCompletedWithSuccess:YES];
}
- (void)applicationDidEnterBackground:(UIApplication *)application
{
NSLog(#"Entering background");
BGProcessingTaskRequest *request = [[BGProcessingTaskRequest alloc] initWithIdentifier:TaskID];
request.requiresNetworkConnectivity = true;
request.requiresExternalPower = false;
request.earliestBeginDate = [NSDate dateWithTimeIntervalSinceNow:60];
#try {
[[BGTaskScheduler sharedScheduler] submitTaskRequest:request error:nil];
}
#catch(NSException *e){
NSLog(#" Unable to submit request");
}
}
#end
Is background fetch broken in iOS 13? Even clicking on the “Simulate background fetch" in Xcode debug menu does not work. It just closes the app and nothing happens. Can anybody help/give any advice?
A few observations:
The setTaskCompletedWithSuccess should be inside the network request’s completion handler. You don’t want to mark the task as complete until the request has had a chance to run and you’ve processed the result.
You are calling submitTaskRequest, but passing nil for the NSError reference. You have also wrapped that in an exception handler. But this API call doesn’t throw exceptions, but rather just passes back errors. But you have to supply it an error reference. E.g.
NSLog(#"Entering background");
BGProcessingTaskRequest *request = [[BGProcessingTaskRequest alloc] initWithIdentifier:TaskID];
request.requiresNetworkConnectivity = true;
request.requiresExternalPower = false;
request.earliestBeginDate = [NSDate dateWithTimeIntervalSinceNow:60];
NSError *error;
if (![[BGTaskScheduler sharedScheduler] submitTaskRequest:request error:&error]) {
NSLog(#"BGTaskScheduler failed: %#", error);
}
In your code, if it failed, you would never know.
You have placed this code in applicationDidEnterBackground. I.e., are you seeing this “Entering background” message at all? The reason I ask is that if you’ve supplied a scene delegate (common if you just created a new iOS 13 app), this method won’t be called, whereas sceneDidEnterBackground will.
You said that you tried “Simulate background fetch”. But you haven’t created a background fetch request (a BGAppRefreshTask). You created a background task (a BGProcessingTask), which is a different thing. To test background processing requests, refer to Starting and Terminating Tasks During Development.
There’s an interesting question as to how you know that the fetch request was processed. You’re just using NSLog (which presumes that you’re keeping your app attached to the Xcode debugger). I would suggest testing this without the app being attached to Xcode. There are a few options:
If you can watch your server logs for requests, that works.
I personally will often put in UserNotifications (and make sure to go into settings and turn on persistent notifications so I don’t miss them).
Another approach that I’ve done is to log these events in some table in my app’s persistent storage and then have some UI within the app to fetch this data so I can confirm what happened.
I’ll often use Unified Logging so that I can watch os_log statements issued by my device from the macOS Console even when Xcode is not running. This is very useful in logging app/scene methods. See WWDC 2016 Unified Logging and Activity Tracing
Whatever you do, for things like background processing, background app refresh, etc., I will program some mechanism so that I can check to see if the requests/tasks took place, even when not attached to Xcode. Being attached to the debugger can, in some cases, affect the app lifecycle, and I want to make sure I’ve got some way to confirm what was going on without the benefit of the console.
Likely obvious, but make sure you never “force quit” the app, as that will stop background processes from taking place.
For more information, See WWDC 2019 video Advances in App Background Execution.
I've built an app for iOS 9 and WatchOS 2. The iOS app will periodically transfer image files from the iPhone to the Watch. Sometimes, these are pushed from the app, sometimes the Watch requests (pulls) them. If pulled, I make the requests asynchronous, and use the exact same iOS code to transfer images in both cases.
About half the time (maybe 2/3), the file transfer works. The other times, it appears that nothing happens. This is the same whether I'm pushing or pulling images.
On the iOS side, I use code similar to this (session activated already):
if ([WCSession isSupported]) {
WCSession *session = [WCSession defaultSession];
if (session.reachable) {
NSData *imgData = UIImagePNGRepresentation(img);
NSURL *tempFile = [[session watchDirectoryURL] URLByAppendingPathComponent: #"camera.png"];
BOOL success = [imgData writeToFile: [tempFile path] atomically: NO];
if (success) {
NSLog(#"transferFile:metadata:");
[session transferFile: tempFile metadata: nil];
} else {
NSLog(#"will not call transferFile:metadata:");
}
} else {
NSLog(#"Camera watch client not reachable.");
}
}
On the watch extension side, I have a singleton that activates the watch session and receives the file:
- (void)session:(WCSession *)session didReceiveFile:(WCSessionFile *)file {
// pass the data file to the data listener (if any)
[self.dataListener session: session didReceiveFile: file];
}
My "data listener" converts the file to a UIImage and displays it on the UI thread. However, that's probably irrelevant, as the unsuccessful operations never get that far.
During unsuccessful transfers, session:didReceiveFile: is never called. If I inspect the iOS app's log, however, I see these messages only during the operations that fail:
Dec 26 15:10:47 hostname companionappd[74893]: (Note ) WatchKit:
application (com.mycompany.MyApp.watchkitapp), install status: 2,
message: application install success
Dec 26 15:10:47 hostname
companionappd[74893]: (Note ) WatchKit: Purging
com.mycompany.MyApp.watchkitapp from installation queue, 0 apps
remaining
What is happening here? It looks like the app is trying to reinstall the Watch app (?). When this is happening, I do not see the watch app crash/close and restart. It simply does nothing. No file received.
On the iOS side, I scale down the image to about 136x170 px, so the PNG files shouldn't be too big.
Any ideas what's going wrong?
Update:
I have posted a complete, minimal project that demonstrates the problem on Github here
I am now under the impression that this is a bug in the simulators. It seems to work more reliably on the Apple Watch hardware. Not sure if it's 100% reliable, though.
Apple bug report filed (#24023088). Will update status if there is any, and leave unsolved for any potential answers that may provide workarounds.
For me, not a single transfer was working anymore. Polling transfer.progress showed isTransferring == true, but I never got beyond 0 completed units.
I ended up:
Deleting apps on watch and iPhone
Rebooting both
Reinstalling
And it works.
This is how I managed to transfer files from phone to watch:
In order for this to work, the file must be locate in appGroupFolder, and "App Groups" must be enabled from Capabilities tab, for phone and watch.
In order to get appGroup folder use following line of code:
NSURL * myFileLocationFolder = [[NSFileManager defaultManager] containerURLForSecurityApplicationGroupIdentifier: #"myGroupID"]; //something like group.bundle.projName
Once you got that use this to send message and handle response from watch:
[session sendMessage:#{#"file":myFileURL.absoluteString} replyHandler:^(NSDictionary<NSString *,id> * _Nonnull replyMessage) {
//got reply
} errorHandler:^(NSError * _Nonnull error) {
//got Error
}];
Even though WCSession *session = [WCSession defaultSession]; I have noticed that sometimes session is deallocated, so you might consider using [WCSession defaultSession]; instead.
To catch this on the phone use:
- (void)session:(WCSession *)session didReceiveMessage:(NSDictionary<NSString *, id> *)message replyHandler:(void(^)(NSDictionary<NSString *, id> *replyMessage))replyHandler{
//message[#"file"] - addres to my file
//do stuff with it here
replyHandler(#{#"myResponse":#"responseData"}); //this call triggers replyHandler block on the watch
}
Now a if you didn't forget to implement WCSessionDelegate and use
if ([WCSession isSupported]) {
_session = [WCSession defaultSession];
_session.delegate = self;
[_session activateSession];
}
//here session is #property (strong, nonatomic) WCSession * session;
It all should work.
Made a broader answer, hopefully will reach out to more people.
In my iphone app, I am displaying information of files added to documents directory, in a table view, as soon as those are added. For this I am using DirectoryWatcher class provided in one of the sample codes by apple.
Below is the block of code showing its use:
- (void)viewDidLoad
{
// start monitoring the document directory…
self.aDirectoryWatcher = [DirectoryWatcher watchFolderWithPath:[self applicationDocumentsDirectory] delegate:self];
// scan for existing documents
[self directoryDidChange:self.aDirectoryWatcher];
}
- (void)directoryDidChange:(DirectoryWatcher *)folderWatcher
{
[self reconcileData];
}
One of the information displayed in table view cell is- file size, which I am obtaining as below:
NSDictionary *fileAttributes = [[NSFileManager defaultManager] attributesOfItemAtPath:[fileURL path] error:nil];
NSNumber * size = [fileAttributes objectForKey:NSFileSize];
Problem is-
When I am trying to add a large file, such as a movie file, then as
soon as transfer starts (copy or move operation) it invokes
directoryDidChange: immediately. It did not wait unless the transfer
is complete. So I always get size as 0.
In case of small sized files, such as images, it works fine.
Now I have two question:
Is there any way to know the complete size of file, which is in transfer state. eg. if message displayed is copying 30 MB of 100 MB, I want to get 100 MB?
Is there any alternative of DirectoryWatcher, which notifies only when file is completely added?
Please suggest.
You are currently looking to the file system, you should look into the response headers from your download requests.
For example when you use NSURLConnection to download the file, you can implement the delegate method connection:didReceiveResponse: and look into the response headers.
- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response
{
if ([response isKindOfClass:[NSHTTPURLResponse class]]) {
NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *)response;
NSLog(#"Expected content length: %lld", httpResponse.expectedContentLength);
}
}
To get notified when it's finished you can implement connectionDidFinishLoading:
- (void)connectionDidFinishLoading:(NSURLConnection *)connection {
// Notify download success
}
and
- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error {
// Notify error
}
My application, during a process I've written, is rising in memory and seems to not be releasing it.
The first thing I'd like to mention is that the basic outline of what I've written is this:
- Request a url (fetching data using NSData -initWithContentsOfURL)
- Parse NSData into NSArray of NSDictionarys using NSJSONSerialization +JSONObjectWithStream
- Loop through decoded NSArray inserting/updating/deleting records in a sqlite database using the FMDB framework with the data decoded
The application does the above, however it does it in a loop for an undetermined period of time, in which the application displays a "Loading" HUD. I thought it may be worth mentioning this, although I find it insignificant how many times it does this process, as that shouldn't affect the memory usage if it were releasing properly. If I am wrong here, please advise me.
My code works fine, well, it does what it's intended to do. However, when I profile the application code, the memory seems to just keep rising. It does drop in segments throughout, but overall it keeps rising (IE doesn't release in full what it previously used).
I have, as previously stated, profiled the application with Allocations, Leaks, VM Tracker, and used Trace Highlights.
Trace Highlights: Shows that the memory usage is gradually going up, but dropping some memory (not all) meaning if the process is running for long enough the memory will reach high usage and terminate.
Allocations: Seems OK. The allocations has spikes but always comes back down to where it started. I took heapshots and they always drop down leaving maximum 500-700kb per segment (left for about 10 minutes)
VM Tracker: Proves to show that memory consistently rises, and is not releasing full memory (as discovered in trace highlights). Resident seems to get really high
Leaks: No leaks found in application
Here's some screenshots of Allocations/VM Tracker running:
It's worth noting that I have in fact tried:
- Adding autoreleasepools
- "force releasing" by assigning each properties; such as NSURLs, NSRequests, etc; to nil
My questions:
- Should I be doing something special to release the memory?
- How could I further debug this issue?
- How best can I find out what's wrong from the data Instruments gives me?
---- EDIT: ----
Here's the code that sends the url request to fetch the data.:
- (void) requestAndParse : (NSString *)url
{
NSURL *theURL;
ASIHTTPRequest *request;
NSData *collectedData;
NSError *error;
#try {
// File cache the NSData
theURL = [[NSURL alloc] initWithString: url];
request = [ASIHTTPRequest requestWithURL: theURL];
[request setDownloadDestinationPath: [[NSHomeDirectory() stringByAppendingPathComponent:#"Documents"] stringByAppendingString:#"/cachefile.txt"]];
[request startSynchronous];
[request waitUntilFinished];
collectedData = [[NSData alloc] initWithContentsOfFile:[[NSHomeDirectory() stringByAppendingPathComponent:#"Documents"] stringByAppendingString:#"/cachefile.txt"]];
if ([collectedData length] > 0) {
records = [NSJSONSerialization JSONObjectWithData:collectedData options:NSJSONReadingMutableContainers error:&error];
}
}
#catch (NSException *exception) {
// Failed
NSLog(#"Parse error: %#", error);
}
#finally {
// DB updates with the records here
...
// remove file
[[NSFileManager defaultManager] removeItemAtPath:[[NSHomeDirectory() stringByAppendingPathComponent:#"Documents"] stringByAppendingString:#"/cachefile.txt"] error:nil];
// release properties used
collectedData = nil;
request = nil;
theURL = nil;
}
}
This above method is called from within a while loop in the Application Delegate. The while loop is an undetermined length, as previously mentioned.
--- EDIT 2: ---
The following is what happens within the #finally statement (updating the SQLite database using FMDB). There are a lot of these methods in my class, one for each table. They all follow the same pattern though, as they are all duplicated from the first one:
-(BOOL) insertBatchOfRecords:(NSArray *)records {
__block BOOL queueReturned = YES;
#autoreleasepool {
FMDatabaseQueue *dbQueue = [self instantiateDatabaseQueue];
[dbQueue inTransaction:^(FMDatabase *tdb, BOOL *rollback) {
if (![tdb open]) {
NSLog(#"Couldn't open DB inside Transaction");
queueReturned = NO;
*rollback = YES;
return;
}
for (NSDictionary *record in records) {
[tdb executeUpdate:#"INSERT OR REPLACE INTO table (attr1, attr2) VALUES (?,?)", [record valueForKey:#"attr1"], [record valueForKey:#"attr2"]];
if ([tdb hadError]) {
queueReturned = NO;
*rollback = YES;
NSLog(#"Failed to insert records because %#", [tdb lastErrorMessage]);
return;
}
}
}];
[dbQueue close];
dbQueue = nil;
}
return queueReturned;
}
And follows is the -instantiateDatabaseQueue method:
-(FMDatabaseQueue *) instantiateDatabaseQueue {
#autoreleasepool {
return [FMDatabaseQueue databaseQueueWithPath: [self.getDocumentsDirectory stringByAppendingPathComponent:#"localdb.db"]];
}
}
The autoreleasepools may make it messy, but the code originally did not have these. I implemented them in various locations to see if there was any improvement (there was not).
--- EDIT 3 ---
I have been profiling the application the past few days, and still have no luck in finding an answer. I have separated the part of the app in question to a separate project of it's own, to make sure that it is indeed this causing the memory usage. This proved to be correct, as the app is still acting the same.
I have taken further profiling pictures, and am still having a hard time identifying what is actually wrong. See below that the allocations looks OK (the VM also doesn't look too bad to me?), and there's still no leaks (no picture of this, because there's none!!)
However, when I profiled on Trace Highlights, the memory usage just keeps going up, until reaching too much usage (around 70+MB on a 3GS) and then crashes due to using so much memory.
I reduced the problem by using ASIHTTPRequest for grabbing the NSData (stores to file instead). Please see revised code above. However, problem still persists, just takes longer to happen!
As per originally, question:
- Is there something wrong with the second part of this app process?
Using try / catch in iOS with ARC can cause memory leaks, and is best avoided.
an alternative approach is to use an async NSURLConnection, or an NSOperation with a synch NSURLConnection.
My game needs to fill tableView cells with a bunch of things from my server database. This has been working fine. Then I upgraded Xcode to 4.6 and targeted iOS6.1, to please the App Review Team folks. Now, one of my connections never completes. (All of the other Posts seem to work correctly, as always.) Here's my post:
- (void) fillCells {
Cell_QtoA *newCell = [[Cell_QtoA alloc] initCellUser:usrID Grp:grpID Qtn:0 Gnm:#"na" Cat:#"na" Sit:#"na" Pfl:#"na" Lks:0 isA:0 ddA:0 ];
NSMutableURLRequest *reqPost = [SimplePost urlencodedRequestWithURL:[NSURL URLWithString:kFillCells] andDataDictionary:[newCell toDictC]];
(void) [[NSURLConnection alloc] initWithRequest:reqPost delegate:self];
}
I think it's working fine. The PHP and database haven't changed. Everything worked great yesterday, before the upgrades. Here's my connection method:
- (void) connection:(NSURLConnection *)connection didReceiveData:(NSData *)data {
NSLog(#"data = %#", data);
NSString *error;
NSArray *array = (NSArray *)[NSPropertyListSerialization propertyListFromData:data mutabilityOption:NSPropertyListImmutable format:0 errorDescription:&error];
if( error ) {
NSLog(#"Error = %#", error);
return;
}
NSLog(#"1st object in array of %d is %#", [array count], array );
}
Because I suspected net speeds to be an issue, I added a timer to the call, which I never needed before:
timer = [NSTimer scheduledTimerWithTimeInterval:2 target:self selector:#selector(fillCells) userInfo:nil repeats:YES];
The timer didn't help. Still get errors:
"Unexpected EOF" and "Unexpected character z (or whatever) at Line 1"
The NSLog of data shows hex data that appears cut off, like:
<3c3f786d 6c207665 ... 63743e0a 3c2f6172 7261793e 0a3c2f70 6c697374 3e>
It's like the reception is being interrupted. Anyone know what's happening here? Thanks!
You aren't using all the response data; as mention in the NSURLConnectionDelegate reference:
The newly available data. The delegate should concatenate the contents
of each data object delivered to build up the complete data for a URL
load.
So you need to create an NSData instance variable; clear before the request and append to it whenever new data arrives. Then use the didFinishLoading delegate method to trigger the call to propertyListFromData with the complete response.