浏览代码

A refactored version of #3086 allowing a delayed start to the network activity indicator

Kevin Harwood 9 年之前
父节点
当前提交
1a42db67b9

+ 105 - 27
Tests/Tests/AFNetworkActivityManagerTests.m

@@ -51,48 +51,126 @@
 
 #pragma mark -
 
-- (void)testThatNetworkActivityIndicatorTurnsOffIndicatorWhenRequestSucceeds {
-    XCTestExpectation *requestCompleteExpectation = [self expectationWithDescription:@"Request should succeed"];
+- (void)testThatNetworkActivityIndicatorTurnsOnAndOffIndicatorWhenRequestSucceeds {
+    self.networkActivityIndicatorManager.activationDelay = 0.0;
+    self.networkActivityIndicatorManager.completionDelay = 0.0;
+
+    XCTestExpectation *startExpectation = [self expectationWithDescription:@"Indicator Visible"];
+    XCTestExpectation *endExpectation = [self expectationWithDescription:@"Indicator Hidden"];
+    [self.networkActivityIndicatorManager setNetworkingActivityActionWithBlock:^(BOOL networkActivityIndicatorVisible) {
+        if (networkActivityIndicatorVisible) {
+            [startExpectation fulfill];
+        } else {
+            [endExpectation fulfill];
+        }
+    }];
+
+    XCTestExpectation *requestExpectation = [self expectationWithDescription:@"Request should succeed"];
     [self.sessionManager
      GET:@"/delay/1"
      parameters:nil
      success:^(NSURLSessionDataTask * _Nonnull task, id  _Nonnull responseObject) {
-         [requestCompleteExpectation fulfill];
+         [requestExpectation fulfill];
      }
      failure:nil];
-    [self expectationForPredicate:[NSPredicate predicateWithFormat:@"isNetworkActivityIndicatorVisible == YES"]
-                                               evaluatedWithObject:self.networkActivityIndicatorManager
-                                                           handler:nil];
     [self waitForExpectationsWithTimeout:10.0 handler:nil];
-
-    [self expectationForPredicate:[NSPredicate predicateWithFormat:@"isNetworkActivityIndicatorVisible == NO"]
-              evaluatedWithObject:self.networkActivityIndicatorManager
-                          handler:nil];
-    [self waitForExpectationsWithTimeout:5.0 handler:nil];
 }
 
-- (void)testThatNetworkActivityIndicatorTurnsOffIndicatorWhenRequestFails {
-    XCTestExpectation *requestCompleteExpectation = [self expectationWithDescription:@"Request should succeed"];
+- (void)testThatNetworkActivityIndicatorTurnsOnAndOffIndicatorWhenRequestFails {
+    self.networkActivityIndicatorManager.activationDelay = 0.0;
+    self.networkActivityIndicatorManager.completionDelay = 0.0;
+
+    XCTestExpectation *startExpectation = [self expectationWithDescription:@"Indicator Visible"];
+    XCTestExpectation *endExpectation = [self expectationWithDescription:@"Indicator Hidden"];
+    [self.networkActivityIndicatorManager setNetworkingActivityActionWithBlock:^(BOOL networkActivityIndicatorVisible) {
+        if (networkActivityIndicatorVisible) {
+            [startExpectation fulfill];
+        } else {
+            [endExpectation fulfill];
+        }
+    }];
+
+    XCTestExpectation *requestExpectation = [self expectationWithDescription:@"Request should fail"];
     [self.sessionManager
-     GET:@"/status/500"
+     GET:@"/status/404"
      parameters:nil
      success:nil
-     failure:^(NSURLSessionDataTask * _Nonnull task, NSError * _Nonnull error) {
-         [requestCompleteExpectation fulfill];
+     failure:^(NSURLSessionDataTask * _Nullable task, NSError * _Nonnull error) {
+         [requestExpectation fulfill];
      }];
+    [self waitForExpectationsWithTimeout:10.0 handler:nil];
+}
 
-    [self
-     keyValueObservingExpectationForObject:self.networkActivityIndicatorManager
-     keyPath:@"isNetworkActivityIndicatorVisible"
-     handler:^BOOL(AFNetworkActivityIndicatorManager * observedObject, NSDictionary * _Nonnull change) {
-         return observedObject.isNetworkActivityIndicatorVisible;
-     }];
-    [self waitForExpectationsWithTimeout:5.0 handler:nil];
+- (void)testThatVisibilityDelaysAreApplied {
+
+    self.networkActivityIndicatorManager.activationDelay = 1.0;
+    self.networkActivityIndicatorManager.completionDelay = 1.0;
+
+    CFTimeInterval requestStartTime = CACurrentMediaTime();
+    __block CFTimeInterval requestEndTime;
+    __block CFTimeInterval indicatorVisbleTime;
+    __block CFTimeInterval indicatorHiddenTime;
+    XCTestExpectation *startExpectation = [self expectationWithDescription:@"Indicator Visible"];
+    XCTestExpectation *endExpectation = [self expectationWithDescription:@"Indicator Hidden"];
+    [self.networkActivityIndicatorManager setNetworkingActivityActionWithBlock:^(BOOL networkActivityIndicatorVisible) {
+        if (networkActivityIndicatorVisible) {
+             indicatorVisbleTime = CACurrentMediaTime();
+            [startExpectation fulfill];
+        } else {
+            indicatorHiddenTime = CACurrentMediaTime();
+            [endExpectation fulfill];
+        }
+    }];
+
+    XCTestExpectation *requestExpectation = [self expectationWithDescription:@"Request should succeed"];
+    [self.sessionManager
+     GET:@"/delay/2"
+     parameters:nil
+     success:^(NSURLSessionDataTask * _Nonnull task, id  _Nonnull responseObject) {
+         requestEndTime = CACurrentMediaTime();
+         [requestExpectation fulfill];
+     }
+     failure:nil];
+    [self waitForExpectationsWithTimeout:10.0 handler:nil];
+    XCTAssertTrue((indicatorVisbleTime - requestStartTime) > self.networkActivityIndicatorManager.activationDelay);
+    XCTAssertTrue((indicatorHiddenTime - requestEndTime) > self.networkActivityIndicatorManager.completionDelay);
+}
+
+- (void)testThatIndicatorBlockIsOnlyCalledOnceEachForStartAndEndForMultipleRequests {
+    self.networkActivityIndicatorManager.activationDelay = 1.0;
+    self.networkActivityIndicatorManager.completionDelay = 1.0;
+
+    XCTestExpectation *startExpectation = [self expectationWithDescription:@"Indicator Visible"];
+    XCTestExpectation *endExpectation = [self expectationWithDescription:@"Indicator Hidden"];
+    [self.networkActivityIndicatorManager setNetworkingActivityActionWithBlock:^(BOOL networkActivityIndicatorVisible) {
+        if (networkActivityIndicatorVisible) {
+            [startExpectation fulfill];
+        } else {
+            [endExpectation fulfill];
+        }
+    }];
+
+    XCTestExpectation *requestExpectation = [self expectationWithDescription:@"Request should succeed"];
+    [self.sessionManager
+     GET:@"/delay/4"
+     parameters:nil
+     success:^(NSURLSessionDataTask * _Nonnull task, id  _Nonnull responseObject) {
+         [requestExpectation fulfill];
+     }
+     failure:nil];
+
+    XCTestExpectation *secondRequestExpectation = [self expectationWithDescription:@"Request should succeed"];
+    [self.sessionManager
+     GET:@"/delay/2"
+     parameters:nil
+     success:^(NSURLSessionDataTask * _Nonnull task, id  _Nonnull responseObject) {
+
+         [secondRequestExpectation fulfill];
+     }
+     failure:nil];
+
+    [self waitForExpectationsWithTimeout:10.0 handler:nil];
 
-    [self expectationForPredicate:[NSPredicate predicateWithFormat:@"isNetworkActivityIndicatorVisible == NO"]
-              evaluatedWithObject:self.networkActivityIndicatorManager
-                          handler:nil];
-    [self waitForExpectationsWithTimeout:5.0 handler:nil];
 }
 
 @end

+ 25 - 2
UIKit+AFNetworking/AFNetworkActivityIndicatorManager.h

@@ -52,9 +52,25 @@ NS_EXTENSION_UNAVAILABLE_IOS("Use view controller based solutions where appropri
 @property (nonatomic, assign, getter = isEnabled) BOOL enabled;
 
 /**
- A Boolean value indicating whether the network activity indicator is currently displayed in the status bar.
+ A Boolean value indicating whether the network activity indicator manager is currently active.
+*/
+@property (readonly, nonatomic, assign, getter=isNetworkActivityIndicatorVisible) BOOL networkActivityIndicatorVisible;
+
+/**
+ A time interval indicating the minimum duration of networking activity that should occur before the activity indicator is displayed. The default value 1 second. If the network activity indicator should be displayed immediately when network activity occurs, this value should be set to 0 seconds.
+ 
+ Apple's HIG describes the following:
+
+ > Display the network activity indicator to provide feedback when your app accesses the network for more than a couple of seconds. If the operation finishes sooner than that, you don’t have to show the network activity indicator, because the indicator is likely to disappear before users notice its presence.
+
+ */
+@property (nonatomic, assign) NSTimeInterval activationDelay;
+
+/**
+ A time interval indicating the duration of time of no networking activity required before the activity indicator is disabled. This allows for continuous display of the network activity indicator across multiple requests. The default value is 0.17 seconds.
  */
-@property (readonly, nonatomic, assign) BOOL isNetworkActivityIndicatorVisible;
+
+@property (nonatomic, assign) NSTimeInterval completionDelay;
 
 /**
  Returns the shared network activity indicator manager object for the system.
@@ -73,6 +89,13 @@ NS_EXTENSION_UNAVAILABLE_IOS("Use view controller based solutions where appropri
  */
 - (void)decrementActivityCount;
 
+/**
+ Set the a custom method to be executed when the network activity indicator manager should be hidden/shown. By default, this is null, and the UIApplication Network Activity Indicator will be managed automatically. If this block is set, it is the responsiblity of the caller to manager the network activity indicator going forward.
+
+ @param block A block to be executed when the network activity indicator status changes.
+ */
+- (void)setNetworkingActivityActionWithBlock:(nullable void (^)(BOOL networkActivityIndicatorVisible))block;
+
 @end
 
 NS_ASSUME_NONNULL_END

+ 133 - 27
UIKit+AFNetworking/AFNetworkActivityIndicatorManager.m

@@ -24,7 +24,15 @@
 #if TARGET_OS_IOS
 #import "AFURLSessionManager.h"
 
-static NSTimeInterval const kAFNetworkActivityIndicatorInvisibilityDelay = 0.17;
+typedef NS_ENUM(NSInteger, AFNetworkActivityManagerState) {
+    AFNetworkActivityManagerStateNotActive,
+    AFNetworkActivityManagerStateDelayingStart,
+    AFNetworkActivityManagerStateActive,
+    AFNetworkActivityManagerStateDelayingEnd
+};
+
+static NSTimeInterval const kDefaultAFNetworkActivityManagerActivationDelay = 1.0;
+static NSTimeInterval const kDefaultAFNetworkActivityManagerCompletionDelay = 0.17;
 
 static NSURLRequest * AFNetworkRequestFromNotification(NSNotification *notification) {
     if ([[notification object] respondsToSelector:@selector(originalRequest)]) {
@@ -34,17 +42,21 @@ static NSURLRequest * AFNetworkRequestFromNotification(NSNotification *notificat
     }
 }
 
+typedef void (^AFNetworkActivityActionBlock)(BOOL networkActivityIndicatorVisible);
+
 @interface AFNetworkActivityIndicatorManager ()
 @property (readwrite, nonatomic, assign) NSInteger activityCount;
-@property (readwrite, nonatomic, strong) NSTimer *activityIndicatorVisibilityTimer;
-@property (readonly, nonatomic, getter = isNetworkActivityIndicatorVisible) BOOL networkActivityIndicatorVisible;
+@property (readwrite, nonatomic, strong) NSTimer *activationDelayTimer;
+@property (readwrite, nonatomic, strong) NSTimer *completionDelayTimer;
+@property (readonly, nonatomic, getter = isNetworkActivityOccurring) BOOL networkActivityOccurring;
+@property (nonatomic, copy) AFNetworkActivityActionBlock networkActivityActionBlock;
+@property (nonatomic, assign) AFNetworkActivityManagerState currentState;
+@property (nonatomic, assign, getter=isNetworkActivityIndicatorVisible) BOOL networkActivityIndicatorVisible;
 
-- (void)updateNetworkActivityIndicatorVisibility;
-- (void)updateNetworkActivityIndicatorVisibilityDelayed;
+- (void)updateCurrentStateForNetworkActivityChange;
 @end
 
 @implementation AFNetworkActivityIndicatorManager
-@dynamic networkActivityIndicatorVisible;
 
 + (instancetype)sharedManager {
     static AFNetworkActivityIndicatorManager *_sharedManager = nil;
@@ -56,18 +68,17 @@ static NSURLRequest * AFNetworkRequestFromNotification(NSNotification *notificat
     return _sharedManager;
 }
 
-+ (NSSet *)keyPathsForValuesAffectingIsNetworkActivityIndicatorVisible {
-    return [NSSet setWithObject:@"activityCount"];
-}
-
 - (instancetype)init {
     self = [super init];
     if (!self) {
         return nil;
     }
+    self.currentState = AFNetworkActivityManagerStateNotActive;
     [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(networkRequestDidStart:) name:AFNetworkingTaskDidResumeNotification object:nil];
     [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(networkRequestDidFinish:) name:AFNetworkingTaskDidSuspendNotification object:nil];
     [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(networkRequestDidFinish:) name:AFNetworkingTaskDidCompleteNotification object:nil];
+    self.activationDelay = kDefaultAFNetworkActivityManagerActivationDelay;
+    self.completionDelay = kDefaultAFNetworkActivityManagerCompletionDelay;
 
     return self;
 }
@@ -75,28 +86,38 @@ static NSURLRequest * AFNetworkRequestFromNotification(NSNotification *notificat
 - (void)dealloc {
     [[NSNotificationCenter defaultCenter] removeObserver:self];
 
-    [_activityIndicatorVisibilityTimer invalidate];
+    [_activationDelayTimer invalidate];
+    [_completionDelayTimer invalidate];
 }
 
-- (void)updateNetworkActivityIndicatorVisibilityDelayed {
-    if (self.enabled) {
-        // Delay hiding of activity indicator for a short interval, to avoid flickering
-        if (![self isNetworkActivityIndicatorVisible]) {
-            [self.activityIndicatorVisibilityTimer invalidate];
-            self.activityIndicatorVisibilityTimer = [NSTimer timerWithTimeInterval:kAFNetworkActivityIndicatorInvisibilityDelay target:self selector:@selector(updateNetworkActivityIndicatorVisibility) userInfo:nil repeats:NO];
-            [[NSRunLoop mainRunLoop] addTimer:self.activityIndicatorVisibilityTimer forMode:NSRunLoopCommonModes];
-        } else {
-            [self performSelectorOnMainThread:@selector(updateNetworkActivityIndicatorVisibility) withObject:nil waitUntilDone:NO modes:@[NSRunLoopCommonModes]];
-        }
+- (void)setEnabled:(BOOL)enabled {
+    _enabled = enabled;
+    if (enabled == NO) {
+        [self setCurrentState:AFNetworkActivityManagerStateNotActive];
     }
 }
 
-- (BOOL)isNetworkActivityIndicatorVisible {
+- (void)setNetworkingActivityActionWithBlock:(void (^)(BOOL networkActivityIndicatorVisible))block {
+    self.networkActivityActionBlock = block;
+}
+
+- (BOOL)isNetworkActivityOccurring {
     return self.activityCount > 0;
 }
 
-- (void)updateNetworkActivityIndicatorVisibility {
-    [[UIApplication sharedApplication] setNetworkActivityIndicatorVisible:[self isNetworkActivityIndicatorVisible]];
+- (void)setNetworkActivityIndicatorVisible:(BOOL)networkActivityIndicatorVisible {
+    if (_networkActivityIndicatorVisible != networkActivityIndicatorVisible) {
+        [self willChangeValueForKey:@"networkActivityIndicatorVisible"];
+        @synchronized(self) {
+             _networkActivityIndicatorVisible = networkActivityIndicatorVisible;
+        }
+        [self didChangeValueForKey:@"networkActivityIndicatorVisible"];
+        if (self.networkActivityActionBlock) {
+            self.networkActivityActionBlock(networkActivityIndicatorVisible);
+        } else {
+            [[UIApplication sharedApplication] setNetworkActivityIndicatorVisible:networkActivityIndicatorVisible];
+        }
+    }
 }
 
 - (void)setActivityCount:(NSInteger)activityCount {
@@ -105,7 +126,7 @@ static NSURLRequest * AFNetworkRequestFromNotification(NSNotification *notificat
 	}
 
     dispatch_async(dispatch_get_main_queue(), ^{
-        [self updateNetworkActivityIndicatorVisibilityDelayed];
+        [self updateCurrentStateForNetworkActivityChange];
     });
 }
 
@@ -117,7 +138,7 @@ static NSURLRequest * AFNetworkRequestFromNotification(NSNotification *notificat
     [self didChangeValueForKey:@"activityCount"];
 
     dispatch_async(dispatch_get_main_queue(), ^{
-        [self updateNetworkActivityIndicatorVisibilityDelayed];
+        [self updateCurrentStateForNetworkActivityChange];
     });
 }
 
@@ -132,7 +153,7 @@ static NSURLRequest * AFNetworkRequestFromNotification(NSNotification *notificat
     [self didChangeValueForKey:@"activityCount"];
 
     dispatch_async(dispatch_get_main_queue(), ^{
-        [self updateNetworkActivityIndicatorVisibilityDelayed];
+        [self updateCurrentStateForNetworkActivityChange];
     });
 }
 
@@ -148,6 +169,91 @@ static NSURLRequest * AFNetworkRequestFromNotification(NSNotification *notificat
     }
 }
 
+#pragma mark - Internal State Management
+- (void)setCurrentState:(AFNetworkActivityManagerState)currentState {
+    @synchronized(self) {
+        if (_currentState != currentState) {
+            [self willChangeValueForKey:@"currentState"];
+            _currentState = currentState;
+            switch (currentState) {
+                case AFNetworkActivityManagerStateNotActive:
+                    [self cancelActivationDelayTimer];
+                    [self cancelCompletionDelayTimer];
+                    [self setNetworkActivityIndicatorVisible:NO];
+                    break;
+                case AFNetworkActivityManagerStateDelayingStart:
+                    [self startActivationDelayTimer];
+                    break;
+                case AFNetworkActivityManagerStateActive:
+                    [self cancelCompletionDelayTimer];
+                    [self setNetworkActivityIndicatorVisible:YES];
+                    break;
+                case AFNetworkActivityManagerStateDelayingEnd:
+                    [self startCompletionDelayTimer];
+                    break;
+            }
+        }
+        [self didChangeValueForKey:@"currentState"];
+    }
+}
+
+- (void)updateCurrentStateForNetworkActivityChange {
+    if (self.enabled) {
+        switch (self.currentState) {
+            case AFNetworkActivityManagerStateNotActive:
+                if (self.isNetworkActivityOccurring) {
+                    [self setCurrentState:AFNetworkActivityManagerStateDelayingStart];
+                }
+                break;
+            case AFNetworkActivityManagerStateDelayingStart:
+                //No op. Let the delay timer finish out.
+                break;
+            case AFNetworkActivityManagerStateActive:
+                if (!self.isNetworkActivityOccurring) {
+                    [self setCurrentState:AFNetworkActivityManagerStateDelayingEnd];
+                }
+                break;
+            case AFNetworkActivityManagerStateDelayingEnd:
+                if (self.isNetworkActivityOccurring) {
+                    [self setCurrentState:AFNetworkActivityManagerStateActive];
+                }
+                break;
+        }
+    }
+}
+
+- (void)startActivationDelayTimer {
+    self.activationDelayTimer = [NSTimer
+                                 timerWithTimeInterval:self.activationDelay target:self selector:@selector(activationDelayTimerFired) userInfo:nil repeats:NO];
+    [[NSRunLoop mainRunLoop] addTimer:self.activationDelayTimer forMode:NSRunLoopCommonModes];
+}
+
+- (void)activationDelayTimerFired {
+    if (self.networkActivityOccurring) {
+        [self setCurrentState:AFNetworkActivityManagerStateActive];
+    } else {
+        [self setCurrentState:AFNetworkActivityManagerStateNotActive];
+    }
+}
+
+- (void)startCompletionDelayTimer {
+    [self.completionDelayTimer invalidate];
+    self.completionDelayTimer = [NSTimer timerWithTimeInterval:self.completionDelay target:self selector:@selector(completionDelayTimerFired) userInfo:nil repeats:NO];
+    [[NSRunLoop mainRunLoop] addTimer:self.completionDelayTimer forMode:NSRunLoopCommonModes];
+}
+
+- (void)completionDelayTimerFired {
+    [self setCurrentState:AFNetworkActivityManagerStateNotActive];
+}
+
+- (void)cancelActivationDelayTimer {
+    [self.activationDelayTimer invalidate];
+}
+
+- (void)cancelCompletionDelayTimer {
+    [self.completionDelayTimer invalidate];
+}
+
 @end
 
 #endif