From 861b105cc4b7c9cc7932ab01e091ae3e31527ecd Mon Sep 17 00:00:00 2001 From: alexey novikov Date: Sat, 31 Mar 2018 12:51:23 +0200 Subject: [PATCH] Protocols proposal + dataSource sample implementation --- CodeChallenge.xcodeproj/project.pbxproj | 22 +++++ .../BusinessLogicControllerProtocol.h | 20 +++++ .../Protocols/Proposal/DataProviderProtocol.h | 38 +++++++++ .../Proposal/NetworkManagerProtocol.h | 34 ++++++++ .../Proposal/PersistencyManagerProtocol.h | 19 +++++ .../Proposal/RepositoriesDataProvider.h | 13 +++ .../Proposal/RepositoriesDataProvider.m | 81 +++++++++++++++++++ 7 files changed, 227 insertions(+) create mode 100644 CodeChallenge/Protocols/Proposal/BusinessLogicControllerProtocol.h create mode 100644 CodeChallenge/Protocols/Proposal/DataProviderProtocol.h create mode 100644 CodeChallenge/Protocols/Proposal/NetworkManagerProtocol.h create mode 100644 CodeChallenge/Protocols/Proposal/PersistencyManagerProtocol.h create mode 100644 CodeChallenge/Protocols/Proposal/RepositoriesDataProvider.h create mode 100644 CodeChallenge/Protocols/Proposal/RepositoriesDataProvider.m diff --git a/CodeChallenge.xcodeproj/project.pbxproj b/CodeChallenge.xcodeproj/project.pbxproj index d6784c5..2a118f5 100644 --- a/CodeChallenge.xcodeproj/project.pbxproj +++ b/CodeChallenge.xcodeproj/project.pbxproj @@ -24,6 +24,7 @@ 8AF0EBB4206F7E6800AF374C /* NSDictionary+Networking.m in Sources */ = {isa = PBXBuildFile; fileRef = 8AF0EBB3206F7E6800AF374C /* NSDictionary+Networking.m */; }; 8AF0EBB9206F875300AF374C /* Avatar.m in Sources */ = {isa = PBXBuildFile; fileRef = 8AF0EBB5206F875200AF374C /* Avatar.m */; }; 8AF0EBBA206F875300AF374C /* _Avatar.m in Sources */ = {isa = PBXBuildFile; fileRef = 8AF0EBB6206F875200AF374C /* _Avatar.m */; }; + 8AF0EBC4206F9DD500AF374C /* RepositoriesDataProvider.m in Sources */ = {isa = PBXBuildFile; fileRef = 8AF0EBC3206F9DD500AF374C /* RepositoriesDataProvider.m */; }; 8B616C5B24EB85483A33F7B5 /* libPods-CodeChallenge.a in Frameworks */ = {isa = PBXBuildFile; fileRef = BE5C9355D5670BBC457EF045 /* libPods-CodeChallenge.a */; }; /* End PBXBuildFile section */ @@ -60,6 +61,12 @@ 8AF0EBB6206F875200AF374C /* _Avatar.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = _Avatar.m; sourceTree = ""; }; 8AF0EBB7206F875300AF374C /* Avatar.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = Avatar.h; sourceTree = ""; }; 8AF0EBB8206F875300AF374C /* _Avatar.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = _Avatar.h; sourceTree = ""; }; + 8AF0EBBB206F94DE00AF374C /* NetworkManagerProtocol.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = NetworkManagerProtocol.h; sourceTree = ""; }; + 8AF0EBBD206F973600AF374C /* DataProviderProtocol.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = DataProviderProtocol.h; sourceTree = ""; }; + 8AF0EBBE206F992400AF374C /* BusinessLogicControllerProtocol.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = BusinessLogicControllerProtocol.h; sourceTree = ""; }; + 8AF0EBC0206F99EF00AF374C /* PersistencyManagerProtocol.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = PersistencyManagerProtocol.h; sourceTree = ""; }; + 8AF0EBC2206F9DD500AF374C /* RepositoriesDataProvider.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = RepositoriesDataProvider.h; sourceTree = ""; }; + 8AF0EBC3206F9DD500AF374C /* RepositoriesDataProvider.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = RepositoriesDataProvider.m; sourceTree = ""; }; A33A15A295127B1F83B1C5AA /* Pods-CodeChallenge.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-CodeChallenge.debug.xcconfig"; path = "Pods/Target Support Files/Pods-CodeChallenge/Pods-CodeChallenge.debug.xcconfig"; sourceTree = ""; }; BA308E849395E49A61DB7D9A /* Pods-CodeChallenge.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-CodeChallenge.release.xcconfig"; path = "Pods/Target Support Files/Pods-CodeChallenge/Pods-CodeChallenge.release.xcconfig"; sourceTree = ""; }; BE5C9355D5670BBC457EF045 /* libPods-CodeChallenge.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-CodeChallenge.a"; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -98,6 +105,7 @@ 6B5FAB5F206F883500371E40 /* Protocols */ = { isa = PBXGroup; children = ( + 8AF0EBC1206F9C0E00AF374C /* Proposal */, 6B5FAB5A206F7E2C00371E40 /* FeedProtocol.h */, 6B5FAB5E206F870D00371E40 /* FetchDelegate.h */, ); @@ -173,6 +181,19 @@ path = Categories; sourceTree = ""; }; + 8AF0EBC1206F9C0E00AF374C /* Proposal */ = { + isa = PBXGroup; + children = ( + 8AF0EBBE206F992400AF374C /* BusinessLogicControllerProtocol.h */, + 8AF0EBBD206F973600AF374C /* DataProviderProtocol.h */, + 8AF0EBBB206F94DE00AF374C /* NetworkManagerProtocol.h */, + 8AF0EBC0206F99EF00AF374C /* PersistencyManagerProtocol.h */, + 8AF0EBC2206F9DD500AF374C /* RepositoriesDataProvider.h */, + 8AF0EBC3206F9DD500AF374C /* RepositoriesDataProvider.m */, + ); + path = Proposal; + sourceTree = ""; + }; 8E9A3E0E896FD7902B4985B0 /* Pods */ = { isa = PBXGroup; children = ( @@ -313,6 +334,7 @@ 8AF0EBAC206F7C7300AF374C /* Commit.m in Sources */, 8AF0EBBA206F875300AF374C /* _Avatar.m in Sources */, 8AC26785206EC0550083E45A /* ViewController.m in Sources */, + 8AF0EBC4206F9DD500AF374C /* RepositoriesDataProvider.m in Sources */, 8AC26790206EC0550083E45A /* main.m in Sources */, 8AF0EBAE206F7C7300AF374C /* _Commit.m in Sources */, 8AC26782206EC0550083E45A /* AppDelegate.m in Sources */, diff --git a/CodeChallenge/Protocols/Proposal/BusinessLogicControllerProtocol.h b/CodeChallenge/Protocols/Proposal/BusinessLogicControllerProtocol.h new file mode 100644 index 0000000..e850db8 --- /dev/null +++ b/CodeChallenge/Protocols/Proposal/BusinessLogicControllerProtocol.h @@ -0,0 +1,20 @@ +// +// BusinessLogicControllerProtocol.h +// CodeChallenge +// +// Created by alexey novikov on 31.03.2018. +// + +#import +#import "Repository.h" + +// This is the top-level abstraction protocol +// Will be implemented by a business logic controller, which will be accessible from the view controllers. +// Implementation of such BL controller will incorporate the logic for making network requests (JSON+image data) +// and persistency. + +@protocol BusinessLogicControllerProtocol +@optional + - (void)loadRepositoriesWithOffset:(NSUInteger)offset limit:(NSUInteger)limit; + - (void)loadCommitsForRepository:(Repository *)repository limit:(NSUInteger)limit; +@end diff --git a/CodeChallenge/Protocols/Proposal/DataProviderProtocol.h b/CodeChallenge/Protocols/Proposal/DataProviderProtocol.h new file mode 100644 index 0000000..6c22a0a --- /dev/null +++ b/CodeChallenge/Protocols/Proposal/DataProviderProtocol.h @@ -0,0 +1,38 @@ +// +// DataProviderProtocol.h +// CodeChallenge +// +// Created by alexey novikov on 31.03.2018. +// + +#import + +@protocol DataConsumer; + + +// for each of two of our UIViewControllers there will be two different objects implementing this protocol. +// UIViewControllers will have strong references to those objects, and provide a callback reference to themselves via +// the `dataConsumer` property. +// Each object that will implement this method will have an NSFetchedResultsController instance inside of it, +// and will be the delegate for that NSFetchedResultsController. These implementation details are never exposed, +// thus providing abstraction for the data layer. +@protocol DataProviderProtocol +@optional + - (id)cellModelAtRow:(NSUInteger)row; + + @property (nonatomic, readonly) NSUInteger numberOfRows; + + // our UIViewController. will be notified in two different cases: + // 1. when the incorporated NSFetchedResultsController fires controllerDidChangeContent: or + // 2. an image has been downloaded for a row. + @property (weak, nonatomic) id dataConsumer; +@end + + +// UIViewControllers will implement this and decide whether or not to reload the data for the entire table, +// or for just one row, in case of the image download +@protocol DataConsumer + - (void)dataProvider:(id)dataProvider didChangeImageForRowAtIndex:(NSUInteger)index; + - (void)dataProviderDidChangeData:(id)dataProvider +@end + diff --git a/CodeChallenge/Protocols/Proposal/NetworkManagerProtocol.h b/CodeChallenge/Protocols/Proposal/NetworkManagerProtocol.h new file mode 100644 index 0000000..f0b5c6f --- /dev/null +++ b/CodeChallenge/Protocols/Proposal/NetworkManagerProtocol.h @@ -0,0 +1,34 @@ +// +// NetworkManagerProtocol.h +// CodeChallenge +// +// Created by alexey novikov on 31.03.2018. +// + +#import + +// this protocol describes the low-level network interaction interface that is never exposed to view controllers, +// but only to the business logic controller. +// object implementing it will know how to create NSURLSessionTasks, will be aware of the base URL, and will form the request parameters. + +typedef void(^DataSuccessBlock)(NSData *imageData) +typedef void(^SuccessBlock)(NSDictionary *payload); +typedef void(^FailureBlock)(NSError *error); + +@protocol NetworkManagerProtocol + - (void)fetchRepositoriesWithOffset:(NSUInteger)offset + limit:(NSUInteger)limit + successBlock:(SuccessBlock)success + failureBlock:(FailureBlock)failure; + + - (void)fetchCommitsForRepository:(NSString *)repository + limit:(NSUInteger)limit + successBlock:(SuccessBlock)success + failureBlock:(FailureBlock)failure; + + - (void)fetchAvatarForAuthor:(NSString *)author + successBlock:(DataSuccessBlock)success + failureBlock:(FailureBlock)failure; + + - (void)cancelAllRequests; +@end diff --git a/CodeChallenge/Protocols/Proposal/PersistencyManagerProtocol.h b/CodeChallenge/Protocols/Proposal/PersistencyManagerProtocol.h new file mode 100644 index 0000000..afb8e27 --- /dev/null +++ b/CodeChallenge/Protocols/Proposal/PersistencyManagerProtocol.h @@ -0,0 +1,19 @@ +// +// PersistencyManagerProtocol.h +// CodeChallenge +// +// Created by alexey novikov on 31.03.2018. +// + +#import + +// this interface describes the serializer. +// object implementing it will hide the details of how exactly the JSON is turned into model objects and persisted (=created and/or updated), +// be it CoreData or plain text files on the disk, thus providing abstraction on this layer. + +@protocol PersistencyManagerProtocol + - (void)createRepositoryWithPayload:(NSDictionary *)payload; + - (void)createCommitWithPayload:(NSDictionary *)payload; + - (void)createAvatarWithData:(NSData *)data; + - (void)resetStorage; +@end diff --git a/CodeChallenge/Protocols/Proposal/RepositoriesDataProvider.h b/CodeChallenge/Protocols/Proposal/RepositoriesDataProvider.h new file mode 100644 index 0000000..82119f9 --- /dev/null +++ b/CodeChallenge/Protocols/Proposal/RepositoriesDataProvider.h @@ -0,0 +1,13 @@ +// +// RepositoriesDataProvider.h +// CodeChallenge +// +// Created by alexey novikov on 31.03.2018. +// + +#import +#import "DataProviderProtocol.h" + +@interface RepositoriesDataProvider : NSObject + +@end diff --git a/CodeChallenge/Protocols/Proposal/RepositoriesDataProvider.m b/CodeChallenge/Protocols/Proposal/RepositoriesDataProvider.m new file mode 100644 index 0000000..5c7f1eb --- /dev/null +++ b/CodeChallenge/Protocols/Proposal/RepositoriesDataProvider.m @@ -0,0 +1,81 @@ +// +// RepositoriesDataProvider.m +// CodeChallenge +// +// Created by alexey novikov on 31.03.2018. +// + +@class RepositoriesCellModel; + +#import +#import + +#import "Repository.h" +#import "RepositoriesDataProvider.h" + +@interface RepositoriesDataProvider () + @property (strong, nonatomic) NSFetchedResultsController *fetchController; +@end + +@implementation RepositoriesDataProvider + +- (instancetype)init { + self = [super init]; + + if (self != nil) { + + } + + return self; +} + +#pragma mark - DataProviderProtocol +- (NSUInteger)numberOfRows { + return self.fetchController.fetchedObjects.count; +} + +- (id)cellModelAtRow:(NSUInteger)row { + if (row >= self.fetchController.fetchedObjects.count) { + return nil; + } + + Repository *r = [self.fetchController.fetchedObjects objectAtIndex:row]; + + RepositoriesCellModel *cellModel = [[RepositoriesCellModel alloc] initWithRepository:r]; + return cellModel; +} + +#pragma mark - NSFRC delegate methods +- (void)controllerDidChangeContent:(NSFetchedResultsController *)controller { + [self notifyDataConsumerAboutDataChange]; +} + +#pragma mark - private subroutines +- (void)notifyDataConsumerAboutDataChange { + if (self.dataConsumer != nil && [self.dataConsumer respondsToSelector:@selector(dataProviderDidChangeData:)]) { + [self.dataConsumer dataProviderDidChangeData:self]; + } +} + +- (void)notifyDataConsumerAboutImageDownload { + +} + +#pragma mark - init subroutines +- (void)initFetchedResultsController { + NSPredicate *predicate = [NSPredicate predicateWithFormat:@""]; + + self.fetchController = [Repository MR_fetchAllSortedBy:@"rank" + ascending:YES + withPredicate:predicate + groupBy:nil + delegate:self]; + NSError *error; + [self.fetchController performFetch:&error]; + + if (error != nil) { + DLog(@"Atata, error: %@", error.localizedDescription); + } +} + +@end