diff --git a/BIObjCHelpers.xcodeproj/project.pbxproj b/BIObjCHelpers.xcodeproj/project.pbxproj index 09c2c6c..0f4f3b8 100644 --- a/BIObjCHelpers.xcodeproj/project.pbxproj +++ b/BIObjCHelpers.xcodeproj/project.pbxproj @@ -9,6 +9,9 @@ /* Begin PBXBuildFile section */ 431124921CE36CDC00950918 /* BIOperationNotifier.h in Headers */ = {isa = PBXBuildFile; fileRef = 431124901CE36CDC00950918 /* BIOperationNotifier.h */; settings = {ATTRIBUTES = (Public, ); }; }; 431124931CE36CDC00950918 /* BIOperationNotifier.m in Sources */ = {isa = PBXBuildFile; fileRef = 431124911CE36CDC00950918 /* BIOperationNotifier.m */; }; + 43A5B5531CFD6EF900F79359 /* BIOperationBase.h in Headers */ = {isa = PBXBuildFile; fileRef = 43A5B5511CFD6EF900F79359 /* BIOperationBase.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 43A5B5541CFD6EF900F79359 /* BIOperationBase.m in Sources */ = {isa = PBXBuildFile; fileRef = 43A5B5521CFD6EF900F79359 /* BIOperationBase.m */; }; + 43A5B5571CFD733300F79359 /* BIOperationBaseTestCase.m in Sources */ = {isa = PBXBuildFile; fileRef = 43A5B5561CFD733300F79359 /* BIOperationBaseTestCase.m */; }; 43F541CD1CD0F276002EB6C6 /* BIObjCHelpers.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 43F541C31CD0F275002EB6C6 /* BIObjCHelpers.framework */; }; 43F541DC1CD0F2AD002EB6C6 /* _BIScrollViewProxy.m in Sources */ = {isa = PBXBuildFile; fileRef = 91614A821B94790400D00EB2 /* _BIScrollViewProxy.m */; }; 43F541DE1CD0F2AD002EB6C6 /* UIScrollView+BIBatching.m in Sources */ = {isa = PBXBuildFile; fileRef = 432001971C7B26C0006A8BB7 /* UIScrollView+BIBatching.m */; }; @@ -142,6 +145,9 @@ 4355334F1B81B87F0052A128 /* _BITableView+Internal.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = "_BITableView+Internal.h"; path = "Views/TableView/_BITableView+Internal.h"; sourceTree = ""; }; 435D59BE1B622E8A00ECA859 /* BIMockHandlerTableView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = BIMockHandlerTableView.h; path = BIHandlerTableView/BIMockHandlerTableView.h; sourceTree = ""; }; 435D59BF1B622E8A00ECA859 /* BIMockHandlerTableView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = BIMockHandlerTableView.m; path = BIHandlerTableView/BIMockHandlerTableView.m; sourceTree = ""; }; + 43A5B5511CFD6EF900F79359 /* BIOperationBase.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = BIOperationBase.h; path = Operations/BIOperationBase.h; sourceTree = ""; }; + 43A5B5521CFD6EF900F79359 /* BIOperationBase.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = BIOperationBase.m; path = Operations/BIOperationBase.m; sourceTree = ""; }; + 43A5B5561CFD733300F79359 /* BIOperationBaseTestCase.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = BIOperationBaseTestCase.m; path = Operations/BIOperationBaseTestCase.m; sourceTree = ""; }; 43A7D0251C735C1B007C8CC0 /* LICENSE.txt */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = LICENSE.txt; sourceTree = ""; }; 43A7D0261C735C1B007C8CC0 /* UIScrollView+InfiniteScroll.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "UIScrollView+InfiniteScroll.h"; sourceTree = ""; }; 43A7D0271C735C1B007C8CC0 /* UIScrollView+InfiniteScroll.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "UIScrollView+InfiniteScroll.m"; sourceTree = ""; }; @@ -283,6 +289,8 @@ 4311248D1CE36CA200950918 /* Operations */ = { isa = PBXGroup; children = ( + 43A5B5511CFD6EF900F79359 /* BIOperationBase.h */, + 43A5B5521CFD6EF900F79359 /* BIOperationBase.m */, 431124901CE36CDC00950918 /* BIOperationNotifier.h */, 431124911CE36CDC00950918 /* BIOperationNotifier.m */, ); @@ -322,6 +330,14 @@ name = BIHandlerTableView; sourceTree = ""; }; + 43A5B5551CFD731B00F79359 /* Operations */ = { + isa = PBXGroup; + children = ( + 43A5B5561CFD733300F79359 /* BIOperationBaseTestCase.m */, + ); + name = Operations; + sourceTree = ""; + }; 43A7D01E1C7349DA007C8CC0 /* ExternalLibs */ = { isa = PBXGroup; children = ( @@ -563,6 +579,7 @@ 43BE6CDF1B568ECB001F0A00 /* Datasource */, 43BE6CEA1B568ECB001F0A00 /* Handler */, 43BE6CED1B568ECB001F0A00 /* Lifecycle */, + 43A5B5551CFD731B00F79359 /* Operations */, 43BE6CEF1B568ECB001F0A00 /* OperationQueue */, 43BE6CF21B568ECB001F0A00 /* Starters */, ); @@ -954,6 +971,7 @@ 43F542341CD0F417002EB6C6 /* NSDate+BIAttributedString.h in Headers */, 43F5423C1CD0F429002EB6C6 /* BIDatasourceTableView.h in Headers */, 43F542381CD0F424002EB6C6 /* BIDatasourceFetchedCollectionView.h in Headers */, + 43A5B5531CFD6EF900F79359 /* BIOperationBase.h in Headers */, 43F542351CD0F41A002EB6C6 /* NSString+BIExtra.h in Headers */, 43F5422C1CD0F393002EB6C6 /* BITableViewCell.h in Headers */, 43F542361CD0F422002EB6C6 /* BIDatasourceBase.h in Headers */, @@ -1190,6 +1208,7 @@ 43F5421E1CD0F2AE002EB6C6 /* BIStartersFactory.m in Sources */, 43F542201CD0F2AE002EB6C6 /* BILaunchStartersFactory.m in Sources */, 43F542231CD0F2AE002EB6C6 /* UIScrollView+InfiniteScroll.m in Sources */, + 43A5B5541CFD6EF900F79359 /* BIOperationBase.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -1200,6 +1219,7 @@ 43FA5D5E1CD2325200FA754E /* BIDatasourceTableViewTestCase.m in Sources */, 43FA5D611CD2325B00FA754E /* BIMockDatasourceFeedTableView.m in Sources */, 43FA5D651CD2326A00FA754E /* BIOperationQueueTestCase.m in Sources */, + 43A5B5571CFD733300F79359 /* BIOperationBaseTestCase.m in Sources */, 43FA5D541CD2321E00FA754E /* BITableViewTestCase.m in Sources */, 43FA5D521CD2321900FA754E /* BIActivityIndicatorContainerViewTestCase.m in Sources */, 43FA5D661CD2326F00FA754E /* BIStartersFactoryTestCase.m in Sources */, diff --git a/BIObjCHelpers/Operations/BIOperationBase.h b/BIObjCHelpers/Operations/BIOperationBase.h new file mode 100644 index 0000000..6f51f6e --- /dev/null +++ b/BIObjCHelpers/Operations/BIOperationBase.h @@ -0,0 +1,36 @@ +// +// BIOperationBase.h +// BIObjCHelpers +// +// Created by Bogdan Iusco on 5/31/16. +// Copyright © 2016 iGama Apps. All rights reserved. +// + +#import + +extern void dispatchCodeOnMainThread(void(^__nonnull codeBlock)()); + +@interface BIOperationBase : NSOperation + +// NSOperation +@property (nonatomic, assign, readonly) BOOL isExecuting; +@property (nonatomic, assign, readonly) BOOL isFinished; + +// Callbacks +@property (nonatomic, copy, nullable) void(^didFinishWithErrorCallback)(NSError *__nonnull error); +@property (nonatomic, copy, nullable) void(^didFinishSuccessfullyCallback)(id __nonnull responseObject); +@property (nonatomic, copy, nullable) void(^didFinishCommonCallback)(); +@property (nonatomic, assign) BOOL runCallbacksOnMainThread; + +// Handle operations response states +- (void)handleDidFinishedWithResponse:(nonnull id)response; +- (void)handleDidFinishedWithError:(nonnull NSError *)error; +- (void)handleDidFinishedCommon; +- (void)finish; + +// Trigger callbacks +- (void)safeCallDidFinishWithErrorCallback:(nonnull NSError *)error; +- (void)safeCallDidFinishSuccessfullyCallback:(nonnull id)responseObject; +- (void)safeCallDidFinishCommon; + +@end diff --git a/BIObjCHelpers/Operations/BIOperationBase.m b/BIObjCHelpers/Operations/BIOperationBase.m new file mode 100644 index 0000000..d86081f --- /dev/null +++ b/BIObjCHelpers/Operations/BIOperationBase.m @@ -0,0 +1,116 @@ +// +// BIOperationBase.m +// BIObjCHelpers +// +// Created by Bogdan Iusco on 5/31/16. +// Copyright © 2016 iGama Apps. All rights reserved. +// + +#import "BIOperationBase.h" + +void dispatchCodeOnMainThread(void(^__nonnull codeBlock)()) { + if (![NSThread isMainThread]) { + dispatch_async(dispatch_get_main_queue(), ^{ + codeBlock(); + }); + } else { + codeBlock(); + } +} + +@implementation BIOperationBase + +#pragma mark - Init methods + +- (nonnull instancetype)init { + self = [super init]; + if (self) { + self.runCallbacksOnMainThread = YES; + } + return self; +} + +#pragma mark - NSOperation methods + +- (void)start { + dispatchCodeOnMainThread( ^{ + [self willChangeValueForKey:@"isExecuting"]; + _isExecuting = YES; + [self didChangeValueForKey:@"isExecuting"]; + }); +} + +#pragma mark - Public methods + +- (void)safeCallDidFinishWithErrorCallback:(nonnull NSError *)error { + void(^block)() = ^{ + if (self.didFinishWithErrorCallback) { + self.didFinishWithErrorCallback(error); + } + [self handleDidFinishedCommon]; + }; + if (self.runCallbacksOnMainThread) { + dispatchCodeOnMainThread( ^{ + block(); + }); + } else { + block(); + } +} + +- (void)safeCallDidFinishSuccessfullyCallback:(nonnull id)responseObject { + void(^block)() = ^{ + if (self.didFinishSuccessfullyCallback) { + self.didFinishSuccessfullyCallback(responseObject); + } + [self handleDidFinishedCommon]; + }; + if (self.runCallbacksOnMainThread) { + dispatchCodeOnMainThread( ^{ + block(); + }); + } else { + block(); + } +} + +- (void)safeCallDidFinishCommon { + void(^block)() = ^{ + if (self.didFinishCommonCallback) { + self.didFinishCommonCallback(); + } + }; + if (self.runCallbacksOnMainThread) { + dispatchCodeOnMainThread( ^{ + block(); + }); + } else { + block(); + } +} + +- (void)handleDidFinishedWithResponse:(id)response { + [self safeCallDidFinishSuccessfullyCallback:response]; +} + +- (void)handleDidFinishedWithError:(NSError *)error { + [self safeCallDidFinishWithErrorCallback:error]; +} + +- (void)handleDidFinishedCommon { + [self safeCallDidFinishCommon]; + [self finish]; +} + +- (void)finish { + dispatchCodeOnMainThread( ^{ + [self willChangeValueForKey:@"isExecuting"]; + [self willChangeValueForKey:@"isFinished"]; + _isExecuting = NO; + _isFinished = YES; + [self didChangeValueForKey:@"isExecuting"]; + [self didChangeValueForKey:@"isFinished"]; + }); +} + +@end diff --git a/BIObjCHelpers/Operations/BIOperationNotifier.h b/BIObjCHelpers/Operations/BIOperationNotifier.h index 1457b46..e6b07c8 100644 --- a/BIObjCHelpers/Operations/BIOperationNotifier.h +++ b/BIObjCHelpers/Operations/BIOperationNotifier.h @@ -6,6 +6,8 @@ // Copyright © 2016 iGama Apps. All rights reserved. // +#import + #import @class BIOperationNotifier; @@ -19,16 +21,9 @@ @end -@interface BIOperationNotifier : NSOperation - -@property (nonatomic, copy, nullable) void(^didFinishWithErrorCallback)(NSError *__nonnull error); -@property (nonatomic, copy, nullable) void(^didFinishSuccessfullyCallback)(id __nonnull responseObject); -@property (nonatomic, copy, nullable) void(^didFinishCommonCallback)(); +@interface BIOperationNotifier : BIOperationBase - (void)registerOperationFinishedListener:(id __nonnull)listener; - (void)unregisterOperationFinishedListener:(id __nonnull)listener; -- (void)safeCallDidFinishWithErrorCallback:(nonnull NSError *)error; -- (void)safeCallDidFinishSuccessfullyCallback:(nonnull id)responseObject; - @end diff --git a/BIObjCHelpers/Operations/BIOperationNotifier.m b/BIObjCHelpers/Operations/BIOperationNotifier.m index e829e5e..3c7b6a1 100644 --- a/BIObjCHelpers/Operations/BIOperationNotifier.m +++ b/BIObjCHelpers/Operations/BIOperationNotifier.m @@ -16,34 +16,57 @@ @interface BIOperationNotifier() @implementation BIOperationNotifier +#pragma mark - Public methods + +- (void)registerOperationFinishedListener:(id)listener { + [self.operationFinishedListeners addObject:listener]; +} + +- (void)unregisterOperationFinishedListener:(id)listener { + [self.operationFinishedListeners removeObject:listener]; +} + +#pragma mark - BIOperationBase methods + - (void)safeCallDidFinishWithErrorCallback:(nonnull NSError *)error { - dispatch_async(dispatch_get_main_queue(), ^{ + void(^block)() = ^{ [self BI_notifyOperationDidFinishWithError:error]; - [self safeCallDidFinishCommon]; - }); + [self handleDidFinishedCommon]; + }; + if (self.runCallbacksOnMainThread) { + dispatchCodeOnMainThread( ^{ + block(); + }); + } else { + block(); + } } - (void)safeCallDidFinishSuccessfullyCallback:(nonnull id)responseObject { - dispatch_async(dispatch_get_main_queue(), ^{ + void(^block)() = ^{ [self BI_notifyOperationDidFinishWithResponse:responseObject]; - [self safeCallDidFinishCommon]; - }); + [self handleDidFinishedCommon]; + }; + if (self.runCallbacksOnMainThread) { + dispatchCodeOnMainThread( ^{ + block(); + }); + } else { + block(); + } } - (void)safeCallDidFinishCommon { - dispatch_async(dispatch_get_main_queue(), ^{ + void(^block)() = ^{ [self BI_notifyOperationDidFinishCommon]; - }); -} - -#pragma mark - Public methods - -- (void)registerOperationFinishedListener:(id)listener { - [self.operationFinishedListeners addObject:listener]; -} - -- (void)unregisterOperationFinishedListener:(id)listener { - [self.operationFinishedListeners removeObject:listener]; + }; + if (self.runCallbacksOnMainThread) { + dispatchCodeOnMainThread( ^{ + block(); + }); + } else { + block(); + } } #pragma mark - Properties methods diff --git a/BIObjCHelpers/SupportingFiles/BIObjCHelpers.h b/BIObjCHelpers/SupportingFiles/BIObjCHelpers.h index ec286f2..bbdf97a 100644 --- a/BIObjCHelpers/SupportingFiles/BIObjCHelpers.h +++ b/BIObjCHelpers/SupportingFiles/BIObjCHelpers.h @@ -24,6 +24,7 @@ FOUNDATION_EXPORT const unsigned char BIObjCHelpersVersionString[]; #import #import #import +#import // Views #import diff --git a/BIObjCHelpersTests/Tests/Operations/BIOperationBaseTestCase.m b/BIObjCHelpersTests/Tests/Operations/BIOperationBaseTestCase.m new file mode 100644 index 0000000..f27226b --- /dev/null +++ b/BIObjCHelpersTests/Tests/Operations/BIOperationBaseTestCase.m @@ -0,0 +1,146 @@ +// +// BIOperationBaseTestCase.m +// BIObjCHelpers +// +// Created by Bogdan Iusco on 5/31/16. +// Copyright © 2016 iGama Apps. All rights reserved. +// + +#import +#import + +@interface BIOperationBaseTestCase : XCTestCase + +@property (nonatomic, strong, nullable, readwrite) BIOperationBase *operation; + +@end + +@implementation BIOperationBaseTestCase + +#pragma mark - Setup methods + +- (void)setUp { + [super setUp]; + self.operation = [BIOperationBase new]; +} + +#pragma mark - Test start + +- (void)test_start { + [self.operation start]; + XCTAssertTrue(self.operation.isExecuting); + XCTAssertFalse(self.operation.isFinished); + XCTAssertTrue(self.operation.runCallbacksOnMainThread); +} + +#pragma mark - Test finish + +- (void)test_finish { + [self.operation start]; + [self.operation finish]; + XCTAssertFalse(self.operation.isExecuting); + XCTAssertTrue(self.operation.isFinished); +} + +#pragma mark - Test handleDidFinishedWithResponse + +- (void)test_handleDidFinishedWithResponseMainThread { + XCTestExpectation *expectation = [self expectationWithDescription:@"Call"]; + __block NSNumber *receivedResponse; + self.operation.didFinishSuccessfullyCallback = ^(NSNumber *response) { + receivedResponse = response; + [expectation fulfill]; + }; + [self.operation start]; + NSNumber *sentReponse = @(YES); + [self.operation handleDidFinishedWithResponse:sentReponse]; + [self waitForExpectationsWithTimeout:2 handler:nil]; + XCTAssertEqualObjects(receivedResponse, sentReponse); +} + +- (void)test_handleDidFinishedWithResponseGlobalQueue { + XCTestExpectation *expectation = [self expectationWithDescription:@"Call"]; + __block NSNumber *receivedResponse; + self.operation.didFinishSuccessfullyCallback = ^(NSNumber *response) { + receivedResponse = response; + [expectation fulfill]; + }; + [self.operation start]; + NSNumber *sentReponse = @(YES); + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + [self.operation handleDidFinishedWithResponse:sentReponse]; + }); + [self waitForExpectationsWithTimeout:2 handler:nil]; + XCTAssertEqualObjects(receivedResponse, sentReponse); +} + +#pragma mark - Test handleDidFinishedWithError + +- (void)test_handleDidFinishedWithErrorMainThread { + XCTestExpectation *expectation = [self expectationWithDescription:@"Call"]; + __block NSError *receivedError; + self.operation.didFinishWithErrorCallback = ^(NSError *error) { + receivedError = error; + [expectation fulfill]; + }; + [self.operation start]; + NSError *sentError = [NSError errorWithDomain:@"test domain" code:100 userInfo:nil]; + [self.operation handleDidFinishedWithError:sentError]; + [self waitForExpectationsWithTimeout:2 handler:nil]; + XCTAssertEqualObjects(receivedError, sentError); +} + +- (void)test_handleDidFinishedWithErrorGlobalQueue { + XCTestExpectation *expectation = [self expectationWithDescription:@"Call"]; + __block NSError *receivedError; + self.operation.didFinishWithErrorCallback = ^(NSError *error) { + receivedError = error; + [expectation fulfill]; + }; + [self.operation start]; + NSError *sentError = [NSError errorWithDomain:@"test domain" code:100 userInfo:nil]; + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + [self.operation handleDidFinishedWithError:sentError]; + }); + + [self waitForExpectationsWithTimeout:2 handler:nil]; + XCTAssertEqualObjects(receivedError, sentError); +} + +#pragma mark - Test handleDidFinishedCommon + +- (void)test_handleDidFinishedCommonSuccess { + XCTestExpectation *expectation = [self expectationWithDescription:@"Call"]; + __block BOOL wasCalled = NO; + self.operation.didFinishCommonCallback = ^() { + wasCalled = YES; + [expectation fulfill]; + }; + [self.operation start]; + [self.operation handleDidFinishedWithResponse:@(YES)]; + [self waitForExpectationsWithTimeout:2 handler:nil]; + XCTAssertTrue(wasCalled); + XCTAssertFalse(self.operation.isExecuting); + XCTAssertTrue(self.operation.isFinished); +} + +- (void)test_handleDidFinishedCommonError { + XCTestExpectation *expectation = [self expectationWithDescription:@"Call"]; + __block BOOL wasCalled = NO; + self.operation.didFinishCommonCallback = ^() { + wasCalled = YES; + [expectation fulfill]; + }; + [self.operation start]; + NSError *sentError = [NSError errorWithDomain:@"test domain" code:100 userInfo:nil]; + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + [self.operation handleDidFinishedWithError:sentError]; + }); + + [self waitForExpectationsWithTimeout:2 handler:nil]; + XCTAssertTrue(wasCalled); + XCTAssertFalse(self.operation.isExecuting); + XCTAssertTrue(self.operation.isFinished); +} + +@end