Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

More tweaks to progress tracking

Run synchronous requests in custom runloop mode again
  • Loading branch information...
commit 3cbd8d6f61546030a426c9af7fd1326bbe8cd4aa 1 parent bd94484
@pokeb authored
View
13 Classes/ASIHTTPRequest.h
@@ -266,7 +266,8 @@ extern unsigned long const ASIWWANBandwidthThrottleAmount;
NSTimeInterval timeOutSeconds;
// Will be YES when a HEAD request will handle the content-length before this request starts
- BOOL shouldResetProgressIndicators;
+ BOOL shouldResetUploadProgress;
+ BOOL shouldResetDownloadProgress;
// Used by HEAD requests when showAccurateProgress is YES to preset the content-length for this request
ASIHTTPRequest *mainRequest;
@@ -385,6 +386,9 @@ extern unsigned long const ASIWWANBandwidthThrottleAmount;
// An ID that uniquely identifies this request - primarily used for debugging persistent connections
NSNumber *requestID;
+
+ // Will be ASIHTTPRequestRunLoopMode for synchronous requests, NSDefaultRunLoopMode for all other requests
+ NSString *runLoopMode;
}
#pragma mark init / dealloc
@@ -466,10 +470,10 @@ extern unsigned long const ASIWWANBandwidthThrottleAmount;
// Called when authorisation is needed, as we only find out we don't have permission to something when the upload is complete
- (void)removeUploadProgressSoFar;
-// Called when we get a content-length header and shouldResetProgressIndicators is true
+// Called when we get a content-length header and shouldResetDownloadProgress is true
- (void)incrementDownloadSizeBy:(long long)length;
-// Called when a request starts and shouldResetProgressIndicators is true
+// Called when a request starts and shouldResetUploadProgress is true
// Also called (with a negative length) to remove the size of the underlying buffer used for uploading
- (void)incrementUploadSizeBy:(long long)length;
@@ -723,7 +727,8 @@ extern unsigned long const ASIWWANBandwidthThrottleAmount;
@property (retain) NSMutableData *postBody;
@property (assign,readonly) unsigned long long contentLength;
@property (assign) unsigned long long postLength;
-@property (assign) BOOL shouldResetProgressIndicators;
+@property (assign) BOOL shouldResetDownloadProgress;
+@property (assign) BOOL shouldResetUploadProgress;
@property (assign) ASIHTTPRequest *mainRequest;
@property (assign) BOOL showAccurateProgress;
@property (assign,readonly) unsigned long long totalBytesRead;
View
75 Classes/ASIHTTPRequest.m
@@ -23,10 +23,12 @@
// Automatically set on build
-NSString *ASIHTTPRequestVersion = @"v1.6.1-15 2010-04-14";
+NSString *ASIHTTPRequestVersion = @"v1.6.1-16 2010-04-15";
NSString* const NetworkRequestErrorDomain = @"ASIHTTPRequestErrorDomain";
+static NSString *ASIHTTPRequestRunLoopMode = @"ASIHTTPRequestRunLoopMode";
+
static const CFOptionFlags kNetworkEvents = kCFStreamEventOpenCompleted | kCFStreamEventHasBytesAvailable | kCFStreamEventEndEncountered | kCFStreamEventErrorOccurred;
// In memory caches of credentials, used on when useSessionPersistence is YES
@@ -187,6 +189,7 @@ + (void)reachabilityChanged:(NSNotification *)note;
@property (retain, nonatomic) NSTimer *statusTimer;
@property (assign, nonatomic) BOOL downloadComplete;
@property (retain) NSNumber *requestID;
+@property (assign, nonatomic) NSString *runLoopMode;
@end
@@ -225,12 +228,14 @@ - (id)initWithURL:(NSURL *)newURL
self = [self init];
[self setRequestMethod:@"GET"];
+ [self setRunLoopMode:NSDefaultRunLoopMode];
[self setShouldAttemptPersistentConnection:YES];
[self setPersistentConnectionTimeoutSeconds:60.0];
[self setShouldPresentCredentialsBeforeChallenge:YES];
[self setShouldRedirect:YES];
[self setShowAccurateProgress:YES];
- [self setShouldResetProgressIndicators:YES];
+ [self setShouldResetDownloadProgress:YES];
+ [self setShouldResetUploadProgress:YES];
[self setAllowCompressedResponse:YES];
[self setDefaultResponseEncoding:NSISOLatin1StringEncoding];
[self setShouldPresentProxyAuthenticationDialog:YES];
@@ -498,6 +503,7 @@ - (void)startSynchronous
#if DEBUG_REQUEST_STATUS || DEBUG_THROTTLING
NSLog(@"Starting synchronous request %@",self);
#endif
+ [self setRunLoopMode:ASIHTTPRequestRunLoopMode];
[self setInProgress:YES];
@try {
if (![self isCancelled] && ![self complete]) {
@@ -516,7 +522,6 @@ - (void)start
{
#if TARGET_OS_IPHONE
[self performSelectorInBackground:@selector(startAsynchronous) withObject:nil];
-
#else
SInt32 versionMajor;
@@ -1015,7 +1020,7 @@ - (void)startRequest
// We've kept it open until now (when we've just opened a new stream) so that the new stream can make use of the old connection
// http://lists.apple.com/archives/Macnetworkprog/2006/Mar/msg00119.html
if (oldStream) {
- CFReadStreamClose((CFReadStreamRef)oldStream);
+ [oldStream close];
[oldStream release];
oldStream = nil;
}
@@ -1031,14 +1036,16 @@ - (void)startRequest
[[self cancelledLock] unlock];
- if (![self mainRequest] && [self shouldResetProgressIndicators]) {
- if ([self showAccurateProgress]) {
- [self incrementUploadSizeBy:[self postLength]];
- } else {
- [self incrementUploadSizeBy:1];
+ if (![self mainRequest]) {
+ if ([self shouldResetUploadProgress]) {
+ if ([self showAccurateProgress]) {
+ [self incrementUploadSizeBy:[self postLength]];
+ } else {
+ [self incrementUploadSizeBy:1];
+ }
+ [ASIHTTPRequest updateProgressIndicator:[self uploadProgressDelegate] withProgress:0 ofTotal:1];
}
- [ASIHTTPRequest updateProgressIndicator:[self uploadProgressDelegate] withProgress:0 ofTotal:1];
- if (![self partialDownloadSize]) {
+ if ([self shouldResetDownloadProgress] && ![self partialDownloadSize]) {
[ASIHTTPRequest updateProgressIndicator:[self downloadProgressDelegate] withProgress:0 ofTotal:1];
}
}
@@ -1048,12 +1055,13 @@ - (void)startRequest
[self setLastActivityTime:[NSDate date]];
- [self setStatusTimer:[NSTimer scheduledTimerWithTimeInterval:0.25 target:self selector:@selector(updateStatus:) userInfo:nil repeats:YES]];
-
+ [self setStatusTimer:[NSTimer timerWithTimeInterval:0.25 target:self selector:@selector(updateStatus:) userInfo:nil repeats:YES]];
+ [[NSRunLoop currentRunLoop] addTimer:[self statusTimer] forMode:[self runLoopMode]];
+
// If we're running asynchronously on the main thread, the runloop will already be running and we can return control
- if (![NSThread isMainThread] || [self isSynchronous]) {
+ if (![NSThread isMainThread] || [self isSynchronous] || ![[self runLoopMode] isEqualToString:NSDefaultRunLoopMode]) {
while (!complete) {
- CFRunLoopRun();
+ [[NSRunLoop currentRunLoop] runMode:[self runLoopMode] beforeDate:[NSDate distantFuture]];
}
}
}
@@ -1322,6 +1330,9 @@ - (void)updateDownloadProgress
value = 1;
[self setUpdatedProgress:YES];
}
+ if (!value) {
+ return;
+ }
[ASIHTTPRequest performSelector:@selector(request:didReceiveBytes:) onTarget:[self queue] withObject:self amount:&value];
[ASIHTTPRequest performSelector:@selector(request:didReceiveBytes:) onTarget:[self downloadProgressDelegate] withObject:self amount:&value];
@@ -1358,6 +1369,10 @@ - (void)updateUploadProgress
[self setUpdatedProgress:YES];
}
+ if (!value) {
+ return;
+ }
+
[ASIHTTPRequest performSelector:@selector(request:didSendBytes:) onTarget:[self queue] withObject:self amount:&value];
[ASIHTTPRequest performSelector:@selector(request:didSendBytes:) onTarget:[self uploadProgressDelegate] withObject:self amount:&value];
[ASIHTTPRequest updateProgressIndicator:[self uploadProgressDelegate] withProgress:[self totalBytesSent]-[self uploadBufferSize] ofTotal:[self postLength]];
@@ -1368,7 +1383,6 @@ - (void)incrementDownloadSizeBy:(long long)length
{
[ASIHTTPRequest performSelector:@selector(request:incrementDownloadSizeBy:) onTarget:[self queue] withObject:self amount:&length];
[ASIHTTPRequest performSelector:@selector(request:incrementDownloadSizeBy:) onTarget:[self uploadProgressDelegate] withObject:self amount:&length];
- [ASIHTTPRequest updateProgressIndicator:[self uploadProgressDelegate] withProgress:0 ofTotal:length];
}
@@ -1376,7 +1390,6 @@ - (void)incrementUploadSizeBy:(long long)length
{
[ASIHTTPRequest performSelector:@selector(request:incrementUploadSizeBy:) onTarget:[self queue] withObject:self amount:&length];
[ASIHTTPRequest performSelector:@selector(request:incrementUploadSizeBy:) onTarget:[self uploadProgressDelegate] withObject:self amount:&length];
- [ASIHTTPRequest updateProgressIndicator:[self uploadProgressDelegate] withProgress:0 ofTotal:length];
}
@@ -1625,18 +1638,18 @@ - (void)readResponseHeaders
SInt32 length = CFStringGetIntValue((CFStringRef)cLength);
// Workaround for Apache HEAD requests for dynamically generated content returning the wrong Content-Length when using gzip
- if ([self mainRequest] && [self allowCompressedResponse] && length == 20 && [self showAccurateProgress] && [self shouldResetProgressIndicators]) {
+ if ([self mainRequest] && [self allowCompressedResponse] && length == 20 && [self showAccurateProgress] && [self shouldResetDownloadProgress]) {
[[self mainRequest] setShowAccurateProgress:NO];
[[self mainRequest] incrementDownloadSizeBy:1];
} else {
[theRequest setContentLength:length];
- if ([self showAccurateProgress] && [self shouldResetProgressIndicators]) {
+ if ([self showAccurateProgress] && [self shouldResetDownloadProgress]) {
[theRequest incrementDownloadSizeBy:[theRequest contentLength]+[theRequest partialDownloadSize]];
}
}
- } else if ([self showAccurateProgress] && [self shouldResetProgressIndicators]) {
+ } else if ([self showAccurateProgress] && [self shouldResetDownloadProgress]) {
[theRequest setShowAccurateProgress:NO];
[theRequest incrementDownloadSizeBy:1];
}
@@ -2434,7 +2447,7 @@ - (void)handleBytesAvailable
UInt8 buffer[bufferSize];
- CFIndex bytesRead = CFReadStreamRead((CFReadStreamRef)[self readStream], buffer, sizeof(buffer));
+ NSInteger bytesRead = [[self readStream] read:buffer maxLength:sizeof(buffer)];
// Less than zero is an error
if (bytesRead < 0) {
@@ -2663,12 +2676,10 @@ - (void)destroyReadStream
[connectionsLock lock];
if (![self connectionCanBeReused]) {
- CFReadStreamUnscheduleFromRunLoop((CFReadStreamRef)[self readStream], CFRunLoopGetCurrent(), kCFRunLoopDefaultMode);
- CFReadStreamClose((CFReadStreamRef)[self readStream]);
+ [[self readStream] removeFromRunLoop:[NSRunLoop currentRunLoop] forMode:[self runLoopMode]];
+ [[self readStream] close];
[self setReadStreamIsScheduled:NO];
-
}
-
[self setReadStream:nil];
[connectionsLock unlock];
}
@@ -2679,7 +2690,7 @@ - (void)scheduleReadStream
if ([self readStream] && ![self readStreamIsScheduled]) {
// Reset the timeout
[self setLastActivityTime:[NSDate date]];
- CFReadStreamScheduleWithRunLoop((CFReadStreamRef)[self readStream], CFRunLoopGetCurrent(), kCFRunLoopDefaultMode);
+ [[self readStream] scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:[self runLoopMode]];
[self setReadStreamIsScheduled:YES];
}
}
@@ -2687,7 +2698,7 @@ - (void)scheduleReadStream
- (void)unscheduleReadStream
{
if ([self readStream] && [self readStreamIsScheduled]) {
- CFReadStreamUnscheduleFromRunLoop((CFReadStreamRef)[self readStream], CFRunLoopGetCurrent(), kCFRunLoopDefaultMode);
+ [[self readStream] removeFromRunLoop:[NSRunLoop currentRunLoop] forMode:[self runLoopMode]];
[self setReadStreamIsScheduled:NO];
}
}
@@ -2709,7 +2720,7 @@ + (void)expirePersistentConnections
#endif
NSInputStream *stream = [existingConnection objectForKey:@"stream"];
if (stream) {
- CFReadStreamClose((CFReadStreamRef)stream);
+ [stream close];
}
[persistentConnectionsPool removeObject:existingConnection];
i--;
@@ -2755,7 +2766,8 @@ - (id)copyWithZone:(NSZone *)zone
[newRequest setDidFinishSelector:[self didFinishSelector]];
[newRequest setDidFailSelector:[self didFailSelector]];
[newRequest setTimeOutSeconds:[self timeOutSeconds]];
- [newRequest setShouldResetProgressIndicators:[self shouldResetProgressIndicators]];
+ [newRequest setShouldResetDownloadProgress:[self shouldResetDownloadProgress]];
+ [newRequest setShouldResetUploadProgress:[self shouldResetUploadProgress]];
[newRequest setShowAccurateProgress:[self showAccurateProgress]];
[newRequest setDefaultResponseEncoding:[self defaultResponseEncoding]];
[newRequest setAllowResumeForFileDownloads:[self allowResumeForFileDownloads]];
@@ -3585,7 +3597,6 @@ + (void)registerForNetworkReachabilityNotifications
+ (void)unsubscribeFromNetworkReachabilityNotifications
{
[[NSNotificationCenter defaultCenter] removeObserver:self name:@"kNetworkReachabilityChangedNotification" object:nil];
-
}
+ (BOOL)isNetworkReachableViaWWAN
@@ -3691,7 +3702,8 @@ + (NSString*)base64forData:(NSData*)theData {
@synthesize contentLength;
@synthesize partialDownloadSize;
@synthesize postLength;
-@synthesize shouldResetProgressIndicators;
+@synthesize shouldResetDownloadProgress;
+@synthesize shouldResetUploadProgress;
@synthesize mainRequest;
@synthesize totalBytesRead;
@synthesize totalBytesSent;
@@ -3748,4 +3760,5 @@ + (NSString*)base64forData:(NSData*)theData {
@synthesize shouldUseRFC2616RedirectBehaviour;
@synthesize downloadComplete;
@synthesize requestID;
+@synthesize runLoopMode;
@end
View
2  Classes/ASINetworkQueue.h
@@ -55,7 +55,7 @@
int requestsCount;
// When NO, this request will only update the progress indicator when it completes
- // When YES, this request will update the progress indicator according to how much data it has recieved so far
+ // When YES, this request will update the progress indicator according to how much data it has received so far
// When YES, the queue will first perform HEAD requests for all GET requests in the queue, so it can calculate the total download size before it starts
// NO means better performance, because it skips this step for GET requests, and it won't waste time updating the progress indicator until a request completes
// Set to YES if the size of a requests in the queue varies greatly for much more accurate results
View
60 Classes/ASINetworkQueue.m
@@ -71,24 +71,12 @@ - (void)reset
[self setRequestDidFailSelector:NULL];
[self setRequestDidFinishSelector:NULL];
[self setQueueDidFinishSelector:NULL];
- [self setTotalBytesToUpload:0];
- [self setBytesUploadedSoFar:0];
- [self setTotalBytesToDownload:0];
- [self setBytesDownloadedSoFar:0];
[self setSuspended:YES];
}
- (void)go
{
- if (![self showAccurateProgress]) {
- if ([self downloadProgressDelegate]) {
- [self setTotalBytesToDownload:[self requestsCount]];
- }
- if ([self uploadProgressDelegate]) {
- [self setTotalBytesToUpload:[self requestsCount]];
- }
- }
[self setSuspended:NO];
}
@@ -172,35 +160,33 @@ - (void)addOperation:(NSOperation *)operation
// If this is a GET request and we want accurate progress, perform a HEAD request first to get the content-length
// We'll only do this before the queue is started
// If requests are added after the queue is started they will probably move the overall progress backwards anyway, so there's no value performing the HEAD requests first
- // Instead, they'll update the total progress if and when they recieve a content-length header
- if ([[request requestMethod] isEqualToString:@"GET"] && [self isSuspended]) {
- ASIHTTPRequest *HEADRequest = [request HEADRequest];
- [self addHEADOperation:HEADRequest];
-
- if ([request shouldResetProgressIndicators]) {
- [self resetProgressDelegate:[request downloadProgressDelegate]];
- }
-
- //Tell the request not to reset the progress indicator when it gets a content-length, as we will get the length from the HEAD request
- [request setShouldResetProgressIndicators:NO];
-
- [request addDependency:HEADRequest];
-
- // If we want to track uploading for this request accurately, we need to add the size of the post content to the total
- } else if (uploadProgressDelegate) {
- [request buildPostBody];
- [self setTotalBytesToUpload:[self totalBytesToUpload]+[request postLength]];
-
-
- if ([request shouldResetProgressIndicators]) {
- [self resetProgressDelegate:[request uploadProgressDelegate]];
+ // Instead, they'll update the total progress if and when they receive a content-length header
+ if ([[request requestMethod] isEqualToString:@"GET"]) {
+ if ([self isSuspended]) {
+ ASIHTTPRequest *HEADRequest = [request HEADRequest];
+ [self addHEADOperation:HEADRequest];
+ [request addDependency:HEADRequest];
+ if ([request shouldResetDownloadProgress]) {
+ [self resetProgressDelegate:[request downloadProgressDelegate]];
+ [request setShouldResetDownloadProgress:NO];
+ }
}
-
- [request setShouldResetProgressIndicators:NO];
}
+ [request buildPostBody];
+ [self request:nil incrementUploadSizeBy:[request postLength]];
+
+
+ } else {
+ [self request:nil incrementDownloadSizeBy:1];
+ [self request:nil incrementUploadSizeBy:1];
}
+ // Tell the request not to increment the upload size when it starts, as we've already added its length
+ if ([request shouldResetUploadProgress]) {
+ [self resetProgressDelegate:[request uploadProgressDelegate]];
+ [request setShouldResetUploadProgress:NO];
+ }
+
[request setShowAccurateProgress:[self showAccurateProgress]];
-
[request setQueue:self];
[super addOperation:request];
View
7 Classes/ASIProgressDelegate.h
@@ -21,17 +21,18 @@
- (void)setMaxValue:(double)newMax;
#endif
-// Called when the request recieves some data - bytes is the length of that data
-// bytes may be less than zero if a request needs to remove its progress so far
+// Called when the request receives some data - bytes is the length of that data
- (void)request:(ASIHTTPRequest *)request didReceiveBytes:(long long)bytes;
-// Called when the request sends some data.
+// Called when the request sends some data
// The first 32KB (128KB on older platforms) of data sent is not included in this amount because of limitations with the CFNetwork API
+// bytes may be less than zero if a request needs to remove upload progress (probably because the request needs to run again)
- (void)request:(ASIHTTPRequest *)request didSendBytes:(long long)bytes;
// Called when a request needs to change the length of the content to download
- (void)request:(ASIHTTPRequest *)request incrementDownloadSizeBy:(long long)newLength;
// Called when a request needs to change the length of the content to upload
+// newLength may be less than zero when a request needs to remove the size of the internal buffer from progress tracking
- (void)request:(ASIHTTPRequest *)request incrementUploadSizeBy:(long long)newLength;
@end
View
31 Classes/Tests/ASINetworkQueueTests.m
@@ -182,7 +182,10 @@ - (void)testAccurateProgressFallsBackToSimpleProgress
BOOL success = (progress == 1.0);
GHAssertTrue(success,@"Failed to increment progress properly");
- [networkQueue cancelAllOperations];
+ [networkQueue reset];
+ [networkQueue setDownloadProgressDelegate:self];
+ [networkQueue setDelegate:self];
+ [networkQueue setQueueDidFinishSelector:@selector(queueFinished:)];
// This test will request gzipped content, but the content-length header we get on the HEAD request will be wrong, ASIHTTPRequest should fall back to simple progress
// This is to workaround an issue Apache has with HEAD requests for dynamically generated content when accepting gzip - it returns the content-length of a gzipped empty body
@@ -210,14 +213,13 @@ - (void)testAddingRequestsToQueueWhileInProgress
[self setAddMoreRequestsQueue:[ASINetworkQueue queue]];
[[self addMoreRequestsQueue] setDownloadProgressDelegate:self];
[[self addMoreRequestsQueue] setDelegate:self];
- [[self addMoreRequestsQueue] setShowAccurateProgress:NO];
+ [[self addMoreRequestsQueue] setShowAccurateProgress:YES];
[[self addMoreRequestsQueue]setQueueDidFinishSelector:@selector(addMoreRequestsQueueFinished:)];
requestsFinishedCount = 0;
complete = NO;
progress = 0;
- [[self addMoreRequestsQueue] setShowAccurateProgress:YES];
int i;
for (i=0; i<5; i++) {
@@ -311,13 +313,18 @@ - (void)testUploadProgress
[[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:0.25]];
}
[[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:1]];
- BOOL success = (progress > 0.95);
+ BOOL success = (progress == 1.0f);
GHAssertTrue(success,@"Failed to increment progress properly");
//Now test again with accurate progress
complete = NO;
progress = 0;
- [networkQueue cancelAllOperations];
+ [networkQueue reset];
+ [networkQueue setUploadProgressDelegate:self];
+ [networkQueue setDelegate:self];
+ [networkQueue setShowAccurateProgress:NO];
+ [networkQueue setRequestDidFailSelector:@selector(uploadFailed:)];
+ [networkQueue setQueueDidFinishSelector:@selector(queueFinished:)];
[networkQueue setShowAccurateProgress:YES];
for (i=0; i<3; i++) {
@@ -335,7 +342,7 @@ - (void)testUploadProgress
[[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:0.25]];
}
[[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:1]];
- success = (progress > 0.95);
+ success = (progress == 1.0f);
GHAssertTrue(success,@"Failed to increment progress properly");
}
@@ -709,7 +716,7 @@ - (void)testPartialResume
success = (amountDownloaded == 1036935);
GHAssertTrue(success,@"Failed to complete the download");
- success = (progress > 0.95);
+ success = (progress == 1.0f);
GHAssertTrue(success,@"Failed to increment progress properly");
@@ -1112,6 +1119,10 @@ - (void)testCopy
- (void)testQueueFinishedCalledOnFailure
{
+ [self performSelectorOnMainThread:@selector(runTestQueueFinishedCalledOnFailureTest) withObject:nil waitUntilDone:YES];
+}
+- (void)runTestQueueFinishedCalledOnFailureTest
+{
complete = NO;
ASINetworkQueue *networkQueue = [[ASINetworkQueue queue] retain];
[networkQueue setDelegate:self];
@@ -1136,7 +1147,9 @@ - (void)testQueueFinishedCalledOnFailure
queueFinishedCallCount = 0;
complete = NO;
- [networkQueue reset];
+
+ [networkQueue release];
+ networkQueue = [[ASINetworkQueue queue] retain];
[networkQueue setDelegate:self];
[networkQueue setQueueDidFinishSelector:@selector(queueFailureFinishCallOnce:)];
[networkQueue setMaxConcurrentOperationCount:1];
@@ -1150,7 +1163,7 @@ - (void)testQueueFinishedCalledOnFailure
dateStarted = [NSDate date];
while (!complete) {
- [[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:0.5]];
+ [[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:2.0f]];
if ([dateStarted timeIntervalSinceNow] < -10) {
break;
}
Please sign in to comment.
Something went wrong with that request. Please try again.