Skip to content

Commit

Permalink
Added NSProgress support
Browse files Browse the repository at this point in the history
  • Loading branch information
ipodishima committed Mar 17, 2016
1 parent 6420958 commit 4318a70
Show file tree
Hide file tree
Showing 17 changed files with 487 additions and 103 deletions.
40 changes: 37 additions & 3 deletions CHANGELOG
Original file line number Diff line number Diff line change
@@ -1,11 +1,45 @@
## 0.0.3 - Released Feb 29, 2015
## 0.0.4 - Released Mar 17, 2016
- Added `NSProgress` support
You can track progress using
```objc
[mapper.progress addObserver:self
forKeyPath:NSStringFromSelector(@selector(fractionCompleted))
options:NSKeyValueObservingOptionNew
context:NULL];
```

```objc
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSString *,id> *)change context:(void *)context {
if ([keyPath isEqualToString:NSStringFromSelector(@selector(fractionCompleted))] && [object isKindOfClass:[NSProgress class]]) {
NSLog(@"Mapping progress = %f", [change[@"new"] doubleValue]);
} else {
[super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
}
}
```

It also supports cancellation

```objc
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0), ^{
[mapper mapFromRepresentation:JSON mapping:employeeMapping completion:^(NSArray *mappedObjects, NSError *error) {
NSLog(@"Mapped objects %@ - Error %@", mappedObjects, error);
}];
});

[mapper.progress cancel];
```

- Added a basic sample on view controller

## 0.0.3 - Released Feb 29, 2016
- Added `WANSCodingStore` to the main header
- Added a way to register default mapping block for specific classes.
For example, you can now add a default mapping block to turn `strings` to `NSDate`

## 0.0.2 - Released Feb 26, 2015
## 0.0.2 - Released Feb 26, 2016
- Exposed the store on WAMapper
- Added a new store: `WANSCodingStore`

## 0.0.1 - Released Feb 23, 2015
## 0.0.1 - Released Feb 23, 2016
Initial release
7 changes: 5 additions & 2 deletions Files/WAMapper.h
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,15 @@
@class WAEntityMapping;
@protocol WAStoreProtocol;

typedef void (^WAMapperCompletionBlock)(NSArray *mappedObjects);
typedef void (^WAMapperProgressBlock)(NSProgress *progress);
typedef void (^WAMapperCompletionBlock)(NSArray *mappedObjects, NSError *error);

/**
This class will transform a dictionary representation to an object
It supports NSProgress with cancellation (but no pausing). Be aware that according to Apple Documentation about `NSProgressReporting`, "Objects that adopt this protocol should typically be "one-shot""
This means that you should allocate one mapper per mapping execution
*/
@interface WAMapper : NSObject
@interface WAMapper : NSObject <NSProgressReporting>

- (instancetype)init NS_UNAVAILABLE;
+ (instancetype)new NS_UNAVAILABLE;
Expand Down
57 changes: 45 additions & 12 deletions Files/WAMapper.m
Original file line number Diff line number Diff line change
Expand Up @@ -27,13 +27,18 @@ @interface WAMapper ()
@end

@implementation WAMapper
@synthesize progress = _progress;

- (instancetype)initWithStore:(id<WAStoreProtocol>)store {
WAMProtocolClassAssert(store, WAStoreProtocol);

self = [super init];
if (self) {
self.store = store;
self->_store = store;

self->_progress = [NSProgress progressWithTotalUnitCount:-1];
self->_progress.cancellable = YES;
self->_progress.pausable = NO;
}

return self;
Expand All @@ -46,14 +51,20 @@ + (instancetype)newMapperWithStore:(id<WAStoreProtocol>)store {
- (void)mapFromRepresentation:(id)json mapping:(WAEntityMapping *)mapping completion:(WAMapperCompletionBlock)completion {
WAMParameterAssert(completion);
WAMAssert(self.store, @"You need to setup a store");
NSArray *objects = [self _arrayFromRepresentation:json mapping:mapping];

self->_progress.totalUnitCount = 2 /* store begin and commit transaction */ + [objects count];

[self.store beginTransaction];

NSArray *mappedObjects = [self _mapFromRepresentation:json mapping:mapping];
self->_progress.completedUnitCount++;

NSError *error = nil;
NSArray *mappedObjects = [self _mapFromArray:objects mapping:mapping updateProgress:YES error:&error];

[self.store commitTransaction];
self->_progress.completedUnitCount++;

completion(mappedObjects);
completion(mappedObjects, error);
}

- (void)addDefaultMappingBlock:(WAMappingBlock)mappingBlock forDestinationClass:(Class)destinationClass {
Expand All @@ -69,8 +80,7 @@ - (void)addDefaultMappingBlock:(WAMappingBlock)mappingBlock forDestinationClass:

#pragma mark - Private methods

- (NSArray *)_mapFromRepresentation:(id)json mapping:(WAEntityMapping *)mapping {

- (NSArray *)_arrayFromRepresentation:(id)json mapping:(WAEntityMapping *)mapping {
NSArray *resolvedArray = nil;
if ([json isKindOfClass:[NSArray class]] && [[json firstObject] isKindOfClass:[NSDictionary class]]) {
resolvedArray = json;
Expand All @@ -87,13 +97,20 @@ - (NSArray *)_mapFromRepresentation:(id)json mapping:(WAEntityMapping *)mapping
} else {
resolvedArray = @[@{mapping.inverseIdentificationAttribute: json}];
}
} else {
}

return resolvedArray;
}

- (NSArray *)_mapFromArray:(NSArray *)objectsArray mapping:(WAEntityMapping *)mapping updateProgress:(BOOL)updateProgress error:(NSError **)error {

if (!objectsArray) {
return nil;
}

// Grab the attributes values
NSMutableArray *objectsIdentificationAttributes = [NSMutableArray array];
for (id representation in resolvedArray) {
for (id representation in objectsArray) {
id object = nil;
if ([representation isKindOfClass:[NSDictionary class]] && mapping.inverseAttributeMappings) {
object = representation[mapping.inverseIdentificationAttribute];
Expand Down Expand Up @@ -132,8 +149,18 @@ - (NSArray *)_mapFromRepresentation:(id)json mapping:(WAEntityMapping *)mapping
NSMutableArray *objectsMapped = [NSMutableArray new];

// Go through all objects in json
for (NSDictionary *dic in resolvedArray) {
for (NSDictionary *dic in objectsArray) {
// Get the object if existing
if (self->_progress.isCancelled) {
if (error) {
*error = [NSError errorWithDomain:NSCocoaErrorDomain
code:NSUserCancelledError
userInfo:nil];
}

break;
}

id inverseAttribute = mapping.inverseIdentificationAttribute;
id sourceIDValue = nil;
if (inverseAttribute) {
Expand All @@ -156,9 +183,12 @@ - (NSArray *)_mapFromRepresentation:(id)json mapping:(WAEntityMapping *)mapping

[objectsMapped addObject:objectToApplyMappingOn];
[objectsMapped addObjectsFromArray:relationShipObjects];

if (updateProgress) {
self->_progress.completedUnitCount++;
}
}


return [objectsMapped copy];
}

Expand Down Expand Up @@ -238,8 +268,11 @@ - (NSArray *)_applyMapping:(WAEntityMapping *)mapping onObject:(id)object withRe

id finalObjects = nil;
if (finalValue) {
NSArray *mappedObjects = [self _mapFromRepresentation:finalValue
mapping:relationShipMapping.mapping];
// We do not update progress on childs. We could thought, but it involves add child behaviour on NSProgress which is far easier on iOS 9+
NSArray *mappedObjects = [self _mapFromArray:[self _arrayFromRepresentation:value mapping:relationShipMapping.mapping]
mapping:relationShipMapping.mapping
updateProgress:NO
error:nil];

[relationShipObjects addObjectsFromArray:mappedObjects];

Expand Down
6 changes: 4 additions & 2 deletions Files/WAReverseMapper.h
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,10 @@ typedef BOOL (^WAReverseMapperShouldMapRelationshipBlock)(NSString *sourceRelati

/**
* This class performs a reverse mapper by turning objects back into dictionaries
* It supports NSProgress with cancellation (but no pausing). Be aware that according to Apple Documentation about `NSProgressReporting`, "Objects that adopt this protocol should typically be "one-shot""
* This means that you should allocate one reversemapper per execution
*/
@interface WAReverseMapper : NSObject
@interface WAReverseMapper : NSObject <NSProgressReporting>

/**
* Turns objects into a dictionary
Expand All @@ -27,7 +29,7 @@ typedef BOOL (^WAReverseMapperShouldMapRelationshipBlock)(NSString *sourceRelati
*
* @return an array of dictionary which are a representation of the objects
*/
- (NSArray <NSDictionary *>*)reverseMapObjects:(NSArray *)objects fromMapping:(WAEntityMapping *)mapping shouldMapRelationship:(WAReverseMapperShouldMapRelationshipBlock)shouldMapRelationshipBlock;
- (NSArray <NSDictionary *>*)reverseMapObjects:(NSArray *)objects fromMapping:(WAEntityMapping *)mapping shouldMapRelationship:(WAReverseMapperShouldMapRelationshipBlock)shouldMapRelationshipBlock error:(NSError **)error;

/**
* Add a reverse default mapping block for a class. For example, you could have an API returning dates all with the same format. You can register the transformation once here.
Expand Down
28 changes: 27 additions & 1 deletion Files/WAReverseMapper.m
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,20 @@ @interface WAReverseMapper ()
@end

@implementation WAReverseMapper
@synthesize progress = _progress;

- (instancetype)init {
self = [super init];
if (self) {
self->_progress = [NSProgress progressWithTotalUnitCount:-1];
self->_progress.cancellable = YES;
self->_progress.pausable = NO;
}

return self;
}

- (NSArray *)reverseMapObjects:(NSArray *)objects fromMapping:(WAEntityMapping *)mapping shouldMapRelationship:(WAReverseMapperShouldMapRelationshipBlock)shouldMapRelationshipBlock {
- (NSArray *)reverseMapObjects:(NSArray *)objects fromMapping:(WAEntityMapping *)mapping shouldMapRelationship:(WAReverseMapperShouldMapRelationshipBlock)shouldMapRelationshipBlock error:(NSError *__autoreleasing *)error {
WAMParameterAssert(objects);

NSArray *resolvedArray = nil;
Expand All @@ -40,18 +52,32 @@ - (NSArray *)reverseMapObjects:(NSArray *)objects fromMapping:(WAEntityMapping *
return nil;
}

self->_progress.totalUnitCount = [objects count];

NSMutableArray *allObjectsAsDictionaries = [NSMutableArray array];
// We use this dictionary to avoid infinite loops
NSMutableDictionary *alreadyMappedObjects = [NSMutableDictionary dictionary];

for (id obj in objects) {
if (self->_progress.isCancelled) {
if (error) {
*error = [NSError errorWithDomain:NSCocoaErrorDomain
code:NSUserCancelledError
userInfo:nil];
}

break;
}

NSDictionary *objectAsDictionary = [self _reverseMapObject:obj
withMapping:mapping
shouldMapRelationship:shouldMapRelationshipBlock
alreadyMappedObjects:&alreadyMappedObjects];
if (objectAsDictionary) {
[allObjectsAsDictionaries addObject:objectAsDictionary];
}

self->_progress.completedUnitCount++;
}

return [allObjectsAsDictionaries copy];
Expand Down
36 changes: 36 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -246,6 +246,42 @@ id(^toDateMappingBlock)(id ) = ^id(id value) {
The same thing happens to the reverse mapper. Note that if you provide a custom mapping on an `NSDate` object for a specific property (like a date with only the year), you can add the property to the entity mapping which will override the default behavior for this specific property.
# Progress and cancellation
Both `WAMapper` and `WAReverseMapper` support `NSProgress` by implementing `NSProgressReporting` protocol. Note that Apple explicitely says `Objects that adopt this protocol should typically be "one-shot"` which means you should use one `WAMapper` per map operation.
## Progress
You can track the progress using this little piece of code. Note that the progress counts the main top objects mapped (if your array contains one object with a thousand objects as relationship, the progress will not reflect the thousand subobjects mapped). This is per `NSProgress` class which supports child progress only from iOS 9.
```objc
[mapper.progress addObserver:self
forKeyPath:NSStringFromSelector(@selector(fractionCompleted))
options:NSKeyValueObservingOptionNew
context:NULL];
```

```objc
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSString *,id> *)change context:(void *)context {
if ([keyPath isEqualToString:NSStringFromSelector(@selector(fractionCompleted))] && [object isKindOfClass:[NSProgress class]]) {
NSLog(@"Mapping progress = %f", [change[@"new"] doubleValue]);
} else {
[super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
}
}
```

## Cancellation
You can cancel the mapping or the reverse mapping using this piece of code. Note that for cancellation to happen, you have to call the mapping from an other thread!

```objc
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0), ^{
[mapper mapFromRepresentation:JSON mapping:employeeMapping completion:^(NSArray *mappedObjects, NSError *error) {
NSLog(@"Mapped objects %@ - Error %@", mappedObjects, error);
}];
});

[mapper.progress cancel];
```
# Side notes
## TODOs
Expand Down
4 changes: 2 additions & 2 deletions WAMapping.podspec
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
Pod::Spec.new do |s|
s.name = "WAMapping"
s.version = "0.0.3"
s.version = "0.0.4"
s.summary = "WAMapping is a library which turns dictionary to object and vice versa. Designed for speed!"
s.homepage = "https://github.com/Wasappli/WAMapping"
s.license = { :type => "MIT", :file => "LICENSE" }
s.author = { "Marian Paul" => "marian@wasapp.li" }
s.platform = :ios, "7.0"
s.source = { :git => "https://github.com/Wasappli/WAMapping.git", :tag => "0.0.3" }
s.source = { :git => "https://github.com/Wasappli/WAMapping.git", :tag => "0.0.4" }
s.source_files = "Files/*.{h,m}"
s.requires_arc = true
s.frameworks = "CoreData"
Expand Down
Loading

0 comments on commit 4318a70

Please sign in to comment.