
.
Kiwi
BDD- RSpec - , . «--».
OHTTPStubs
, web- . "", - .
.
There are only two hard things in Computer Science: cache invalidation and naming things.
-- Phil Karlton
« » :
..
, .
- inMemory CoreData , NSCache . , , — . NSURLConnection .
NSURLCache
NSURLCache NSURLRequest . , .
:
NSURLCache *urlCache = [[NSURLCache alloc] initWithMemoryCapacity:kInMemoryCacheSize diskCapacity:kOnDiskCacheSize diskPath:path]; [NSURLCache setSharedURLCache:urlCache];
, .
NSURLConnectionDelegate , "" :
- (NSCachedURLResponse *)connection:(NSURLConnection *)connection willCacheResponse:(NSCachedURLResponse *)cachedResponse;
?
NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse*)[cachedResponse response]; NSDictionary *headers = [httpResponse allHeaderFields]; NSString *cacheControl = [headers valueForKey:@"Cache-Control"]; NSString *expires = [headers valueForKey:@"Expires"]; if((cacheControl == nil) && (expires == nil)) { NSMutableDictionary *modifiedHeaders = [httpResponse.allHeaderFields mutableCopy]; NSString *expireDate = [NSDate RFC822StringFromDate:[[NSDate date] dateByAddingTimeInterval:cacheTime]]; NSDictionary *values = @{@"Expires": expireDate, @"Cache-Control": @"public"}; [modifiedHeaders addEntriesFromDictionary:values]; NSHTTPURLResponse *response = [[NSHTTPURLResponse alloc] initWithURL:httpResponse.URL statusCode:httpResponse.statusCode HTTPVersion:@"HTTP/1.1" headerFields:modifiedHeaders]; NSCachedURLResponse *modifiedCachedResponse = [[NSCachedURLResponse alloc] initWithResponse:response data:cachedResponse.data userInfo:cachedResponse.userInfo storagePolicy:NSURLCacheStorageAllowed]; return modifiedCachedResponse; } return cachedResponse;
HTTP- .
, ( , , ). , ( cacheTime ) .
— ( ). , , .
:
(, , ..); - NSURLCache , .
:
30 ; ; : 200- , NSURLConnection .
""
API (, ), fields :
. . . , .
, Player , JSON-, . , fields , NSDictionary . , - . , .
JSON -> NSObject Mantle , API ( RACSignal API ):
- (RACSignal *)searchPlayers:(NSString *)query limit:(NSUInteger)limit { NSMutableDictionary *parameters = [NSMutableDictionary dictionary]; parameters[@"search"] = query; if (limit) parameters[@"limit"] = @(limit); return [self getPath:@"account/list" parameters:parameters resultClass:WOTSearchPlayer.class]; }
, resultClass , ? :
- (RACSignal *)searchPlayers:(NSString *)query limit:(NSUInteger)limit resultClass:(Class)resultClass
, API, .
API , : , , "" . ( ). , ( AFJSONRequestOperation ):
op1 = [AFJSONRequestOperation JSONRequestOperationWithRequest:nil success:^(id JSON) { op2 = [AFJSONRequestOperation JSONRequestOperationWithRequest:nil success:^(id JSON) { op3 = [AFJSONRequestOperation JSONRequestOperationWithRequest:nil success:^(id JSON) { // Combine all the responses and return } failure:^(NSError *error) { // Handle error }]; } failure:^(NSError *error) { // Handle error }]; } failure:^(NSError *error) { // Handle error }];
, , . ReactiveCocoa , .
API :
- (RACSignal *)searchPlayers:(NSString *)query limit:(NSUInteger)limit; - (RACSignal *)fetchPlayers:(NSArray *)playerIDs;
RACSignal — , , . — , . / :
RACSignal *fetchPlayersAndRatings = [[[[API searchPlayers:@"" limit:0] flattenMap:^RACStream *(id players) { // skipping data processing return [API fetchPlayers:@[]]; }] flattenMap:^RACStream *(id fullPlayers) { // Skipping data processing return [API statsSliceForPlayer:nil hoursAgo:@[] fields:nil]; }] flattenMap:^RACStream *(id value) { // Compose all responses here id composedResponse; return [RACSignal return:composedResponse]; }]; [fetchPlayersAndRatings subscribeNext:^(id x) { // Fully composed response } error:^(NSError *error) { // All erros go to one place }];
ReactiveCocoa — , . , Promises Futures.
:
Core ( , ); WOT ( World of Tanks–); WOWP ( World of Warplanes–)
, , — . , ( ) CocoaPods , . subspecs , :
# Subspecs s.subspec 'Core' do |cs| cs.source_files = 'WGNAPIClient/Core/**/*.{h,m}' end s.subspec 'WOT' do |cs| cs.dependency 'WGNAPIClient/Core' cs.source_files = 'WGNAPIClient/WOT/**/*.{h,m}' end s.subspec 'WOWP' do |cs| cs.dependency 'WGNAPIClient/Core' cs.source_files = 'WGNAPIClient/WOWP/**/*.{h,m}' end
"" "" , :
pod 'WGNAPIClient/WOT' pod 'WGNAPIClient/WOWP'
. .
(98%). :
; .
:
context(@"API works with players data", ^{ it (@"should search players", ^{ stubResponse(@"/wot/account/list", @"players-search.json"); RACSignal *searchRequest = [client searchPlayers:@"garnett" limit:0]; NSArray *response = [searchRequest asynchronousFirstOrDefault:nil success:NULL error:&error]; [[error should] beNil]; [[response should] beKindOfClass:NSArray.class]; [[response.lastObject should] beKindOfClass:WOTSearchPlayer.class]; WOTSearchPlayer *player = response[0]; [[player.ID should] equal:@"1785514"]; }); });
, / , RACSignal , :
(id)asynchronousFirstOrDefault:(id)defaultValue success:(BOOL *)success error:(NSError **)error;
describe(@"WOTRatingType", ^{ it(@"should parse json to model object", ^{ NSDictionary *json = @{ @"threshold": @5, @"type": @"1", @"rank_fields": @[ @"xp_amount_rank", @"xp_max_rank", @"battles_count_rank", @"spotted_count_rank", @"damage_dealt_rank", ] }; NSError *error; WOTRatingType *ratingType = [MTLJSONAdapter modelOfClass:WOTRatingType.class fromJSONDictionary:json error:&error]; [[error should] beNil]; [[ratingType should] beKindOfClass:WOTRatingType.class]; [[json[@"threshold"] should] equal:theValue(ratingType.threshold)]; * [[json[@"type"] should] equal:ratingType.type]; [[ratingType.rankFields shouldNot] beNil]; [[ratingType.rankFields should] beKindOfClass:NSArray.class]; [[ratingType.rankFields should] haveCountOf:5]; [[@"maximumXP" should] equal:ratingType.rankFields[1]]; }); });
* — - - Kiwi, , nil .
API :
; .
, , : query http-. 
, ReactiveCocoa , ( ). ( ) 2k LOC ( ~1k - , ~700 - ), : — ..
Kiwi
BDD- RSpec - , . «--».
OHTTPStubs
, web- . "", - .
.
There are only two hard things in Computer Science: cache invalidation and naming things.
-- Phil Karlton
« » :
..
, .
- inMemory CoreData , NSCache . , , — . NSURLConnection .
NSURLCache
NSURLCache NSURLRequest . , .
:
NSURLCache *urlCache = [[NSURLCache alloc] initWithMemoryCapacity:kInMemoryCacheSize diskCapacity:kOnDiskCacheSize diskPath:path]; [NSURLCache setSharedURLCache:urlCache];
, .
NSURLConnectionDelegate , "" :
- (NSCachedURLResponse *)connection:(NSURLConnection *)connection willCacheResponse:(NSCachedURLResponse *)cachedResponse;
?
NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse*)[cachedResponse response]; NSDictionary *headers = [httpResponse allHeaderFields]; NSString *cacheControl = [headers valueForKey:@"Cache-Control"]; NSString *expires = [headers valueForKey:@"Expires"]; if((cacheControl == nil) && (expires == nil)) { NSMutableDictionary *modifiedHeaders = [httpResponse.allHeaderFields mutableCopy]; NSString *expireDate = [NSDate RFC822StringFromDate:[[NSDate date] dateByAddingTimeInterval:cacheTime]]; NSDictionary *values = @{@"Expires": expireDate, @"Cache-Control": @"public"}; [modifiedHeaders addEntriesFromDictionary:values]; NSHTTPURLResponse *response = [[NSHTTPURLResponse alloc] initWithURL:httpResponse.URL statusCode:httpResponse.statusCode HTTPVersion:@"HTTP/1.1" headerFields:modifiedHeaders]; NSCachedURLResponse *modifiedCachedResponse = [[NSCachedURLResponse alloc] initWithResponse:response data:cachedResponse.data userInfo:cachedResponse.userInfo storagePolicy:NSURLCacheStorageAllowed]; return modifiedCachedResponse; } return cachedResponse;
HTTP- .
, ( , , ). , ( cacheTime ) .
— ( ). , , .
:
(, , ..); - NSURLCache , .
:
30 ; ; : 200- , NSURLConnection .
""
API (, ), fields :
. . . , .
, Player , JSON-, . , fields , NSDictionary . , - . , .
JSON -> NSObject Mantle , API ( RACSignal API ):
- (RACSignal *)searchPlayers:(NSString *)query limit:(NSUInteger)limit { NSMutableDictionary *parameters = [NSMutableDictionary dictionary]; parameters[@"search"] = query; if (limit) parameters[@"limit"] = @(limit); return [self getPath:@"account/list" parameters:parameters resultClass:WOTSearchPlayer.class]; }
, resultClass , ? :
- (RACSignal *)searchPlayers:(NSString *)query limit:(NSUInteger)limit resultClass:(Class)resultClass
, API, .
API , : , , "" . ( ). , ( AFJSONRequestOperation ):
op1 = [AFJSONRequestOperation JSONRequestOperationWithRequest:nil success:^(id JSON) { op2 = [AFJSONRequestOperation JSONRequestOperationWithRequest:nil success:^(id JSON) { op3 = [AFJSONRequestOperation JSONRequestOperationWithRequest:nil success:^(id JSON) { // Combine all the responses and return } failure:^(NSError *error) { // Handle error }]; } failure:^(NSError *error) { // Handle error }]; } failure:^(NSError *error) { // Handle error }];
, , . ReactiveCocoa , .
API :
- (RACSignal *)searchPlayers:(NSString *)query limit:(NSUInteger)limit; - (RACSignal *)fetchPlayers:(NSArray *)playerIDs;
RACSignal — , , . — , . / :
RACSignal *fetchPlayersAndRatings = [[[[API searchPlayers:@"" limit:0] flattenMap:^RACStream *(id players) { // skipping data processing return [API fetchPlayers:@[]]; }] flattenMap:^RACStream *(id fullPlayers) { // Skipping data processing return [API statsSliceForPlayer:nil hoursAgo:@[] fields:nil]; }] flattenMap:^RACStream *(id value) { // Compose all responses here id composedResponse; return [RACSignal return:composedResponse]; }]; [fetchPlayersAndRatings subscribeNext:^(id x) { // Fully composed response } error:^(NSError *error) { // All erros go to one place }];
ReactiveCocoa — , . , Promises Futures.
:
Core ( , ); WOT ( World of Tanks–); WOWP ( World of Warplanes–)
, , — . , ( ) CocoaPods , . subspecs , :
# Subspecs s.subspec 'Core' do |cs| cs.source_files = 'WGNAPIClient/Core/**/*.{h,m}' end s.subspec 'WOT' do |cs| cs.dependency 'WGNAPIClient/Core' cs.source_files = 'WGNAPIClient/WOT/**/*.{h,m}' end s.subspec 'WOWP' do |cs| cs.dependency 'WGNAPIClient/Core' cs.source_files = 'WGNAPIClient/WOWP/**/*.{h,m}' end
"" "" , :
pod 'WGNAPIClient/WOT' pod 'WGNAPIClient/WOWP'
. .
(98%). :
; .
:
context(@"API works with players data", ^{ it (@"should search players", ^{ stubResponse(@"/wot/account/list", @"players-search.json"); RACSignal *searchRequest = [client searchPlayers:@"garnett" limit:0]; NSArray *response = [searchRequest asynchronousFirstOrDefault:nil success:NULL error:&error]; [[error should] beNil]; [[response should] beKindOfClass:NSArray.class]; [[response.lastObject should] beKindOfClass:WOTSearchPlayer.class]; WOTSearchPlayer *player = response[0]; [[player.ID should] equal:@"1785514"]; }); });
, / , RACSignal , :
(id)asynchronousFirstOrDefault:(id)defaultValue success:(BOOL *)success error:(NSError **)error;
describe(@"WOTRatingType", ^{ it(@"should parse json to model object", ^{ NSDictionary *json = @{ @"threshold": @5, @"type": @"1", @"rank_fields": @[ @"xp_amount_rank", @"xp_max_rank", @"battles_count_rank", @"spotted_count_rank", @"damage_dealt_rank", ] }; NSError *error; WOTRatingType *ratingType = [MTLJSONAdapter modelOfClass:WOTRatingType.class fromJSONDictionary:json error:&error]; [[error should] beNil]; [[ratingType should] beKindOfClass:WOTRatingType.class]; [[json[@"threshold"] should] equal:theValue(ratingType.threshold)]; * [[json[@"type"] should] equal:ratingType.type]; [[ratingType.rankFields shouldNot] beNil]; [[ratingType.rankFields should] beKindOfClass:NSArray.class]; [[ratingType.rankFields should] haveCountOf:5]; [[@"maximumXP" should] equal:ratingType.rankFields[1]]; }); });
* — - - Kiwi, , nil .
API :
; .
, , : query http-. 
, ReactiveCocoa , ( ). ( ) 2k LOC ( ~1k - , ~700 - ), : — ..
Kiwi
BDD- RSpec - , . «--».
OHTTPStubs
, web- . "", - .
.
There are only two hard things in Computer Science: cache invalidation and naming things.
-- Phil Karlton
« » :
..
, .
- inMemory CoreData , NSCache . , , — . NSURLConnection .
NSURLCache
NSURLCache NSURLRequest . , .
:
NSURLCache *urlCache = [[NSURLCache alloc] initWithMemoryCapacity:kInMemoryCacheSize diskCapacity:kOnDiskCacheSize diskPath:path]; [NSURLCache setSharedURLCache:urlCache];
, .
NSURLConnectionDelegate , "" :
- (NSCachedURLResponse *)connection:(NSURLConnection *)connection willCacheResponse:(NSCachedURLResponse *)cachedResponse;
?
NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse*)[cachedResponse response]; NSDictionary *headers = [httpResponse allHeaderFields]; NSString *cacheControl = [headers valueForKey:@"Cache-Control"]; NSString *expires = [headers valueForKey:@"Expires"]; if((cacheControl == nil) && (expires == nil)) { NSMutableDictionary *modifiedHeaders = [httpResponse.allHeaderFields mutableCopy]; NSString *expireDate = [NSDate RFC822StringFromDate:[[NSDate date] dateByAddingTimeInterval:cacheTime]]; NSDictionary *values = @{@"Expires": expireDate, @"Cache-Control": @"public"}; [modifiedHeaders addEntriesFromDictionary:values]; NSHTTPURLResponse *response = [[NSHTTPURLResponse alloc] initWithURL:httpResponse.URL statusCode:httpResponse.statusCode HTTPVersion:@"HTTP/1.1" headerFields:modifiedHeaders]; NSCachedURLResponse *modifiedCachedResponse = [[NSCachedURLResponse alloc] initWithResponse:response data:cachedResponse.data userInfo:cachedResponse.userInfo storagePolicy:NSURLCacheStorageAllowed]; return modifiedCachedResponse; } return cachedResponse;
HTTP- .
, ( , , ). , ( cacheTime ) .
— ( ). , , .
:
(, , ..); - NSURLCache , .
:
30 ; ; : 200- , NSURLConnection .
""
API (, ), fields :
. . . , .
, Player , JSON-, . , fields , NSDictionary . , - . , .
JSON -> NSObject Mantle , API ( RACSignal API ):
- (RACSignal *)searchPlayers:(NSString *)query limit:(NSUInteger)limit { NSMutableDictionary *parameters = [NSMutableDictionary dictionary]; parameters[@"search"] = query; if (limit) parameters[@"limit"] = @(limit); return [self getPath:@"account/list" parameters:parameters resultClass:WOTSearchPlayer.class]; }
, resultClass , ? :
- (RACSignal *)searchPlayers:(NSString *)query limit:(NSUInteger)limit resultClass:(Class)resultClass
, API, .
API , : , , "" . ( ). , ( AFJSONRequestOperation ):
op1 = [AFJSONRequestOperation JSONRequestOperationWithRequest:nil success:^(id JSON) { op2 = [AFJSONRequestOperation JSONRequestOperationWithRequest:nil success:^(id JSON) { op3 = [AFJSONRequestOperation JSONRequestOperationWithRequest:nil success:^(id JSON) { // Combine all the responses and return } failure:^(NSError *error) { // Handle error }]; } failure:^(NSError *error) { // Handle error }]; } failure:^(NSError *error) { // Handle error }];
, , . ReactiveCocoa , .
API :
- (RACSignal *)searchPlayers:(NSString *)query limit:(NSUInteger)limit; - (RACSignal *)fetchPlayers:(NSArray *)playerIDs;
RACSignal — , , . — , . / :
RACSignal *fetchPlayersAndRatings = [[[[API searchPlayers:@"" limit:0] flattenMap:^RACStream *(id players) { // skipping data processing return [API fetchPlayers:@[]]; }] flattenMap:^RACStream *(id fullPlayers) { // Skipping data processing return [API statsSliceForPlayer:nil hoursAgo:@[] fields:nil]; }] flattenMap:^RACStream *(id value) { // Compose all responses here id composedResponse; return [RACSignal return:composedResponse]; }]; [fetchPlayersAndRatings subscribeNext:^(id x) { // Fully composed response } error:^(NSError *error) { // All erros go to one place }];
ReactiveCocoa — , . , Promises Futures.
:
Core ( , ); WOT ( World of Tanks–); WOWP ( World of Warplanes–)
, , — . , ( ) CocoaPods , . subspecs , :
# Subspecs s.subspec 'Core' do |cs| cs.source_files = 'WGNAPIClient/Core/**/*.{h,m}' end s.subspec 'WOT' do |cs| cs.dependency 'WGNAPIClient/Core' cs.source_files = 'WGNAPIClient/WOT/**/*.{h,m}' end s.subspec 'WOWP' do |cs| cs.dependency 'WGNAPIClient/Core' cs.source_files = 'WGNAPIClient/WOWP/**/*.{h,m}' end
"" "" , :
pod 'WGNAPIClient/WOT' pod 'WGNAPIClient/WOWP'
. .
(98%). :
; .
:
context(@"API works with players data", ^{ it (@"should search players", ^{ stubResponse(@"/wot/account/list", @"players-search.json"); RACSignal *searchRequest = [client searchPlayers:@"garnett" limit:0]; NSArray *response = [searchRequest asynchronousFirstOrDefault:nil success:NULL error:&error]; [[error should] beNil]; [[response should] beKindOfClass:NSArray.class]; [[response.lastObject should] beKindOfClass:WOTSearchPlayer.class]; WOTSearchPlayer *player = response[0]; [[player.ID should] equal:@"1785514"]; }); });
, / , RACSignal , :
(id)asynchronousFirstOrDefault:(id)defaultValue success:(BOOL *)success error:(NSError **)error;
describe(@"WOTRatingType", ^{ it(@"should parse json to model object", ^{ NSDictionary *json = @{ @"threshold": @5, @"type": @"1", @"rank_fields": @[ @"xp_amount_rank", @"xp_max_rank", @"battles_count_rank", @"spotted_count_rank", @"damage_dealt_rank", ] }; NSError *error; WOTRatingType *ratingType = [MTLJSONAdapter modelOfClass:WOTRatingType.class fromJSONDictionary:json error:&error]; [[error should] beNil]; [[ratingType should] beKindOfClass:WOTRatingType.class]; [[json[@"threshold"] should] equal:theValue(ratingType.threshold)]; * [[json[@"type"] should] equal:ratingType.type]; [[ratingType.rankFields shouldNot] beNil]; [[ratingType.rankFields should] beKindOfClass:NSArray.class]; [[ratingType.rankFields should] haveCountOf:5]; [[@"maximumXP" should] equal:ratingType.rankFields[1]]; }); });
* — - - Kiwi, , nil .
API :
; .
, , : query http-. 
, ReactiveCocoa , ( ). ( ) 2k LOC ( ~1k - , ~700 - ), : — ..
Kiwi
BDD- RSpec - , . «--».
OHTTPStubs
, web- . "", - .
.
There are only two hard things in Computer Science: cache invalidation and naming things.
-- Phil Karlton
« » :
..
, .
- inMemory CoreData , NSCache . , , — . NSURLConnection .
NSURLCache
NSURLCache NSURLRequest . , .
:
NSURLCache *urlCache = [[NSURLCache alloc] initWithMemoryCapacity:kInMemoryCacheSize diskCapacity:kOnDiskCacheSize diskPath:path]; [NSURLCache setSharedURLCache:urlCache];
, .
NSURLConnectionDelegate , "" :
- (NSCachedURLResponse *)connection:(NSURLConnection *)connection willCacheResponse:(NSCachedURLResponse *)cachedResponse;
?
NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse*)[cachedResponse response]; NSDictionary *headers = [httpResponse allHeaderFields]; NSString *cacheControl = [headers valueForKey:@"Cache-Control"]; NSString *expires = [headers valueForKey:@"Expires"]; if((cacheControl == nil) && (expires == nil)) { NSMutableDictionary *modifiedHeaders = [httpResponse.allHeaderFields mutableCopy]; NSString *expireDate = [NSDate RFC822StringFromDate:[[NSDate date] dateByAddingTimeInterval:cacheTime]]; NSDictionary *values = @{@"Expires": expireDate, @"Cache-Control": @"public"}; [modifiedHeaders addEntriesFromDictionary:values]; NSHTTPURLResponse *response = [[NSHTTPURLResponse alloc] initWithURL:httpResponse.URL statusCode:httpResponse.statusCode HTTPVersion:@"HTTP/1.1" headerFields:modifiedHeaders]; NSCachedURLResponse *modifiedCachedResponse = [[NSCachedURLResponse alloc] initWithResponse:response data:cachedResponse.data userInfo:cachedResponse.userInfo storagePolicy:NSURLCacheStorageAllowed]; return modifiedCachedResponse; } return cachedResponse;
HTTP- .
, ( , , ). , ( cacheTime ) .
— ( ). , , .
:
(, , ..); - NSURLCache , .
:
30 ; ; : 200- , NSURLConnection .
""
API (, ), fields :
. . . , .
, Player , JSON-, . , fields , NSDictionary . , - . , .
JSON -> NSObject Mantle , API ( RACSignal API ):
- (RACSignal *)searchPlayers:(NSString *)query limit:(NSUInteger)limit { NSMutableDictionary *parameters = [NSMutableDictionary dictionary]; parameters[@"search"] = query; if (limit) parameters[@"limit"] = @(limit); return [self getPath:@"account/list" parameters:parameters resultClass:WOTSearchPlayer.class]; }
, resultClass , ? :
- (RACSignal *)searchPlayers:(NSString *)query limit:(NSUInteger)limit resultClass:(Class)resultClass
, API, .
API , : , , "" . ( ). , ( AFJSONRequestOperation ):
op1 = [AFJSONRequestOperation JSONRequestOperationWithRequest:nil success:^(id JSON) { op2 = [AFJSONRequestOperation JSONRequestOperationWithRequest:nil success:^(id JSON) { op3 = [AFJSONRequestOperation JSONRequestOperationWithRequest:nil success:^(id JSON) { // Combine all the responses and return } failure:^(NSError *error) { // Handle error }]; } failure:^(NSError *error) { // Handle error }]; } failure:^(NSError *error) { // Handle error }];
, , . ReactiveCocoa , .
API :
- (RACSignal *)searchPlayers:(NSString *)query limit:(NSUInteger)limit; - (RACSignal *)fetchPlayers:(NSArray *)playerIDs;
RACSignal — , , . — , . / :
RACSignal *fetchPlayersAndRatings = [[[[API searchPlayers:@"" limit:0] flattenMap:^RACStream *(id players) { // skipping data processing return [API fetchPlayers:@[]]; }] flattenMap:^RACStream *(id fullPlayers) { // Skipping data processing return [API statsSliceForPlayer:nil hoursAgo:@[] fields:nil]; }] flattenMap:^RACStream *(id value) { // Compose all responses here id composedResponse; return [RACSignal return:composedResponse]; }]; [fetchPlayersAndRatings subscribeNext:^(id x) { // Fully composed response } error:^(NSError *error) { // All erros go to one place }];
ReactiveCocoa — , . , Promises Futures.
:
Core ( , ); WOT ( World of Tanks–); WOWP ( World of Warplanes–)
, , — . , ( ) CocoaPods , . subspecs , :
# Subspecs s.subspec 'Core' do |cs| cs.source_files = 'WGNAPIClient/Core/**/*.{h,m}' end s.subspec 'WOT' do |cs| cs.dependency 'WGNAPIClient/Core' cs.source_files = 'WGNAPIClient/WOT/**/*.{h,m}' end s.subspec 'WOWP' do |cs| cs.dependency 'WGNAPIClient/Core' cs.source_files = 'WGNAPIClient/WOWP/**/*.{h,m}' end
"" "" , :
pod 'WGNAPIClient/WOT' pod 'WGNAPIClient/WOWP'
. .
(98%). :
; .
:
context(@"API works with players data", ^{ it (@"should search players", ^{ stubResponse(@"/wot/account/list", @"players-search.json"); RACSignal *searchRequest = [client searchPlayers:@"garnett" limit:0]; NSArray *response = [searchRequest asynchronousFirstOrDefault:nil success:NULL error:&error]; [[error should] beNil]; [[response should] beKindOfClass:NSArray.class]; [[response.lastObject should] beKindOfClass:WOTSearchPlayer.class]; WOTSearchPlayer *player = response[0]; [[player.ID should] equal:@"1785514"]; }); });
, / , RACSignal , :
(id)asynchronousFirstOrDefault:(id)defaultValue success:(BOOL *)success error:(NSError **)error;
describe(@"WOTRatingType", ^{ it(@"should parse json to model object", ^{ NSDictionary *json = @{ @"threshold": @5, @"type": @"1", @"rank_fields": @[ @"xp_amount_rank", @"xp_max_rank", @"battles_count_rank", @"spotted_count_rank", @"damage_dealt_rank", ] }; NSError *error; WOTRatingType *ratingType = [MTLJSONAdapter modelOfClass:WOTRatingType.class fromJSONDictionary:json error:&error]; [[error should] beNil]; [[ratingType should] beKindOfClass:WOTRatingType.class]; [[json[@"threshold"] should] equal:theValue(ratingType.threshold)]; * [[json[@"type"] should] equal:ratingType.type]; [[ratingType.rankFields shouldNot] beNil]; [[ratingType.rankFields should] beKindOfClass:NSArray.class]; [[ratingType.rankFields should] haveCountOf:5]; [[@"maximumXP" should] equal:ratingType.rankFields[1]]; }); });
* — - - Kiwi, , nil .
API :
; .
, , : query http-. 
, ReactiveCocoa , ( ). ( ) 2k LOC ( ~1k - , ~700 - ), : — ..
Kiwi
BDD- RSpec - , . «--».
OHTTPStubs
, web- . "", - .
.
There are only two hard things in Computer Science: cache invalidation and naming things.
-- Phil Karlton
« » :
..
, .
- inMemory CoreData , NSCache . , , — . NSURLConnection .
NSURLCache
NSURLCache NSURLRequest . , .
:
NSURLCache *urlCache = [[NSURLCache alloc] initWithMemoryCapacity:kInMemoryCacheSize diskCapacity:kOnDiskCacheSize diskPath:path]; [NSURLCache setSharedURLCache:urlCache];
, .
NSURLConnectionDelegate , "" :
- (NSCachedURLResponse *)connection:(NSURLConnection *)connection willCacheResponse:(NSCachedURLResponse *)cachedResponse;
?
NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse*)[cachedResponse response]; NSDictionary *headers = [httpResponse allHeaderFields]; NSString *cacheControl = [headers valueForKey:@"Cache-Control"]; NSString *expires = [headers valueForKey:@"Expires"]; if((cacheControl == nil) && (expires == nil)) { NSMutableDictionary *modifiedHeaders = [httpResponse.allHeaderFields mutableCopy]; NSString *expireDate = [NSDate RFC822StringFromDate:[[NSDate date] dateByAddingTimeInterval:cacheTime]]; NSDictionary *values = @{@"Expires": expireDate, @"Cache-Control": @"public"}; [modifiedHeaders addEntriesFromDictionary:values]; NSHTTPURLResponse *response = [[NSHTTPURLResponse alloc] initWithURL:httpResponse.URL statusCode:httpResponse.statusCode HTTPVersion:@"HTTP/1.1" headerFields:modifiedHeaders]; NSCachedURLResponse *modifiedCachedResponse = [[NSCachedURLResponse alloc] initWithResponse:response data:cachedResponse.data userInfo:cachedResponse.userInfo storagePolicy:NSURLCacheStorageAllowed]; return modifiedCachedResponse; } return cachedResponse;
HTTP- .
, ( , , ). , ( cacheTime ) .
— ( ). , , .
:
(, , ..); - NSURLCache , .
:
30 ; ; : 200- , NSURLConnection .
""
API (, ), fields :
. . . , .
, Player , JSON-, . , fields , NSDictionary . , - . , .
JSON -> NSObject Mantle , API ( RACSignal API ):
- (RACSignal *)searchPlayers:(NSString *)query limit:(NSUInteger)limit { NSMutableDictionary *parameters = [NSMutableDictionary dictionary]; parameters[@"search"] = query; if (limit) parameters[@"limit"] = @(limit); return [self getPath:@"account/list" parameters:parameters resultClass:WOTSearchPlayer.class]; }
, resultClass , ? :
- (RACSignal *)searchPlayers:(NSString *)query limit:(NSUInteger)limit resultClass:(Class)resultClass
, API, .
API , : , , "" . ( ). , ( AFJSONRequestOperation ):
op1 = [AFJSONRequestOperation JSONRequestOperationWithRequest:nil success:^(id JSON) { op2 = [AFJSONRequestOperation JSONRequestOperationWithRequest:nil success:^(id JSON) { op3 = [AFJSONRequestOperation JSONRequestOperationWithRequest:nil success:^(id JSON) { // Combine all the responses and return } failure:^(NSError *error) { // Handle error }]; } failure:^(NSError *error) { // Handle error }]; } failure:^(NSError *error) { // Handle error }];
, , . ReactiveCocoa , .
API :
- (RACSignal *)searchPlayers:(NSString *)query limit:(NSUInteger)limit; - (RACSignal *)fetchPlayers:(NSArray *)playerIDs;
RACSignal — , , . — , . / :
RACSignal *fetchPlayersAndRatings = [[[[API searchPlayers:@"" limit:0] flattenMap:^RACStream *(id players) { // skipping data processing return [API fetchPlayers:@[]]; }] flattenMap:^RACStream *(id fullPlayers) { // Skipping data processing return [API statsSliceForPlayer:nil hoursAgo:@[] fields:nil]; }] flattenMap:^RACStream *(id value) { // Compose all responses here id composedResponse; return [RACSignal return:composedResponse]; }]; [fetchPlayersAndRatings subscribeNext:^(id x) { // Fully composed response } error:^(NSError *error) { // All erros go to one place }];
ReactiveCocoa — , . , Promises Futures.
:
Core ( , ); WOT ( World of Tanks–); WOWP ( World of Warplanes–)
, , — . , ( ) CocoaPods , . subspecs , :
# Subspecs s.subspec 'Core' do |cs| cs.source_files = 'WGNAPIClient/Core/**/*.{h,m}' end s.subspec 'WOT' do |cs| cs.dependency 'WGNAPIClient/Core' cs.source_files = 'WGNAPIClient/WOT/**/*.{h,m}' end s.subspec 'WOWP' do |cs| cs.dependency 'WGNAPIClient/Core' cs.source_files = 'WGNAPIClient/WOWP/**/*.{h,m}' end
"" "" , :
pod 'WGNAPIClient/WOT' pod 'WGNAPIClient/WOWP'
. .
(98%). :
; .
:
context(@"API works with players data", ^{ it (@"should search players", ^{ stubResponse(@"/wot/account/list", @"players-search.json"); RACSignal *searchRequest = [client searchPlayers:@"garnett" limit:0]; NSArray *response = [searchRequest asynchronousFirstOrDefault:nil success:NULL error:&error]; [[error should] beNil]; [[response should] beKindOfClass:NSArray.class]; [[response.lastObject should] beKindOfClass:WOTSearchPlayer.class]; WOTSearchPlayer *player = response[0]; [[player.ID should] equal:@"1785514"]; }); });
, / , RACSignal , :
(id)asynchronousFirstOrDefault:(id)defaultValue success:(BOOL *)success error:(NSError **)error;
describe(@"WOTRatingType", ^{ it(@"should parse json to model object", ^{ NSDictionary *json = @{ @"threshold": @5, @"type": @"1", @"rank_fields": @[ @"xp_amount_rank", @"xp_max_rank", @"battles_count_rank", @"spotted_count_rank", @"damage_dealt_rank", ] }; NSError *error; WOTRatingType *ratingType = [MTLJSONAdapter modelOfClass:WOTRatingType.class fromJSONDictionary:json error:&error]; [[error should] beNil]; [[ratingType should] beKindOfClass:WOTRatingType.class]; [[json[@"threshold"] should] equal:theValue(ratingType.threshold)]; * [[json[@"type"] should] equal:ratingType.type]; [[ratingType.rankFields shouldNot] beNil]; [[ratingType.rankFields should] beKindOfClass:NSArray.class]; [[ratingType.rankFields should] haveCountOf:5]; [[@"maximumXP" should] equal:ratingType.rankFields[1]]; }); });
* — - - Kiwi, , nil .
API :
; .
, , : query http-. 
, ReactiveCocoa , ( ). ( ) 2k LOC ( ~1k - , ~700 - ), : — ..
Kiwi
BDD- RSpec - , . «--».
OHTTPStubs
, web- . "", - .
.
There are only two hard things in Computer Science: cache invalidation and naming things.
-- Phil Karlton
« » :
..
, .
- inMemory CoreData , NSCache . , , — . NSURLConnection .
NSURLCache
NSURLCache NSURLRequest . , .
:
NSURLCache *urlCache = [[NSURLCache alloc] initWithMemoryCapacity:kInMemoryCacheSize diskCapacity:kOnDiskCacheSize diskPath:path]; [NSURLCache setSharedURLCache:urlCache];
, .
NSURLConnectionDelegate , "" :
- (NSCachedURLResponse *)connection:(NSURLConnection *)connection willCacheResponse:(NSCachedURLResponse *)cachedResponse;
?
NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse*)[cachedResponse response]; NSDictionary *headers = [httpResponse allHeaderFields]; NSString *cacheControl = [headers valueForKey:@"Cache-Control"]; NSString *expires = [headers valueForKey:@"Expires"]; if((cacheControl == nil) && (expires == nil)) { NSMutableDictionary *modifiedHeaders = [httpResponse.allHeaderFields mutableCopy]; NSString *expireDate = [NSDate RFC822StringFromDate:[[NSDate date] dateByAddingTimeInterval:cacheTime]]; NSDictionary *values = @{@"Expires": expireDate, @"Cache-Control": @"public"}; [modifiedHeaders addEntriesFromDictionary:values]; NSHTTPURLResponse *response = [[NSHTTPURLResponse alloc] initWithURL:httpResponse.URL statusCode:httpResponse.statusCode HTTPVersion:@"HTTP/1.1" headerFields:modifiedHeaders]; NSCachedURLResponse *modifiedCachedResponse = [[NSCachedURLResponse alloc] initWithResponse:response data:cachedResponse.data userInfo:cachedResponse.userInfo storagePolicy:NSURLCacheStorageAllowed]; return modifiedCachedResponse; } return cachedResponse;
HTTP- .
, ( , , ). , ( cacheTime ) .
— ( ). , , .
:
(, , ..); - NSURLCache , .
:
30 ; ; : 200- , NSURLConnection .
""
API (, ), fields :
. . . , .
, Player , JSON-, . , fields , NSDictionary . , - . , .
JSON -> NSObject Mantle , API ( RACSignal API ):
- (RACSignal *)searchPlayers:(NSString *)query limit:(NSUInteger)limit { NSMutableDictionary *parameters = [NSMutableDictionary dictionary]; parameters[@"search"] = query; if (limit) parameters[@"limit"] = @(limit); return [self getPath:@"account/list" parameters:parameters resultClass:WOTSearchPlayer.class]; }
, resultClass , ? :
- (RACSignal *)searchPlayers:(NSString *)query limit:(NSUInteger)limit resultClass:(Class)resultClass
, API, .
API , : , , "" . ( ). , ( AFJSONRequestOperation ):
op1 = [AFJSONRequestOperation JSONRequestOperationWithRequest:nil success:^(id JSON) { op2 = [AFJSONRequestOperation JSONRequestOperationWithRequest:nil success:^(id JSON) { op3 = [AFJSONRequestOperation JSONRequestOperationWithRequest:nil success:^(id JSON) { // Combine all the responses and return } failure:^(NSError *error) { // Handle error }]; } failure:^(NSError *error) { // Handle error }]; } failure:^(NSError *error) { // Handle error }];
, , . ReactiveCocoa , .
API :
- (RACSignal *)searchPlayers:(NSString *)query limit:(NSUInteger)limit; - (RACSignal *)fetchPlayers:(NSArray *)playerIDs;
RACSignal — , , . — , . / :
RACSignal *fetchPlayersAndRatings = [[[[API searchPlayers:@"" limit:0] flattenMap:^RACStream *(id players) { // skipping data processing return [API fetchPlayers:@[]]; }] flattenMap:^RACStream *(id fullPlayers) { // Skipping data processing return [API statsSliceForPlayer:nil hoursAgo:@[] fields:nil]; }] flattenMap:^RACStream *(id value) { // Compose all responses here id composedResponse; return [RACSignal return:composedResponse]; }]; [fetchPlayersAndRatings subscribeNext:^(id x) { // Fully composed response } error:^(NSError *error) { // All erros go to one place }];
ReactiveCocoa — , . , Promises Futures.
:
Core ( , ); WOT ( World of Tanks–); WOWP ( World of Warplanes–)
, , — . , ( ) CocoaPods , . subspecs , :
# Subspecs s.subspec 'Core' do |cs| cs.source_files = 'WGNAPIClient/Core/**/*.{h,m}' end s.subspec 'WOT' do |cs| cs.dependency 'WGNAPIClient/Core' cs.source_files = 'WGNAPIClient/WOT/**/*.{h,m}' end s.subspec 'WOWP' do |cs| cs.dependency 'WGNAPIClient/Core' cs.source_files = 'WGNAPIClient/WOWP/**/*.{h,m}' end
"" "" , :
pod 'WGNAPIClient/WOT' pod 'WGNAPIClient/WOWP'
. .
(98%). :
; .
:
context(@"API works with players data", ^{ it (@"should search players", ^{ stubResponse(@"/wot/account/list", @"players-search.json"); RACSignal *searchRequest = [client searchPlayers:@"garnett" limit:0]; NSArray *response = [searchRequest asynchronousFirstOrDefault:nil success:NULL error:&error]; [[error should] beNil]; [[response should] beKindOfClass:NSArray.class]; [[response.lastObject should] beKindOfClass:WOTSearchPlayer.class]; WOTSearchPlayer *player = response[0]; [[player.ID should] equal:@"1785514"]; }); });
, / , RACSignal , :
(id)asynchronousFirstOrDefault:(id)defaultValue success:(BOOL *)success error:(NSError **)error;
describe(@"WOTRatingType", ^{ it(@"should parse json to model object", ^{ NSDictionary *json = @{ @"threshold": @5, @"type": @"1", @"rank_fields": @[ @"xp_amount_rank", @"xp_max_rank", @"battles_count_rank", @"spotted_count_rank", @"damage_dealt_rank", ] }; NSError *error; WOTRatingType *ratingType = [MTLJSONAdapter modelOfClass:WOTRatingType.class fromJSONDictionary:json error:&error]; [[error should] beNil]; [[ratingType should] beKindOfClass:WOTRatingType.class]; [[json[@"threshold"] should] equal:theValue(ratingType.threshold)]; * [[json[@"type"] should] equal:ratingType.type]; [[ratingType.rankFields shouldNot] beNil]; [[ratingType.rankFields should] beKindOfClass:NSArray.class]; [[ratingType.rankFields should] haveCountOf:5]; [[@"maximumXP" should] equal:ratingType.rankFields[1]]; }); });
* — - - Kiwi, , nil .
API :
; .
, , : query http-. 
, ReactiveCocoa , ( ). ( ) 2k LOC ( ~1k - , ~700 - ), : — ..
Kiwi
BDD- RSpec - , . «--».
OHTTPStubs
, web- . "", - .
.
There are only two hard things in Computer Science: cache invalidation and naming things.
-- Phil Karlton
« » :
..
, .
- inMemory CoreData , NSCache . , , — . NSURLConnection .
NSURLCache
NSURLCache NSURLRequest . , .
:
NSURLCache *urlCache = [[NSURLCache alloc] initWithMemoryCapacity:kInMemoryCacheSize diskCapacity:kOnDiskCacheSize diskPath:path]; [NSURLCache setSharedURLCache:urlCache];
, .
NSURLConnectionDelegate , "" :
- (NSCachedURLResponse *)connection:(NSURLConnection *)connection willCacheResponse:(NSCachedURLResponse *)cachedResponse;
?
NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse*)[cachedResponse response]; NSDictionary *headers = [httpResponse allHeaderFields]; NSString *cacheControl = [headers valueForKey:@"Cache-Control"]; NSString *expires = [headers valueForKey:@"Expires"]; if((cacheControl == nil) && (expires == nil)) { NSMutableDictionary *modifiedHeaders = [httpResponse.allHeaderFields mutableCopy]; NSString *expireDate = [NSDate RFC822StringFromDate:[[NSDate date] dateByAddingTimeInterval:cacheTime]]; NSDictionary *values = @{@"Expires": expireDate, @"Cache-Control": @"public"}; [modifiedHeaders addEntriesFromDictionary:values]; NSHTTPURLResponse *response = [[NSHTTPURLResponse alloc] initWithURL:httpResponse.URL statusCode:httpResponse.statusCode HTTPVersion:@"HTTP/1.1" headerFields:modifiedHeaders]; NSCachedURLResponse *modifiedCachedResponse = [[NSCachedURLResponse alloc] initWithResponse:response data:cachedResponse.data userInfo:cachedResponse.userInfo storagePolicy:NSURLCacheStorageAllowed]; return modifiedCachedResponse; } return cachedResponse;
HTTP- .
, ( , , ). , ( cacheTime ) .
— ( ). , , .
:
(, , ..); - NSURLCache , .
:
30 ; ; : 200- , NSURLConnection .
""
API (, ), fields :
. . . , .
, Player , JSON-, . , fields , NSDictionary . , - . , .
JSON -> NSObject Mantle , API ( RACSignal API ):
- (RACSignal *)searchPlayers:(NSString *)query limit:(NSUInteger)limit { NSMutableDictionary *parameters = [NSMutableDictionary dictionary]; parameters[@"search"] = query; if (limit) parameters[@"limit"] = @(limit); return [self getPath:@"account/list" parameters:parameters resultClass:WOTSearchPlayer.class]; }
, resultClass , ? :
- (RACSignal *)searchPlayers:(NSString *)query limit:(NSUInteger)limit resultClass:(Class)resultClass
, API, .
API , : , , "" . ( ). , ( AFJSONRequestOperation ):
op1 = [AFJSONRequestOperation JSONRequestOperationWithRequest:nil success:^(id JSON) { op2 = [AFJSONRequestOperation JSONRequestOperationWithRequest:nil success:^(id JSON) { op3 = [AFJSONRequestOperation JSONRequestOperationWithRequest:nil success:^(id JSON) { // Combine all the responses and return } failure:^(NSError *error) { // Handle error }]; } failure:^(NSError *error) { // Handle error }]; } failure:^(NSError *error) { // Handle error }];
, , . ReactiveCocoa , .
API :
- (RACSignal *)searchPlayers:(NSString *)query limit:(NSUInteger)limit; - (RACSignal *)fetchPlayers:(NSArray *)playerIDs;
RACSignal — , , . — , . / :
RACSignal *fetchPlayersAndRatings = [[[[API searchPlayers:@"" limit:0] flattenMap:^RACStream *(id players) { // skipping data processing return [API fetchPlayers:@[]]; }] flattenMap:^RACStream *(id fullPlayers) { // Skipping data processing return [API statsSliceForPlayer:nil hoursAgo:@[] fields:nil]; }] flattenMap:^RACStream *(id value) { // Compose all responses here id composedResponse; return [RACSignal return:composedResponse]; }]; [fetchPlayersAndRatings subscribeNext:^(id x) { // Fully composed response } error:^(NSError *error) { // All erros go to one place }];
ReactiveCocoa — , . , Promises Futures.
:
Core ( , ); WOT ( World of Tanks–); WOWP ( World of Warplanes–)
, , — . , ( ) CocoaPods , . subspecs , :
# Subspecs s.subspec 'Core' do |cs| cs.source_files = 'WGNAPIClient/Core/**/*.{h,m}' end s.subspec 'WOT' do |cs| cs.dependency 'WGNAPIClient/Core' cs.source_files = 'WGNAPIClient/WOT/**/*.{h,m}' end s.subspec 'WOWP' do |cs| cs.dependency 'WGNAPIClient/Core' cs.source_files = 'WGNAPIClient/WOWP/**/*.{h,m}' end
"" "" , :
pod 'WGNAPIClient/WOT' pod 'WGNAPIClient/WOWP'
. .
(98%). :
; .
:
context(@"API works with players data", ^{ it (@"should search players", ^{ stubResponse(@"/wot/account/list", @"players-search.json"); RACSignal *searchRequest = [client searchPlayers:@"garnett" limit:0]; NSArray *response = [searchRequest asynchronousFirstOrDefault:nil success:NULL error:&error]; [[error should] beNil]; [[response should] beKindOfClass:NSArray.class]; [[response.lastObject should] beKindOfClass:WOTSearchPlayer.class]; WOTSearchPlayer *player = response[0]; [[player.ID should] equal:@"1785514"]; }); });
, / , RACSignal , :
(id)asynchronousFirstOrDefault:(id)defaultValue success:(BOOL *)success error:(NSError **)error;
describe(@"WOTRatingType", ^{ it(@"should parse json to model object", ^{ NSDictionary *json = @{ @"threshold": @5, @"type": @"1", @"rank_fields": @[ @"xp_amount_rank", @"xp_max_rank", @"battles_count_rank", @"spotted_count_rank", @"damage_dealt_rank", ] }; NSError *error; WOTRatingType *ratingType = [MTLJSONAdapter modelOfClass:WOTRatingType.class fromJSONDictionary:json error:&error]; [[error should] beNil]; [[ratingType should] beKindOfClass:WOTRatingType.class]; [[json[@"threshold"] should] equal:theValue(ratingType.threshold)]; * [[json[@"type"] should] equal:ratingType.type]; [[ratingType.rankFields shouldNot] beNil]; [[ratingType.rankFields should] beKindOfClass:NSArray.class]; [[ratingType.rankFields should] haveCountOf:5]; [[@"maximumXP" should] equal:ratingType.rankFields[1]]; }); });
* — - - Kiwi, , nil .
API :
; .
, , : query http-. 
, ReactiveCocoa , ( ). ( ) 2k LOC ( ~1k - , ~700 - ), : — ..
Kiwi
BDD- RSpec - , . «--».
OHTTPStubs
, web- . "", - .
.
There are only two hard things in Computer Science: cache invalidation and naming things.
-- Phil Karlton
« » :
..
, .
- inMemory CoreData , NSCache . , , — . NSURLConnection .
NSURLCache
NSURLCache NSURLRequest . , .
:
NSURLCache *urlCache = [[NSURLCache alloc] initWithMemoryCapacity:kInMemoryCacheSize diskCapacity:kOnDiskCacheSize diskPath:path]; [NSURLCache setSharedURLCache:urlCache];
, .
NSURLConnectionDelegate , "" :
- (NSCachedURLResponse *)connection:(NSURLConnection *)connection willCacheResponse:(NSCachedURLResponse *)cachedResponse;
?
NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse*)[cachedResponse response]; NSDictionary *headers = [httpResponse allHeaderFields]; NSString *cacheControl = [headers valueForKey:@"Cache-Control"]; NSString *expires = [headers valueForKey:@"Expires"]; if((cacheControl == nil) && (expires == nil)) { NSMutableDictionary *modifiedHeaders = [httpResponse.allHeaderFields mutableCopy]; NSString *expireDate = [NSDate RFC822StringFromDate:[[NSDate date] dateByAddingTimeInterval:cacheTime]]; NSDictionary *values = @{@"Expires": expireDate, @"Cache-Control": @"public"}; [modifiedHeaders addEntriesFromDictionary:values]; NSHTTPURLResponse *response = [[NSHTTPURLResponse alloc] initWithURL:httpResponse.URL statusCode:httpResponse.statusCode HTTPVersion:@"HTTP/1.1" headerFields:modifiedHeaders]; NSCachedURLResponse *modifiedCachedResponse = [[NSCachedURLResponse alloc] initWithResponse:response data:cachedResponse.data userInfo:cachedResponse.userInfo storagePolicy:NSURLCacheStorageAllowed]; return modifiedCachedResponse; } return cachedResponse;
HTTP- .
, ( , , ). , ( cacheTime ) .
— ( ). , , .
:
(, , ..); - NSURLCache , .
:
30 ; ; : 200- , NSURLConnection .
""
API (, ), fields :
. . . , .
, Player , JSON-, . , fields , NSDictionary . , - . , .
JSON -> NSObject Mantle , API ( RACSignal API ):
- (RACSignal *)searchPlayers:(NSString *)query limit:(NSUInteger)limit { NSMutableDictionary *parameters = [NSMutableDictionary dictionary]; parameters[@"search"] = query; if (limit) parameters[@"limit"] = @(limit); return [self getPath:@"account/list" parameters:parameters resultClass:WOTSearchPlayer.class]; }
, resultClass , ? :
- (RACSignal *)searchPlayers:(NSString *)query limit:(NSUInteger)limit resultClass:(Class)resultClass
, API, .
API , : , , "" . ( ). , ( AFJSONRequestOperation ):
op1 = [AFJSONRequestOperation JSONRequestOperationWithRequest:nil success:^(id JSON) { op2 = [AFJSONRequestOperation JSONRequestOperationWithRequest:nil success:^(id JSON) { op3 = [AFJSONRequestOperation JSONRequestOperationWithRequest:nil success:^(id JSON) { // Combine all the responses and return } failure:^(NSError *error) { // Handle error }]; } failure:^(NSError *error) { // Handle error }]; } failure:^(NSError *error) { // Handle error }];
, , . ReactiveCocoa , .
API :
- (RACSignal *)searchPlayers:(NSString *)query limit:(NSUInteger)limit; - (RACSignal *)fetchPlayers:(NSArray *)playerIDs;
RACSignal — , , . — , . / :
RACSignal *fetchPlayersAndRatings = [[[[API searchPlayers:@"" limit:0] flattenMap:^RACStream *(id players) { // skipping data processing return [API fetchPlayers:@[]]; }] flattenMap:^RACStream *(id fullPlayers) { // Skipping data processing return [API statsSliceForPlayer:nil hoursAgo:@[] fields:nil]; }] flattenMap:^RACStream *(id value) { // Compose all responses here id composedResponse; return [RACSignal return:composedResponse]; }]; [fetchPlayersAndRatings subscribeNext:^(id x) { // Fully composed response } error:^(NSError *error) { // All erros go to one place }];
ReactiveCocoa — , . , Promises Futures.
:
Core ( , ); WOT ( World of Tanks–); WOWP ( World of Warplanes–)
, , — . , ( ) CocoaPods , . subspecs , :
# Subspecs s.subspec 'Core' do |cs| cs.source_files = 'WGNAPIClient/Core/**/*.{h,m}' end s.subspec 'WOT' do |cs| cs.dependency 'WGNAPIClient/Core' cs.source_files = 'WGNAPIClient/WOT/**/*.{h,m}' end s.subspec 'WOWP' do |cs| cs.dependency 'WGNAPIClient/Core' cs.source_files = 'WGNAPIClient/WOWP/**/*.{h,m}' end
"" "" , :
pod 'WGNAPIClient/WOT' pod 'WGNAPIClient/WOWP'
. .
(98%). :
; .
:
context(@"API works with players data", ^{ it (@"should search players", ^{ stubResponse(@"/wot/account/list", @"players-search.json"); RACSignal *searchRequest = [client searchPlayers:@"garnett" limit:0]; NSArray *response = [searchRequest asynchronousFirstOrDefault:nil success:NULL error:&error]; [[error should] beNil]; [[response should] beKindOfClass:NSArray.class]; [[response.lastObject should] beKindOfClass:WOTSearchPlayer.class]; WOTSearchPlayer *player = response[0]; [[player.ID should] equal:@"1785514"]; }); });
, / , RACSignal , :
(id)asynchronousFirstOrDefault:(id)defaultValue success:(BOOL *)success error:(NSError **)error;
describe(@"WOTRatingType", ^{ it(@"should parse json to model object", ^{ NSDictionary *json = @{ @"threshold": @5, @"type": @"1", @"rank_fields": @[ @"xp_amount_rank", @"xp_max_rank", @"battles_count_rank", @"spotted_count_rank", @"damage_dealt_rank", ] }; NSError *error; WOTRatingType *ratingType = [MTLJSONAdapter modelOfClass:WOTRatingType.class fromJSONDictionary:json error:&error]; [[error should] beNil]; [[ratingType should] beKindOfClass:WOTRatingType.class]; [[json[@"threshold"] should] equal:theValue(ratingType.threshold)]; * [[json[@"type"] should] equal:ratingType.type]; [[ratingType.rankFields shouldNot] beNil]; [[ratingType.rankFields should] beKindOfClass:NSArray.class]; [[ratingType.rankFields should] haveCountOf:5]; [[@"maximumXP" should] equal:ratingType.rankFields[1]]; }); });
* — - - Kiwi, , nil .
API :
; .
, , : query http-. 
, ReactiveCocoa , ( ). ( ) 2k LOC ( ~1k - , ~700 - ), : — ..
Kiwi
BDD- RSpec - , . «--».
OHTTPStubs
, web- . "", - .
.
There are only two hard things in Computer Science: cache invalidation and naming things.
-- Phil Karlton
« » :
..
, .
- inMemory CoreData , NSCache . , , — . NSURLConnection .
NSURLCache
NSURLCache NSURLRequest . , .
:
NSURLCache *urlCache = [[NSURLCache alloc] initWithMemoryCapacity:kInMemoryCacheSize diskCapacity:kOnDiskCacheSize diskPath:path]; [NSURLCache setSharedURLCache:urlCache];
, .
NSURLConnectionDelegate , "" :
- (NSCachedURLResponse *)connection:(NSURLConnection *)connection willCacheResponse:(NSCachedURLResponse *)cachedResponse;
?
NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse*)[cachedResponse response]; NSDictionary *headers = [httpResponse allHeaderFields]; NSString *cacheControl = [headers valueForKey:@"Cache-Control"]; NSString *expires = [headers valueForKey:@"Expires"]; if((cacheControl == nil) && (expires == nil)) { NSMutableDictionary *modifiedHeaders = [httpResponse.allHeaderFields mutableCopy]; NSString *expireDate = [NSDate RFC822StringFromDate:[[NSDate date] dateByAddingTimeInterval:cacheTime]]; NSDictionary *values = @{@"Expires": expireDate, @"Cache-Control": @"public"}; [modifiedHeaders addEntriesFromDictionary:values]; NSHTTPURLResponse *response = [[NSHTTPURLResponse alloc] initWithURL:httpResponse.URL statusCode:httpResponse.statusCode HTTPVersion:@"HTTP/1.1" headerFields:modifiedHeaders]; NSCachedURLResponse *modifiedCachedResponse = [[NSCachedURLResponse alloc] initWithResponse:response data:cachedResponse.data userInfo:cachedResponse.userInfo storagePolicy:NSURLCacheStorageAllowed]; return modifiedCachedResponse; } return cachedResponse;
HTTP- .
, ( , , ). , ( cacheTime ) .
— ( ). , , .
:
(, , ..); - NSURLCache , .
:
30 ; ; : 200- , NSURLConnection .
""
API (, ), fields :
. . . , .
, Player , JSON-, . , fields , NSDictionary . , - . , .
JSON -> NSObject Mantle , API ( RACSignal API ):
- (RACSignal *)searchPlayers:(NSString *)query limit:(NSUInteger)limit { NSMutableDictionary *parameters = [NSMutableDictionary dictionary]; parameters[@"search"] = query; if (limit) parameters[@"limit"] = @(limit); return [self getPath:@"account/list" parameters:parameters resultClass:WOTSearchPlayer.class]; }
, resultClass , ? :
- (RACSignal *)searchPlayers:(NSString *)query limit:(NSUInteger)limit resultClass:(Class)resultClass
, API, .
API , : , , "" . ( ). , ( AFJSONRequestOperation ):
op1 = [AFJSONRequestOperation JSONRequestOperationWithRequest:nil success:^(id JSON) { op2 = [AFJSONRequestOperation JSONRequestOperationWithRequest:nil success:^(id JSON) { op3 = [AFJSONRequestOperation JSONRequestOperationWithRequest:nil success:^(id JSON) { // Combine all the responses and return } failure:^(NSError *error) { // Handle error }]; } failure:^(NSError *error) { // Handle error }]; } failure:^(NSError *error) { // Handle error }];
, , . ReactiveCocoa , .
API :
- (RACSignal *)searchPlayers:(NSString *)query limit:(NSUInteger)limit; - (RACSignal *)fetchPlayers:(NSArray *)playerIDs;
RACSignal — , , . — , . / :
RACSignal *fetchPlayersAndRatings = [[[[API searchPlayers:@"" limit:0] flattenMap:^RACStream *(id players) { // skipping data processing return [API fetchPlayers:@[]]; }] flattenMap:^RACStream *(id fullPlayers) { // Skipping data processing return [API statsSliceForPlayer:nil hoursAgo:@[] fields:nil]; }] flattenMap:^RACStream *(id value) { // Compose all responses here id composedResponse; return [RACSignal return:composedResponse]; }]; [fetchPlayersAndRatings subscribeNext:^(id x) { // Fully composed response } error:^(NSError *error) { // All erros go to one place }];
ReactiveCocoa — , . , Promises Futures.
:
Core ( , ); WOT ( World of Tanks–); WOWP ( World of Warplanes–)
, , — . , ( ) CocoaPods , . subspecs , :
# Subspecs s.subspec 'Core' do |cs| cs.source_files = 'WGNAPIClient/Core/**/*.{h,m}' end s.subspec 'WOT' do |cs| cs.dependency 'WGNAPIClient/Core' cs.source_files = 'WGNAPIClient/WOT/**/*.{h,m}' end s.subspec 'WOWP' do |cs| cs.dependency 'WGNAPIClient/Core' cs.source_files = 'WGNAPIClient/WOWP/**/*.{h,m}' end
"" "" , :
pod 'WGNAPIClient/WOT' pod 'WGNAPIClient/WOWP'
. .
(98%). :
; .
:
context(@"API works with players data", ^{ it (@"should search players", ^{ stubResponse(@"/wot/account/list", @"players-search.json"); RACSignal *searchRequest = [client searchPlayers:@"garnett" limit:0]; NSArray *response = [searchRequest asynchronousFirstOrDefault:nil success:NULL error:&error]; [[error should] beNil]; [[response should] beKindOfClass:NSArray.class]; [[response.lastObject should] beKindOfClass:WOTSearchPlayer.class]; WOTSearchPlayer *player = response[0]; [[player.ID should] equal:@"1785514"]; }); });
, / , RACSignal , :
(id)asynchronousFirstOrDefault:(id)defaultValue success:(BOOL *)success error:(NSError **)error;
describe(@"WOTRatingType", ^{ it(@"should parse json to model object", ^{ NSDictionary *json = @{ @"threshold": @5, @"type": @"1", @"rank_fields": @[ @"xp_amount_rank", @"xp_max_rank", @"battles_count_rank", @"spotted_count_rank", @"damage_dealt_rank", ] }; NSError *error; WOTRatingType *ratingType = [MTLJSONAdapter modelOfClass:WOTRatingType.class fromJSONDictionary:json error:&error]; [[error should] beNil]; [[ratingType should] beKindOfClass:WOTRatingType.class]; [[json[@"threshold"] should] equal:theValue(ratingType.threshold)]; * [[json[@"type"] should] equal:ratingType.type]; [[ratingType.rankFields shouldNot] beNil]; [[ratingType.rankFields should] beKindOfClass:NSArray.class]; [[ratingType.rankFields should] haveCountOf:5]; [[@"maximumXP" should] equal:ratingType.rankFields[1]]; }); });
* — - - Kiwi, , nil .
API :
; .
, , : query http-. 
, ReactiveCocoa , ( ). ( ) 2k LOC ( ~1k - , ~700 - ), : — ..
Kiwi
BDD- RSpec - , . «--».
OHTTPStubs
, web- . "", - .
.
There are only two hard things in Computer Science: cache invalidation and naming things.
-- Phil Karlton
« » :
..
, .
- inMemory CoreData , NSCache . , , — . NSURLConnection .
NSURLCache
NSURLCache NSURLRequest . , .
:
NSURLCache *urlCache = [[NSURLCache alloc] initWithMemoryCapacity:kInMemoryCacheSize diskCapacity:kOnDiskCacheSize diskPath:path]; [NSURLCache setSharedURLCache:urlCache];
, .
NSURLConnectionDelegate , "" :
- (NSCachedURLResponse *)connection:(NSURLConnection *)connection willCacheResponse:(NSCachedURLResponse *)cachedResponse;
?
NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse*)[cachedResponse response]; NSDictionary *headers = [httpResponse allHeaderFields]; NSString *cacheControl = [headers valueForKey:@"Cache-Control"]; NSString *expires = [headers valueForKey:@"Expires"]; if((cacheControl == nil) && (expires == nil)) { NSMutableDictionary *modifiedHeaders = [httpResponse.allHeaderFields mutableCopy]; NSString *expireDate = [NSDate RFC822StringFromDate:[[NSDate date] dateByAddingTimeInterval:cacheTime]]; NSDictionary *values = @{@"Expires": expireDate, @"Cache-Control": @"public"}; [modifiedHeaders addEntriesFromDictionary:values]; NSHTTPURLResponse *response = [[NSHTTPURLResponse alloc] initWithURL:httpResponse.URL statusCode:httpResponse.statusCode HTTPVersion:@"HTTP/1.1" headerFields:modifiedHeaders]; NSCachedURLResponse *modifiedCachedResponse = [[NSCachedURLResponse alloc] initWithResponse:response data:cachedResponse.data userInfo:cachedResponse.userInfo storagePolicy:NSURLCacheStorageAllowed]; return modifiedCachedResponse; } return cachedResponse;
HTTP- .
, ( , , ). , ( cacheTime ) .
— ( ). , , .
:
(, , ..); - NSURLCache , .
:
30 ; ; : 200- , NSURLConnection .
""
API (, ), fields :
. . . , .
, Player , JSON-, . , fields , NSDictionary . , - . , .
JSON -> NSObject Mantle , API ( RACSignal API ):
- (RACSignal *)searchPlayers:(NSString *)query limit:(NSUInteger)limit { NSMutableDictionary *parameters = [NSMutableDictionary dictionary]; parameters[@"search"] = query; if (limit) parameters[@"limit"] = @(limit); return [self getPath:@"account/list" parameters:parameters resultClass:WOTSearchPlayer.class]; }
, resultClass , ? :
- (RACSignal *)searchPlayers:(NSString *)query limit:(NSUInteger)limit resultClass:(Class)resultClass
, API, .
API , : , , "" . ( ). , ( AFJSONRequestOperation ):
op1 = [AFJSONRequestOperation JSONRequestOperationWithRequest:nil success:^(id JSON) { op2 = [AFJSONRequestOperation JSONRequestOperationWithRequest:nil success:^(id JSON) { op3 = [AFJSONRequestOperation JSONRequestOperationWithRequest:nil success:^(id JSON) { // Combine all the responses and return } failure:^(NSError *error) { // Handle error }]; } failure:^(NSError *error) { // Handle error }]; } failure:^(NSError *error) { // Handle error }];
, , . ReactiveCocoa , .
API :
- (RACSignal *)searchPlayers:(NSString *)query limit:(NSUInteger)limit; - (RACSignal *)fetchPlayers:(NSArray *)playerIDs;
RACSignal — , , . — , . / :
RACSignal *fetchPlayersAndRatings = [[[[API searchPlayers:@"" limit:0] flattenMap:^RACStream *(id players) { // skipping data processing return [API fetchPlayers:@[]]; }] flattenMap:^RACStream *(id fullPlayers) { // Skipping data processing return [API statsSliceForPlayer:nil hoursAgo:@[] fields:nil]; }] flattenMap:^RACStream *(id value) { // Compose all responses here id composedResponse; return [RACSignal return:composedResponse]; }]; [fetchPlayersAndRatings subscribeNext:^(id x) { // Fully composed response } error:^(NSError *error) { // All erros go to one place }];
ReactiveCocoa — , . , Promises Futures.
:
Core ( , ); WOT ( World of Tanks–); WOWP ( World of Warplanes–)
, , — . , ( ) CocoaPods , . subspecs , :
# Subspecs s.subspec 'Core' do |cs| cs.source_files = 'WGNAPIClient/Core/**/*.{h,m}' end s.subspec 'WOT' do |cs| cs.dependency 'WGNAPIClient/Core' cs.source_files = 'WGNAPIClient/WOT/**/*.{h,m}' end s.subspec 'WOWP' do |cs| cs.dependency 'WGNAPIClient/Core' cs.source_files = 'WGNAPIClient/WOWP/**/*.{h,m}' end
"" "" , :
pod 'WGNAPIClient/WOT' pod 'WGNAPIClient/WOWP'
. .
(98%). :
; .
:
context(@"API works with players data", ^{ it (@"should search players", ^{ stubResponse(@"/wot/account/list", @"players-search.json"); RACSignal *searchRequest = [client searchPlayers:@"garnett" limit:0]; NSArray *response = [searchRequest asynchronousFirstOrDefault:nil success:NULL error:&error]; [[error should] beNil]; [[response should] beKindOfClass:NSArray.class]; [[response.lastObject should] beKindOfClass:WOTSearchPlayer.class]; WOTSearchPlayer *player = response[0]; [[player.ID should] equal:@"1785514"]; }); });
, / , RACSignal , :
(id)asynchronousFirstOrDefault:(id)defaultValue success:(BOOL *)success error:(NSError **)error;
describe(@"WOTRatingType", ^{ it(@"should parse json to model object", ^{ NSDictionary *json = @{ @"threshold": @5, @"type": @"1", @"rank_fields": @[ @"xp_amount_rank", @"xp_max_rank", @"battles_count_rank", @"spotted_count_rank", @"damage_dealt_rank", ] }; NSError *error; WOTRatingType *ratingType = [MTLJSONAdapter modelOfClass:WOTRatingType.class fromJSONDictionary:json error:&error]; [[error should] beNil]; [[ratingType should] beKindOfClass:WOTRatingType.class]; [[json[@"threshold"] should] equal:theValue(ratingType.threshold)]; * [[json[@"type"] should] equal:ratingType.type]; [[ratingType.rankFields shouldNot] beNil]; [[ratingType.rankFields should] beKindOfClass:NSArray.class]; [[ratingType.rankFields should] haveCountOf:5]; [[@"maximumXP" should] equal:ratingType.rankFields[1]]; }); });
* — - - Kiwi, , nil .
API :
; .
, , : query http-. 
, ReactiveCocoa , ( ). ( ) 2k LOC ( ~1k - , ~700 - ), : — ..
Kiwi
BDD- RSpec - , . «--».
OHTTPStubs
, web- . "", - .
.
There are only two hard things in Computer Science: cache invalidation and naming things.
-- Phil Karlton
« » :
..
, .
- inMemory CoreData , NSCache . , , — . NSURLConnection .
NSURLCache
NSURLCache NSURLRequest . , .
:
NSURLCache *urlCache = [[NSURLCache alloc] initWithMemoryCapacity:kInMemoryCacheSize diskCapacity:kOnDiskCacheSize diskPath:path]; [NSURLCache setSharedURLCache:urlCache];
, .
NSURLConnectionDelegate , "" :
- (NSCachedURLResponse *)connection:(NSURLConnection *)connection willCacheResponse:(NSCachedURLResponse *)cachedResponse;
?
NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse*)[cachedResponse response]; NSDictionary *headers = [httpResponse allHeaderFields]; NSString *cacheControl = [headers valueForKey:@"Cache-Control"]; NSString *expires = [headers valueForKey:@"Expires"]; if((cacheControl == nil) && (expires == nil)) { NSMutableDictionary *modifiedHeaders = [httpResponse.allHeaderFields mutableCopy]; NSString *expireDate = [NSDate RFC822StringFromDate:[[NSDate date] dateByAddingTimeInterval:cacheTime]]; NSDictionary *values = @{@"Expires": expireDate, @"Cache-Control": @"public"}; [modifiedHeaders addEntriesFromDictionary:values]; NSHTTPURLResponse *response = [[NSHTTPURLResponse alloc] initWithURL:httpResponse.URL statusCode:httpResponse.statusCode HTTPVersion:@"HTTP/1.1" headerFields:modifiedHeaders]; NSCachedURLResponse *modifiedCachedResponse = [[NSCachedURLResponse alloc] initWithResponse:response data:cachedResponse.data userInfo:cachedResponse.userInfo storagePolicy:NSURLCacheStorageAllowed]; return modifiedCachedResponse; } return cachedResponse;
HTTP- .
, ( , , ). , ( cacheTime ) .
— ( ). , , .
:
(, , ..); - NSURLCache , .
:
30 ; ; : 200- , NSURLConnection .
""
API (, ), fields :
. . . , .
, Player , JSON-, . , fields , NSDictionary . , - . , .
JSON -> NSObject Mantle , API ( RACSignal API ):
- (RACSignal *)searchPlayers:(NSString *)query limit:(NSUInteger)limit { NSMutableDictionary *parameters = [NSMutableDictionary dictionary]; parameters[@"search"] = query; if (limit) parameters[@"limit"] = @(limit); return [self getPath:@"account/list" parameters:parameters resultClass:WOTSearchPlayer.class]; }
, resultClass , ? :
- (RACSignal *)searchPlayers:(NSString *)query limit:(NSUInteger)limit resultClass:(Class)resultClass
, API, .
API , : , , "" . ( ). , ( AFJSONRequestOperation ):
op1 = [AFJSONRequestOperation JSONRequestOperationWithRequest:nil success:^(id JSON) { op2 = [AFJSONRequestOperation JSONRequestOperationWithRequest:nil success:^(id JSON) { op3 = [AFJSONRequestOperation JSONRequestOperationWithRequest:nil success:^(id JSON) { // Combine all the responses and return } failure:^(NSError *error) { // Handle error }]; } failure:^(NSError *error) { // Handle error }]; } failure:^(NSError *error) { // Handle error }];
, , . ReactiveCocoa , .
API :
- (RACSignal *)searchPlayers:(NSString *)query limit:(NSUInteger)limit; - (RACSignal *)fetchPlayers:(NSArray *)playerIDs;
RACSignal — , , . — , . / :
RACSignal *fetchPlayersAndRatings = [[[[API searchPlayers:@"" limit:0] flattenMap:^RACStream *(id players) { // skipping data processing return [API fetchPlayers:@[]]; }] flattenMap:^RACStream *(id fullPlayers) { // Skipping data processing return [API statsSliceForPlayer:nil hoursAgo:@[] fields:nil]; }] flattenMap:^RACStream *(id value) { // Compose all responses here id composedResponse; return [RACSignal return:composedResponse]; }]; [fetchPlayersAndRatings subscribeNext:^(id x) { // Fully composed response } error:^(NSError *error) { // All erros go to one place }];
ReactiveCocoa — , . , Promises Futures.
:
Core ( , ); WOT ( World of Tanks–); WOWP ( World of Warplanes–)
, , — . , ( ) CocoaPods , . subspecs , :
# Subspecs s.subspec 'Core' do |cs| cs.source_files = 'WGNAPIClient/Core/**/*.{h,m}' end s.subspec 'WOT' do |cs| cs.dependency 'WGNAPIClient/Core' cs.source_files = 'WGNAPIClient/WOT/**/*.{h,m}' end s.subspec 'WOWP' do |cs| cs.dependency 'WGNAPIClient/Core' cs.source_files = 'WGNAPIClient/WOWP/**/*.{h,m}' end
"" "" , :
pod 'WGNAPIClient/WOT' pod 'WGNAPIClient/WOWP'
. .
(98%). :
; .
:
context(@"API works with players data", ^{ it (@"should search players", ^{ stubResponse(@"/wot/account/list", @"players-search.json"); RACSignal *searchRequest = [client searchPlayers:@"garnett" limit:0]; NSArray *response = [searchRequest asynchronousFirstOrDefault:nil success:NULL error:&error]; [[error should] beNil]; [[response should] beKindOfClass:NSArray.class]; [[response.lastObject should] beKindOfClass:WOTSearchPlayer.class]; WOTSearchPlayer *player = response[0]; [[player.ID should] equal:@"1785514"]; }); });
, / , RACSignal , :
(id)asynchronousFirstOrDefault:(id)defaultValue success:(BOOL *)success error:(NSError **)error;
describe(@"WOTRatingType", ^{ it(@"should parse json to model object", ^{ NSDictionary *json = @{ @"threshold": @5, @"type": @"1", @"rank_fields": @[ @"xp_amount_rank", @"xp_max_rank", @"battles_count_rank", @"spotted_count_rank", @"damage_dealt_rank", ] }; NSError *error; WOTRatingType *ratingType = [MTLJSONAdapter modelOfClass:WOTRatingType.class fromJSONDictionary:json error:&error]; [[error should] beNil]; [[ratingType should] beKindOfClass:WOTRatingType.class]; [[json[@"threshold"] should] equal:theValue(ratingType.threshold)]; * [[json[@"type"] should] equal:ratingType.type]; [[ratingType.rankFields shouldNot] beNil]; [[ratingType.rankFields should] beKindOfClass:NSArray.class]; [[ratingType.rankFields should] haveCountOf:5]; [[@"maximumXP" should] equal:ratingType.rankFields[1]]; }); });
* — - - Kiwi, , nil .
API :
; .
, , : query http-. 
, ReactiveCocoa , ( ). ( ) 2k LOC ( ~1k - , ~700 - ), : — ..
Kiwi
BDD- RSpec - , . «--».
OHTTPStubs
, web- . "", - .
.
There are only two hard things in Computer Science: cache invalidation and naming things.
-- Phil Karlton
« » :
..
, .
- inMemory CoreData , NSCache . , , — . NSURLConnection .
NSURLCache
NSURLCache NSURLRequest . , .
:
NSURLCache *urlCache = [[NSURLCache alloc] initWithMemoryCapacity:kInMemoryCacheSize diskCapacity:kOnDiskCacheSize diskPath:path]; [NSURLCache setSharedURLCache:urlCache];
, .
NSURLConnectionDelegate , "" :
- (NSCachedURLResponse *)connection:(NSURLConnection *)connection willCacheResponse:(NSCachedURLResponse *)cachedResponse;
?
NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse*)[cachedResponse response]; NSDictionary *headers = [httpResponse allHeaderFields]; NSString *cacheControl = [headers valueForKey:@"Cache-Control"]; NSString *expires = [headers valueForKey:@"Expires"]; if((cacheControl == nil) && (expires == nil)) { NSMutableDictionary *modifiedHeaders = [httpResponse.allHeaderFields mutableCopy]; NSString *expireDate = [NSDate RFC822StringFromDate:[[NSDate date] dateByAddingTimeInterval:cacheTime]]; NSDictionary *values = @{@"Expires": expireDate, @"Cache-Control": @"public"}; [modifiedHeaders addEntriesFromDictionary:values]; NSHTTPURLResponse *response = [[NSHTTPURLResponse alloc] initWithURL:httpResponse.URL statusCode:httpResponse.statusCode HTTPVersion:@"HTTP/1.1" headerFields:modifiedHeaders]; NSCachedURLResponse *modifiedCachedResponse = [[NSCachedURLResponse alloc] initWithResponse:response data:cachedResponse.data userInfo:cachedResponse.userInfo storagePolicy:NSURLCacheStorageAllowed]; return modifiedCachedResponse; } return cachedResponse;
HTTP- .
, ( , , ). , ( cacheTime ) .
— ( ). , , .
:
(, , ..); - NSURLCache , .
:
30 ; ; : 200- , NSURLConnection .
""
API (, ), fields :
. . . , .
, Player , JSON-, . , fields , NSDictionary . , - . , .
JSON -> NSObject Mantle , API ( RACSignal API ):
- (RACSignal *)searchPlayers:(NSString *)query limit:(NSUInteger)limit { NSMutableDictionary *parameters = [NSMutableDictionary dictionary]; parameters[@"search"] = query; if (limit) parameters[@"limit"] = @(limit); return [self getPath:@"account/list" parameters:parameters resultClass:WOTSearchPlayer.class]; }
, resultClass , ? :
- (RACSignal *)searchPlayers:(NSString *)query limit:(NSUInteger)limit resultClass:(Class)resultClass
, API, .
API , : , , "" . ( ). , ( AFJSONRequestOperation ):
op1 = [AFJSONRequestOperation JSONRequestOperationWithRequest:nil success:^(id JSON) { op2 = [AFJSONRequestOperation JSONRequestOperationWithRequest:nil success:^(id JSON) { op3 = [AFJSONRequestOperation JSONRequestOperationWithRequest:nil success:^(id JSON) { // Combine all the responses and return } failure:^(NSError *error) { // Handle error }]; } failure:^(NSError *error) { // Handle error }]; } failure:^(NSError *error) { // Handle error }];
, , . ReactiveCocoa , .
API :
- (RACSignal *)searchPlayers:(NSString *)query limit:(NSUInteger)limit; - (RACSignal *)fetchPlayers:(NSArray *)playerIDs;
RACSignal — , , . — , . / :
RACSignal *fetchPlayersAndRatings = [[[[API searchPlayers:@"" limit:0] flattenMap:^RACStream *(id players) { // skipping data processing return [API fetchPlayers:@[]]; }] flattenMap:^RACStream *(id fullPlayers) { // Skipping data processing return [API statsSliceForPlayer:nil hoursAgo:@[] fields:nil]; }] flattenMap:^RACStream *(id value) { // Compose all responses here id composedResponse; return [RACSignal return:composedResponse]; }]; [fetchPlayersAndRatings subscribeNext:^(id x) { // Fully composed response } error:^(NSError *error) { // All erros go to one place }];
ReactiveCocoa — , . , Promises Futures.
:
Core ( , ); WOT ( World of Tanks–); WOWP ( World of Warplanes–)
, , — . , ( ) CocoaPods , . subspecs , :
# Subspecs s.subspec 'Core' do |cs| cs.source_files = 'WGNAPIClient/Core/**/*.{h,m}' end s.subspec 'WOT' do |cs| cs.dependency 'WGNAPIClient/Core' cs.source_files = 'WGNAPIClient/WOT/**/*.{h,m}' end s.subspec 'WOWP' do |cs| cs.dependency 'WGNAPIClient/Core' cs.source_files = 'WGNAPIClient/WOWP/**/*.{h,m}' end
"" "" , :
pod 'WGNAPIClient/WOT' pod 'WGNAPIClient/WOWP'
. .
(98%). :
; .
:
context(@"API works with players data", ^{ it (@"should search players", ^{ stubResponse(@"/wot/account/list", @"players-search.json"); RACSignal *searchRequest = [client searchPlayers:@"garnett" limit:0]; NSArray *response = [searchRequest asynchronousFirstOrDefault:nil success:NULL error:&error]; [[error should] beNil]; [[response should] beKindOfClass:NSArray.class]; [[response.lastObject should] beKindOfClass:WOTSearchPlayer.class]; WOTSearchPlayer *player = response[0]; [[player.ID should] equal:@"1785514"]; }); });
, / , RACSignal , :
(id)asynchronousFirstOrDefault:(id)defaultValue success:(BOOL *)success error:(NSError **)error;
describe(@"WOTRatingType", ^{ it(@"should parse json to model object", ^{ NSDictionary *json = @{ @"threshold": @5, @"type": @"1", @"rank_fields": @[ @"xp_amount_rank", @"xp_max_rank", @"battles_count_rank", @"spotted_count_rank", @"damage_dealt_rank", ] }; NSError *error; WOTRatingType *ratingType = [MTLJSONAdapter modelOfClass:WOTRatingType.class fromJSONDictionary:json error:&error]; [[error should] beNil]; [[ratingType should] beKindOfClass:WOTRatingType.class]; [[json[@"threshold"] should] equal:theValue(ratingType.threshold)]; * [[json[@"type"] should] equal:ratingType.type]; [[ratingType.rankFields shouldNot] beNil]; [[ratingType.rankFields should] beKindOfClass:NSArray.class]; [[ratingType.rankFields should] haveCountOf:5]; [[@"maximumXP" should] equal:ratingType.rankFields[1]]; }); });
* — - - Kiwi, , nil .
API :
; .
, , : query http-. 
, ReactiveCocoa , ( ). ( ) 2k LOC ( ~1k - , ~700 - ), : — ..
Kiwi
BDD- RSpec - , . «--».
OHTTPStubs
, web- . "", - .
.
There are only two hard things in Computer Science: cache invalidation and naming things.
-- Phil Karlton
« » :
..
, .
- inMemory CoreData , NSCache . , , — . NSURLConnection .
NSURLCache
NSURLCache NSURLRequest . , .
:
NSURLCache *urlCache = [[NSURLCache alloc] initWithMemoryCapacity:kInMemoryCacheSize diskCapacity:kOnDiskCacheSize diskPath:path]; [NSURLCache setSharedURLCache:urlCache];
, .
NSURLConnectionDelegate , "" :
- (NSCachedURLResponse *)connection:(NSURLConnection *)connection willCacheResponse:(NSCachedURLResponse *)cachedResponse;
?
NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse*)[cachedResponse response]; NSDictionary *headers = [httpResponse allHeaderFields]; NSString *cacheControl = [headers valueForKey:@"Cache-Control"]; NSString *expires = [headers valueForKey:@"Expires"]; if((cacheControl == nil) && (expires == nil)) { NSMutableDictionary *modifiedHeaders = [httpResponse.allHeaderFields mutableCopy]; NSString *expireDate = [NSDate RFC822StringFromDate:[[NSDate date] dateByAddingTimeInterval:cacheTime]]; NSDictionary *values = @{@"Expires": expireDate, @"Cache-Control": @"public"}; [modifiedHeaders addEntriesFromDictionary:values]; NSHTTPURLResponse *response = [[NSHTTPURLResponse alloc] initWithURL:httpResponse.URL statusCode:httpResponse.statusCode HTTPVersion:@"HTTP/1.1" headerFields:modifiedHeaders]; NSCachedURLResponse *modifiedCachedResponse = [[NSCachedURLResponse alloc] initWithResponse:response data:cachedResponse.data userInfo:cachedResponse.userInfo storagePolicy:NSURLCacheStorageAllowed]; return modifiedCachedResponse; } return cachedResponse;
HTTP- .
, ( , , ). , ( cacheTime ) .
— ( ). , , .
:
(, , ..); - NSURLCache , .
:
30 ; ; : 200- , NSURLConnection .
""
API (, ), fields :
. . . , .
, Player , JSON-, . , fields , NSDictionary . , - . , .
JSON -> NSObject Mantle , API ( RACSignal API ):
- (RACSignal *)searchPlayers:(NSString *)query limit:(NSUInteger)limit { NSMutableDictionary *parameters = [NSMutableDictionary dictionary]; parameters[@"search"] = query; if (limit) parameters[@"limit"] = @(limit); return [self getPath:@"account/list" parameters:parameters resultClass:WOTSearchPlayer.class]; }
, resultClass , ? :
- (RACSignal *)searchPlayers:(NSString *)query limit:(NSUInteger)limit resultClass:(Class)resultClass
, API, .
API , : , , "" . ( ). , ( AFJSONRequestOperation ):
op1 = [AFJSONRequestOperation JSONRequestOperationWithRequest:nil success:^(id JSON) { op2 = [AFJSONRequestOperation JSONRequestOperationWithRequest:nil success:^(id JSON) { op3 = [AFJSONRequestOperation JSONRequestOperationWithRequest:nil success:^(id JSON) { // Combine all the responses and return } failure:^(NSError *error) { // Handle error }]; } failure:^(NSError *error) { // Handle error }]; } failure:^(NSError *error) { // Handle error }];
, , . ReactiveCocoa , .
API :
- (RACSignal *)searchPlayers:(NSString *)query limit:(NSUInteger)limit; - (RACSignal *)fetchPlayers:(NSArray *)playerIDs;
RACSignal — , , . — , . / :
RACSignal *fetchPlayersAndRatings = [[[[API searchPlayers:@"" limit:0] flattenMap:^RACStream *(id players) { // skipping data processing return [API fetchPlayers:@[]]; }] flattenMap:^RACStream *(id fullPlayers) { // Skipping data processing return [API statsSliceForPlayer:nil hoursAgo:@[] fields:nil]; }] flattenMap:^RACStream *(id value) { // Compose all responses here id composedResponse; return [RACSignal return:composedResponse]; }]; [fetchPlayersAndRatings subscribeNext:^(id x) { // Fully composed response } error:^(NSError *error) { // All erros go to one place }];
ReactiveCocoa — , . , Promises Futures.
:
Core ( , ); WOT ( World of Tanks–); WOWP ( World of Warplanes–)
, , — . , ( ) CocoaPods , . subspecs , :
# Subspecs s.subspec 'Core' do |cs| cs.source_files = 'WGNAPIClient/Core/**/*.{h,m}' end s.subspec 'WOT' do |cs| cs.dependency 'WGNAPIClient/Core' cs.source_files = 'WGNAPIClient/WOT/**/*.{h,m}' end s.subspec 'WOWP' do |cs| cs.dependency 'WGNAPIClient/Core' cs.source_files = 'WGNAPIClient/WOWP/**/*.{h,m}' end
"" "" , :
pod 'WGNAPIClient/WOT' pod 'WGNAPIClient/WOWP'
. .
(98%). :
; .
:
context(@"API works with players data", ^{ it (@"should search players", ^{ stubResponse(@"/wot/account/list", @"players-search.json"); RACSignal *searchRequest = [client searchPlayers:@"garnett" limit:0]; NSArray *response = [searchRequest asynchronousFirstOrDefault:nil success:NULL error:&error]; [[error should] beNil]; [[response should] beKindOfClass:NSArray.class]; [[response.lastObject should] beKindOfClass:WOTSearchPlayer.class]; WOTSearchPlayer *player = response[0]; [[player.ID should] equal:@"1785514"]; }); });
, / , RACSignal , :
(id)asynchronousFirstOrDefault:(id)defaultValue success:(BOOL *)success error:(NSError **)error;
describe(@"WOTRatingType", ^{ it(@"should parse json to model object", ^{ NSDictionary *json = @{ @"threshold": @5, @"type": @"1", @"rank_fields": @[ @"xp_amount_rank", @"xp_max_rank", @"battles_count_rank", @"spotted_count_rank", @"damage_dealt_rank", ] }; NSError *error; WOTRatingType *ratingType = [MTLJSONAdapter modelOfClass:WOTRatingType.class fromJSONDictionary:json error:&error]; [[error should] beNil]; [[ratingType should] beKindOfClass:WOTRatingType.class]; [[json[@"threshold"] should] equal:theValue(ratingType.threshold)]; * [[json[@"type"] should] equal:ratingType.type]; [[ratingType.rankFields shouldNot] beNil]; [[ratingType.rankFields should] beKindOfClass:NSArray.class]; [[ratingType.rankFields should] haveCountOf:5]; [[@"maximumXP" should] equal:ratingType.rankFields[1]]; }); });
* — - - Kiwi, , nil .
API :
; .
, , : query http-. 
, ReactiveCocoa , ( ). ( ) 2k LOC ( ~1k - , ~700 - ), : — ..
Kiwi
BDD- RSpec - , . «--».
OHTTPStubs
, web- . "", - .
.
There are only two hard things in Computer Science: cache invalidation and naming things.
-- Phil Karlton
« » :
..
, .
- inMemory CoreData , NSCache . , , — . NSURLConnection .
NSURLCache
NSURLCache NSURLRequest . , .
:
NSURLCache *urlCache = [[NSURLCache alloc] initWithMemoryCapacity:kInMemoryCacheSize diskCapacity:kOnDiskCacheSize diskPath:path]; [NSURLCache setSharedURLCache:urlCache];
, .
NSURLConnectionDelegate , "" :
- (NSCachedURLResponse *)connection:(NSURLConnection *)connection willCacheResponse:(NSCachedURLResponse *)cachedResponse;
?
NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse*)[cachedResponse response]; NSDictionary *headers = [httpResponse allHeaderFields]; NSString *cacheControl = [headers valueForKey:@"Cache-Control"]; NSString *expires = [headers valueForKey:@"Expires"]; if((cacheControl == nil) && (expires == nil)) { NSMutableDictionary *modifiedHeaders = [httpResponse.allHeaderFields mutableCopy]; NSString *expireDate = [NSDate RFC822StringFromDate:[[NSDate date] dateByAddingTimeInterval:cacheTime]]; NSDictionary *values = @{@"Expires": expireDate, @"Cache-Control": @"public"}; [modifiedHeaders addEntriesFromDictionary:values]; NSHTTPURLResponse *response = [[NSHTTPURLResponse alloc] initWithURL:httpResponse.URL statusCode:httpResponse.statusCode HTTPVersion:@"HTTP/1.1" headerFields:modifiedHeaders]; NSCachedURLResponse *modifiedCachedResponse = [[NSCachedURLResponse alloc] initWithResponse:response data:cachedResponse.data userInfo:cachedResponse.userInfo storagePolicy:NSURLCacheStorageAllowed]; return modifiedCachedResponse; } return cachedResponse;
HTTP- .
, ( , , ). , ( cacheTime ) .
— ( ). , , .
:
(, , ..); - NSURLCache , .
:
30 ; ; : 200- , NSURLConnection .
""
API (, ), fields :
. . . , .
, Player , JSON-, . , fields , NSDictionary . , - . , .
JSON -> NSObject Mantle , API ( RACSignal API ):
- (RACSignal *)searchPlayers:(NSString *)query limit:(NSUInteger)limit { NSMutableDictionary *parameters = [NSMutableDictionary dictionary]; parameters[@"search"] = query; if (limit) parameters[@"limit"] = @(limit); return [self getPath:@"account/list" parameters:parameters resultClass:WOTSearchPlayer.class]; }
, resultClass , ? :
- (RACSignal *)searchPlayers:(NSString *)query limit:(NSUInteger)limit resultClass:(Class)resultClass
, API, .
API , : , , "" . ( ). , ( AFJSONRequestOperation ):
op1 = [AFJSONRequestOperation JSONRequestOperationWithRequest:nil success:^(id JSON) { op2 = [AFJSONRequestOperation JSONRequestOperationWithRequest:nil success:^(id JSON) { op3 = [AFJSONRequestOperation JSONRequestOperationWithRequest:nil success:^(id JSON) { // Combine all the responses and return } failure:^(NSError *error) { // Handle error }]; } failure:^(NSError *error) { // Handle error }]; } failure:^(NSError *error) { // Handle error }];
, , . ReactiveCocoa , .
API :
- (RACSignal *)searchPlayers:(NSString *)query limit:(NSUInteger)limit; - (RACSignal *)fetchPlayers:(NSArray *)playerIDs;
RACSignal — , , . — , . / :
RACSignal *fetchPlayersAndRatings = [[[[API searchPlayers:@"" limit:0] flattenMap:^RACStream *(id players) { // skipping data processing return [API fetchPlayers:@[]]; }] flattenMap:^RACStream *(id fullPlayers) { // Skipping data processing return [API statsSliceForPlayer:nil hoursAgo:@[] fields:nil]; }] flattenMap:^RACStream *(id value) { // Compose all responses here id composedResponse; return [RACSignal return:composedResponse]; }]; [fetchPlayersAndRatings subscribeNext:^(id x) { // Fully composed response } error:^(NSError *error) { // All erros go to one place }];
ReactiveCocoa — , . , Promises Futures.
:
Core ( , ); WOT ( World of Tanks–); WOWP ( World of Warplanes–)
, , — . , ( ) CocoaPods , . subspecs , :
# Subspecs s.subspec 'Core' do |cs| cs.source_files = 'WGNAPIClient/Core/**/*.{h,m}' end s.subspec 'WOT' do |cs| cs.dependency 'WGNAPIClient/Core' cs.source_files = 'WGNAPIClient/WOT/**/*.{h,m}' end s.subspec 'WOWP' do |cs| cs.dependency 'WGNAPIClient/Core' cs.source_files = 'WGNAPIClient/WOWP/**/*.{h,m}' end
"" "" , :
pod 'WGNAPIClient/WOT' pod 'WGNAPIClient/WOWP'
. .
(98%). :
; .
:
context(@"API works with players data", ^{ it (@"should search players", ^{ stubResponse(@"/wot/account/list", @"players-search.json"); RACSignal *searchRequest = [client searchPlayers:@"garnett" limit:0]; NSArray *response = [searchRequest asynchronousFirstOrDefault:nil success:NULL error:&error]; [[error should] beNil]; [[response should] beKindOfClass:NSArray.class]; [[response.lastObject should] beKindOfClass:WOTSearchPlayer.class]; WOTSearchPlayer *player = response[0]; [[player.ID should] equal:@"1785514"]; }); });
, / , RACSignal , :
(id)asynchronousFirstOrDefault:(id)defaultValue success:(BOOL *)success error:(NSError **)error;
describe(@"WOTRatingType", ^{ it(@"should parse json to model object", ^{ NSDictionary *json = @{ @"threshold": @5, @"type": @"1", @"rank_fields": @[ @"xp_amount_rank", @"xp_max_rank", @"battles_count_rank", @"spotted_count_rank", @"damage_dealt_rank", ] }; NSError *error; WOTRatingType *ratingType = [MTLJSONAdapter modelOfClass:WOTRatingType.class fromJSONDictionary:json error:&error]; [[error should] beNil]; [[ratingType should] beKindOfClass:WOTRatingType.class]; [[json[@"threshold"] should] equal:theValue(ratingType.threshold)]; * [[json[@"type"] should] equal:ratingType.type]; [[ratingType.rankFields shouldNot] beNil]; [[ratingType.rankFields should] beKindOfClass:NSArray.class]; [[ratingType.rankFields should] haveCountOf:5]; [[@"maximumXP" should] equal:ratingType.rankFields[1]]; }); });
* — - - Kiwi, , nil .
API :
; .
, , : query http-. 
, ReactiveCocoa , ( ). ( ) 2k LOC ( ~1k - , ~700 - ), : — .     . 
 
 Kiwi 
  BDD-    RSpec -  ,   .   «--». 
 
 OHTTPStubs 
   ,      web-   .      "",      - . 
 
     . 
 
    
 There are only two hard things in Computer Science: cache invalidation and naming things. 
 
 -- Phil Karlton 
  « »  : 
 
                .. 
 ,            . 
       -     inMemory CoreData ,       NSCache .   ,       ,         —         .         NSURLConnection . 
 
 NSURLCache 
 NSURLCache        NSURLRequest   .      ,    . 
 
   : 
 NSURLCache *urlCache = [[NSURLCache alloc] initWithMemoryCapacity:kInMemoryCacheSize diskCapacity:kOnDiskCacheSize diskPath:path]; [NSURLCache setSharedURLCache:urlCache]; 
     ,           . 
 
  NSURLConnectionDelegate   ,     ""    : 
 - (NSCachedURLResponse *)connection:(NSURLConnection *)connection willCacheResponse:(NSCachedURLResponse *)cachedResponse; 
    ? 
 
  NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse*)[cachedResponse response]; NSDictionary *headers = [httpResponse allHeaderFields]; NSString *cacheControl = [headers valueForKey:@"Cache-Control"]; NSString *expires = [headers valueForKey:@"Expires"]; if((cacheControl == nil) && (expires == nil)) { NSMutableDictionary *modifiedHeaders = [httpResponse.allHeaderFields mutableCopy]; NSString *expireDate = [NSDate RFC822StringFromDate:[[NSDate date] dateByAddingTimeInterval:cacheTime]]; NSDictionary *values = @{@"Expires": expireDate, @"Cache-Control": @"public"}; [modifiedHeaders addEntriesFromDictionary:values]; NSHTTPURLResponse *response = [[NSHTTPURLResponse alloc] initWithURL:httpResponse.URL statusCode:httpResponse.statusCode HTTPVersion:@"HTTP/1.1" headerFields:modifiedHeaders]; NSCachedURLResponse *modifiedCachedResponse = [[NSCachedURLResponse alloc] initWithResponse:response data:cachedResponse.data userInfo:cachedResponse.userInfo storagePolicy:NSURLCacheStorageAllowed]; return modifiedCachedResponse; } return cachedResponse; 
      HTTP-      . 
 
 ,     (    ,    ,            ).   ,     ( cacheTime )   . 
     —        (      ).    ,      ,    . 
 
 : 
 
        (, ,   ..);  -   NSURLCache ,   . 
 : 
     30  ;       ; :         200- , NSURLConnection   . 
  ""  
          API (,    ),     fields : 
 
   .   .    .    ,   . 
  ,   Player ,      JSON-,   .        ,       fields ,        NSDictionary .         ,  -       .          ,           . 
   JSON -> NSObject    Mantle ,     API      ( RACSignal   API      ): 
 
 - (RACSignal *)searchPlayers:(NSString *)query limit:(NSUInteger)limit { NSMutableDictionary *parameters = [NSMutableDictionary dictionary]; parameters[@"search"] = query; if (limit) parameters[@"limit"] = @(limit); return [self getPath:@"account/list" parameters:parameters resultClass:WOTSearchPlayer.class]; } 
  ,       resultClass ,         ? : 
 
  - (RACSignal *)searchPlayers:(NSString *)query limit:(NSUInteger)limit resultClass:(Class)resultClass 
 ,     API,           . 
 
       
     API     ,               : ,   ,     ""    . (    ).    ,       (   AFJSONRequestOperation ): 
 
  op1 = [AFJSONRequestOperation JSONRequestOperationWithRequest:nil success:^(id JSON) { op2 = [AFJSONRequestOperation JSONRequestOperationWithRequest:nil success:^(id JSON) { op3 = [AFJSONRequestOperation JSONRequestOperationWithRequest:nil success:^(id JSON) { // Combine all the responses and return } failure:^(NSError *error) { // Handle error }]; } failure:^(NSError *error) { // Handle error }]; } failure:^(NSError *error) { // Handle error }]; 
  ,    ,          .         ReactiveCocoa  ,       . 
  API    : 
 
 - (RACSignal *)searchPlayers:(NSString *)query limit:(NSUInteger)limit; - (RACSignal *)fetchPlayers:(NSArray *)playerIDs; 
 RACSignal —  ,       ,     .    —       ,    .          / : 
 
  RACSignal *fetchPlayersAndRatings = [[[[API searchPlayers:@"" limit:0] flattenMap:^RACStream *(id players) { // skipping data processing return [API fetchPlayers:@[]]; }] flattenMap:^RACStream *(id fullPlayers) { // Skipping data processing return [API statsSliceForPlayer:nil hoursAgo:@[] fields:nil]; }] flattenMap:^RACStream *(id value) { // Compose all responses here id composedResponse; return [RACSignal return:composedResponse]; }]; [fetchPlayersAndRatings subscribeNext:^(id x) { // Fully composed response } error:^(NSError *error) { // All erros go to one place }]; 
  ReactiveCocoa   —      ,      .        ,         Promises  Futures. 
 
      
        : 
 Core ( , ); WOT (    World of Tanks–); WOWP (    World of Warplanes–) 
  ,         ,      — . ,    (    )       CocoaPods ,       .       subspecs ,        : 
 
  # Subspecs s.subspec 'Core' do |cs| cs.source_files = 'WGNAPIClient/Core/**/*.{h,m}' end s.subspec 'WOT' do |cs| cs.dependency 'WGNAPIClient/Core' cs.source_files = 'WGNAPIClient/WOT/**/*.{h,m}' end s.subspec 'WOWP' do |cs| cs.dependency 'WGNAPIClient/Core' cs.source_files = 'WGNAPIClient/WOWP/**/*.{h,m}' end 
 
    ""  ""  ,      : 
 pod 'WGNAPIClient/WOT' pod 'WGNAPIClient/WOWP' 
     
              .        . 
         (98%).           : 
 
   ;    . 
   
     : 
  context(@"API works with players data", ^{ it (@"should search players", ^{ stubResponse(@"/wot/account/list", @"players-search.json"); RACSignal *searchRequest = [client searchPlayers:@"garnett" limit:0]; NSArray *response = [searchRequest asynchronousFirstOrDefault:nil success:NULL error:&error]; [[error should] beNil]; [[response should] beKindOfClass:NSArray.class]; [[response.lastObject should] beKindOfClass:WOTSearchPlayer.class]; WOTSearchPlayer *player = response[0]; [[player.ID should] equal:@"1785514"]; }); }); 
 ,     / ,    RACSignal      ,       : 
 (id)asynchronousFirstOrDefault:(id)defaultValue success:(BOOL *)success error:(NSError **)error; 
   
 describe(@"WOTRatingType", ^{ it(@"should parse json to model object", ^{ NSDictionary *json = @{ @"threshold": @5, @"type": @"1", @"rank_fields": @[ @"xp_amount_rank", @"xp_max_rank", @"battles_count_rank", @"spotted_count_rank", @"damage_dealt_rank", ] }; NSError *error; WOTRatingType *ratingType = [MTLJSONAdapter modelOfClass:WOTRatingType.class fromJSONDictionary:json error:&error]; [[error should] beNil]; [[ratingType should] beKindOfClass:WOTRatingType.class]; [[json[@"threshold"] should] equal:theValue(ratingType.threshold)]; * [[json[@"type"] should] equal:ratingType.type]; [[ratingType.rankFields shouldNot] beNil]; [[ratingType.rankFields should] beKindOfClass:NSArray.class]; [[ratingType.rankFields should] haveCountOf:5]; [[@"maximumXP" should] equal:ratingType.rankFields[1]]; }); }); 
 
 * —    - -    Kiwi,      ,     nil . 
 
       API    : 
    ;   . 
  ,  ,       :  query  http-.   
 
  
    ,      ReactiveCocoa ,         (    ).    ( )   2k LOC (  ~1k -  ,   ~700 -     ),     :        —         . .
Kiwi
BDD- RSpec - , . «--».
OHTTPStubs
, web- . "", - .
.
There are only two hard things in Computer Science: cache invalidation and naming things.
-- Phil Karlton
« » :
..
, .
- inMemory CoreData , NSCache . , , — . NSURLConnection .
NSURLCache
NSURLCache NSURLRequest . , .
:
NSURLCache *urlCache = [[NSURLCache alloc] initWithMemoryCapacity:kInMemoryCacheSize diskCapacity:kOnDiskCacheSize diskPath:path]; [NSURLCache setSharedURLCache:urlCache];
, .
NSURLConnectionDelegate , "" :
- (NSCachedURLResponse *)connection:(NSURLConnection *)connection willCacheResponse:(NSCachedURLResponse *)cachedResponse;
?
NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse*)[cachedResponse response]; NSDictionary *headers = [httpResponse allHeaderFields]; NSString *cacheControl = [headers valueForKey:@"Cache-Control"]; NSString *expires = [headers valueForKey:@"Expires"]; if((cacheControl == nil) && (expires == nil)) { NSMutableDictionary *modifiedHeaders = [httpResponse.allHeaderFields mutableCopy]; NSString *expireDate = [NSDate RFC822StringFromDate:[[NSDate date] dateByAddingTimeInterval:cacheTime]]; NSDictionary *values = @{@"Expires": expireDate, @"Cache-Control": @"public"}; [modifiedHeaders addEntriesFromDictionary:values]; NSHTTPURLResponse *response = [[NSHTTPURLResponse alloc] initWithURL:httpResponse.URL statusCode:httpResponse.statusCode HTTPVersion:@"HTTP/1.1" headerFields:modifiedHeaders]; NSCachedURLResponse *modifiedCachedResponse = [[NSCachedURLResponse alloc] initWithResponse:response data:cachedResponse.data userInfo:cachedResponse.userInfo storagePolicy:NSURLCacheStorageAllowed]; return modifiedCachedResponse; } return cachedResponse;
HTTP- .
, ( , , ). , ( cacheTime ) .
— ( ). , , .
:
(, , ..); - NSURLCache , .
:
30 ; ; : 200- , NSURLConnection .
""
API (, ), fields :
. . . , .
, Player , JSON-, . , fields , NSDictionary . , - . , .
JSON -> NSObject Mantle , API ( RACSignal API ):
- (RACSignal *)searchPlayers:(NSString *)query limit:(NSUInteger)limit { NSMutableDictionary *parameters = [NSMutableDictionary dictionary]; parameters[@"search"] = query; if (limit) parameters[@"limit"] = @(limit); return [self getPath:@"account/list" parameters:parameters resultClass:WOTSearchPlayer.class]; }
, resultClass , ? :
- (RACSignal *)searchPlayers:(NSString *)query limit:(NSUInteger)limit resultClass:(Class)resultClass
, API, .
API , : , , "" . ( ). , ( AFJSONRequestOperation ):
op1 = [AFJSONRequestOperation JSONRequestOperationWithRequest:nil success:^(id JSON) { op2 = [AFJSONRequestOperation JSONRequestOperationWithRequest:nil success:^(id JSON) { op3 = [AFJSONRequestOperation JSONRequestOperationWithRequest:nil success:^(id JSON) { // Combine all the responses and return } failure:^(NSError *error) { // Handle error }]; } failure:^(NSError *error) { // Handle error }]; } failure:^(NSError *error) { // Handle error }];
, , . ReactiveCocoa , .
API :
- (RACSignal *)searchPlayers:(NSString *)query limit:(NSUInteger)limit; - (RACSignal *)fetchPlayers:(NSArray *)playerIDs;
RACSignal — , , . — , . / :
RACSignal *fetchPlayersAndRatings = [[[[API searchPlayers:@"" limit:0] flattenMap:^RACStream *(id players) { // skipping data processing return [API fetchPlayers:@[]]; }] flattenMap:^RACStream *(id fullPlayers) { // Skipping data processing return [API statsSliceForPlayer:nil hoursAgo:@[] fields:nil]; }] flattenMap:^RACStream *(id value) { // Compose all responses here id composedResponse; return [RACSignal return:composedResponse]; }]; [fetchPlayersAndRatings subscribeNext:^(id x) { // Fully composed response } error:^(NSError *error) { // All erros go to one place }];
ReactiveCocoa — , . , Promises Futures.
:
Core ( , ); WOT ( World of Tanks–); WOWP ( World of Warplanes–)
, , — . , ( ) CocoaPods , . subspecs , :
# Subspecs s.subspec 'Core' do |cs| cs.source_files = 'WGNAPIClient/Core/**/*.{h,m}' end s.subspec 'WOT' do |cs| cs.dependency 'WGNAPIClient/Core' cs.source_files = 'WGNAPIClient/WOT/**/*.{h,m}' end s.subspec 'WOWP' do |cs| cs.dependency 'WGNAPIClient/Core' cs.source_files = 'WGNAPIClient/WOWP/**/*.{h,m}' end
"" "" , :
pod 'WGNAPIClient/WOT' pod 'WGNAPIClient/WOWP'
. .
(98%). :
; .
:
context(@"API works with players data", ^{ it (@"should search players", ^{ stubResponse(@"/wot/account/list", @"players-search.json"); RACSignal *searchRequest = [client searchPlayers:@"garnett" limit:0]; NSArray *response = [searchRequest asynchronousFirstOrDefault:nil success:NULL error:&error]; [[error should] beNil]; [[response should] beKindOfClass:NSArray.class]; [[response.lastObject should] beKindOfClass:WOTSearchPlayer.class]; WOTSearchPlayer *player = response[0]; [[player.ID should] equal:@"1785514"]; }); });
, / , RACSignal , :
(id)asynchronousFirstOrDefault:(id)defaultValue success:(BOOL *)success error:(NSError **)error;
describe(@"WOTRatingType", ^{ it(@"should parse json to model object", ^{ NSDictionary *json = @{ @"threshold": @5, @"type": @"1", @"rank_fields": @[ @"xp_amount_rank", @"xp_max_rank", @"battles_count_rank", @"spotted_count_rank", @"damage_dealt_rank", ] }; NSError *error; WOTRatingType *ratingType = [MTLJSONAdapter modelOfClass:WOTRatingType.class fromJSONDictionary:json error:&error]; [[error should] beNil]; [[ratingType should] beKindOfClass:WOTRatingType.class]; [[json[@"threshold"] should] equal:theValue(ratingType.threshold)]; * [[json[@"type"] should] equal:ratingType.type]; [[ratingType.rankFields shouldNot] beNil]; [[ratingType.rankFields should] beKindOfClass:NSArray.class]; [[ratingType.rankFields should] haveCountOf:5]; [[@"maximumXP" should] equal:ratingType.rankFields[1]]; }); });
* — - - Kiwi, , nil .
API :
; .
, , : query http-. 
, ReactiveCocoa , ( ). ( ) 2k LOC ( ~1k - , ~700 - ), : — .     . 
 
 Kiwi 
  BDD-    RSpec -  ,   .   «--». 
 
 OHTTPStubs 
   ,      web-   .      "",      - . 
 
     . 
 
    
 There are only two hard things in Computer Science: cache invalidation and naming things. 
 
 -- Phil Karlton 
  « »  : 
 
                .. 
 ,            . 
       -     inMemory CoreData ,       NSCache .   ,       ,         —         .         NSURLConnection . 
 
 NSURLCache 
 NSURLCache        NSURLRequest   .      ,    . 
 
   : 
 NSURLCache *urlCache = [[NSURLCache alloc] initWithMemoryCapacity:kInMemoryCacheSize diskCapacity:kOnDiskCacheSize diskPath:path]; [NSURLCache setSharedURLCache:urlCache]; 
     ,           . 
 
  NSURLConnectionDelegate   ,     ""    : 
 - (NSCachedURLResponse *)connection:(NSURLConnection *)connection willCacheResponse:(NSCachedURLResponse *)cachedResponse; 
    ? 
 
  NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse*)[cachedResponse response]; NSDictionary *headers = [httpResponse allHeaderFields]; NSString *cacheControl = [headers valueForKey:@"Cache-Control"]; NSString *expires = [headers valueForKey:@"Expires"]; if((cacheControl == nil) && (expires == nil)) { NSMutableDictionary *modifiedHeaders = [httpResponse.allHeaderFields mutableCopy]; NSString *expireDate = [NSDate RFC822StringFromDate:[[NSDate date] dateByAddingTimeInterval:cacheTime]]; NSDictionary *values = @{@"Expires": expireDate, @"Cache-Control": @"public"}; [modifiedHeaders addEntriesFromDictionary:values]; NSHTTPURLResponse *response = [[NSHTTPURLResponse alloc] initWithURL:httpResponse.URL statusCode:httpResponse.statusCode HTTPVersion:@"HTTP/1.1" headerFields:modifiedHeaders]; NSCachedURLResponse *modifiedCachedResponse = [[NSCachedURLResponse alloc] initWithResponse:response data:cachedResponse.data userInfo:cachedResponse.userInfo storagePolicy:NSURLCacheStorageAllowed]; return modifiedCachedResponse; } return cachedResponse; 
      HTTP-      . 
 
 ,     (    ,    ,            ).   ,     ( cacheTime )   . 
     —        (      ).    ,      ,    . 
 
 : 
 
        (, ,   ..);  -   NSURLCache ,   . 
 : 
     30  ;       ; :         200- , NSURLConnection   . 
  ""  
          API (,    ),     fields : 
 
   .   .    .    ,   . 
  ,   Player ,      JSON-,   .        ,       fields ,        NSDictionary .         ,  -       .          ,           . 
   JSON -> NSObject    Mantle ,     API      ( RACSignal   API      ): 
 
 - (RACSignal *)searchPlayers:(NSString *)query limit:(NSUInteger)limit { NSMutableDictionary *parameters = [NSMutableDictionary dictionary]; parameters[@"search"] = query; if (limit) parameters[@"limit"] = @(limit); return [self getPath:@"account/list" parameters:parameters resultClass:WOTSearchPlayer.class]; } 
  ,       resultClass ,         ? : 
 
  - (RACSignal *)searchPlayers:(NSString *)query limit:(NSUInteger)limit resultClass:(Class)resultClass 
 ,     API,           . 
 
       
     API     ,               : ,   ,     ""    . (    ).    ,       (   AFJSONRequestOperation ): 
 
  op1 = [AFJSONRequestOperation JSONRequestOperationWithRequest:nil success:^(id JSON) { op2 = [AFJSONRequestOperation JSONRequestOperationWithRequest:nil success:^(id JSON) { op3 = [AFJSONRequestOperation JSONRequestOperationWithRequest:nil success:^(id JSON) { // Combine all the responses and return } failure:^(NSError *error) { // Handle error }]; } failure:^(NSError *error) { // Handle error }]; } failure:^(NSError *error) { // Handle error }]; 
  ,    ,          .         ReactiveCocoa  ,       . 
  API    : 
 
 - (RACSignal *)searchPlayers:(NSString *)query limit:(NSUInteger)limit; - (RACSignal *)fetchPlayers:(NSArray *)playerIDs; 
 RACSignal —  ,       ,     .    —       ,    .          / : 
 
  RACSignal *fetchPlayersAndRatings = [[[[API searchPlayers:@"" limit:0] flattenMap:^RACStream *(id players) { // skipping data processing return [API fetchPlayers:@[]]; }] flattenMap:^RACStream *(id fullPlayers) { // Skipping data processing return [API statsSliceForPlayer:nil hoursAgo:@[] fields:nil]; }] flattenMap:^RACStream *(id value) { // Compose all responses here id composedResponse; return [RACSignal return:composedResponse]; }]; [fetchPlayersAndRatings subscribeNext:^(id x) { // Fully composed response } error:^(NSError *error) { // All erros go to one place }]; 
  ReactiveCocoa   —      ,      .        ,         Promises  Futures. 
 
      
        : 
 Core ( , ); WOT (    World of Tanks–); WOWP (    World of Warplanes–) 
  ,         ,      — . ,    (    )       CocoaPods ,       .       subspecs ,        : 
 
  # Subspecs s.subspec 'Core' do |cs| cs.source_files = 'WGNAPIClient/Core/**/*.{h,m}' end s.subspec 'WOT' do |cs| cs.dependency 'WGNAPIClient/Core' cs.source_files = 'WGNAPIClient/WOT/**/*.{h,m}' end s.subspec 'WOWP' do |cs| cs.dependency 'WGNAPIClient/Core' cs.source_files = 'WGNAPIClient/WOWP/**/*.{h,m}' end 
 
    ""  ""  ,      : 
 pod 'WGNAPIClient/WOT' pod 'WGNAPIClient/WOWP' 
     
              .        . 
         (98%).           : 
 
   ;    . 
   
     : 
  context(@"API works with players data", ^{ it (@"should search players", ^{ stubResponse(@"/wot/account/list", @"players-search.json"); RACSignal *searchRequest = [client searchPlayers:@"garnett" limit:0]; NSArray *response = [searchRequest asynchronousFirstOrDefault:nil success:NULL error:&error]; [[error should] beNil]; [[response should] beKindOfClass:NSArray.class]; [[response.lastObject should] beKindOfClass:WOTSearchPlayer.class]; WOTSearchPlayer *player = response[0]; [[player.ID should] equal:@"1785514"]; }); }); 
 ,     / ,    RACSignal      ,       : 
 (id)asynchronousFirstOrDefault:(id)defaultValue success:(BOOL *)success error:(NSError **)error; 
   
 describe(@"WOTRatingType", ^{ it(@"should parse json to model object", ^{ NSDictionary *json = @{ @"threshold": @5, @"type": @"1", @"rank_fields": @[ @"xp_amount_rank", @"xp_max_rank", @"battles_count_rank", @"spotted_count_rank", @"damage_dealt_rank", ] }; NSError *error; WOTRatingType *ratingType = [MTLJSONAdapter modelOfClass:WOTRatingType.class fromJSONDictionary:json error:&error]; [[error should] beNil]; [[ratingType should] beKindOfClass:WOTRatingType.class]; [[json[@"threshold"] should] equal:theValue(ratingType.threshold)]; * [[json[@"type"] should] equal:ratingType.type]; [[ratingType.rankFields shouldNot] beNil]; [[ratingType.rankFields should] beKindOfClass:NSArray.class]; [[ratingType.rankFields should] haveCountOf:5]; [[@"maximumXP" should] equal:ratingType.rankFields[1]]; }); }); 
 
 * —    - -    Kiwi,      ,     nil . 
 
       API    : 
    ;   . 
  ,  ,       :  query  http-.   
 
  
    ,      ReactiveCocoa ,         (    ).    ( )   2k LOC (  ~1k -  ,   ~700 -     ),     :        —         ..
Kiwi
BDD- RSpec - , . «--».
OHTTPStubs
, web- . "", - .
.
There are only two hard things in Computer Science: cache invalidation and naming things.
-- Phil Karlton
« » :
..
, .
- inMemory CoreData , NSCache . , , — . NSURLConnection .
NSURLCache
NSURLCache NSURLRequest . , .
:
NSURLCache *urlCache = [[NSURLCache alloc] initWithMemoryCapacity:kInMemoryCacheSize diskCapacity:kOnDiskCacheSize diskPath:path]; [NSURLCache setSharedURLCache:urlCache];
, .
NSURLConnectionDelegate , "" :
- (NSCachedURLResponse *)connection:(NSURLConnection *)connection willCacheResponse:(NSCachedURLResponse *)cachedResponse;
?
NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse*)[cachedResponse response]; NSDictionary *headers = [httpResponse allHeaderFields]; NSString *cacheControl = [headers valueForKey:@"Cache-Control"]; NSString *expires = [headers valueForKey:@"Expires"]; if((cacheControl == nil) && (expires == nil)) { NSMutableDictionary *modifiedHeaders = [httpResponse.allHeaderFields mutableCopy]; NSString *expireDate = [NSDate RFC822StringFromDate:[[NSDate date] dateByAddingTimeInterval:cacheTime]]; NSDictionary *values = @{@"Expires": expireDate, @"Cache-Control": @"public"}; [modifiedHeaders addEntriesFromDictionary:values]; NSHTTPURLResponse *response = [[NSHTTPURLResponse alloc] initWithURL:httpResponse.URL statusCode:httpResponse.statusCode HTTPVersion:@"HTTP/1.1" headerFields:modifiedHeaders]; NSCachedURLResponse *modifiedCachedResponse = [[NSCachedURLResponse alloc] initWithResponse:response data:cachedResponse.data userInfo:cachedResponse.userInfo storagePolicy:NSURLCacheStorageAllowed]; return modifiedCachedResponse; } return cachedResponse;
HTTP- .
, ( , , ). , ( cacheTime ) .
— ( ). , , .
:
(, , ..); - NSURLCache , .
:
30 ; ; : 200- , NSURLConnection .
""
API (, ), fields :
. . . , .
, Player , JSON-, . , fields , NSDictionary . , - . , .
JSON -> NSObject Mantle , API ( RACSignal API ):
- (RACSignal *)searchPlayers:(NSString *)query limit:(NSUInteger)limit { NSMutableDictionary *parameters = [NSMutableDictionary dictionary]; parameters[@"search"] = query; if (limit) parameters[@"limit"] = @(limit); return [self getPath:@"account/list" parameters:parameters resultClass:WOTSearchPlayer.class]; }
, resultClass , ? :
- (RACSignal *)searchPlayers:(NSString *)query limit:(NSUInteger)limit resultClass:(Class)resultClass
, API, .
API , : , , "" . ( ). , ( AFJSONRequestOperation ):
op1 = [AFJSONRequestOperation JSONRequestOperationWithRequest:nil success:^(id JSON) { op2 = [AFJSONRequestOperation JSONRequestOperationWithRequest:nil success:^(id JSON) { op3 = [AFJSONRequestOperation JSONRequestOperationWithRequest:nil success:^(id JSON) { // Combine all the responses and return } failure:^(NSError *error) { // Handle error }]; } failure:^(NSError *error) { // Handle error }]; } failure:^(NSError *error) { // Handle error }];
, , . ReactiveCocoa , .
API :
- (RACSignal *)searchPlayers:(NSString *)query limit:(NSUInteger)limit; - (RACSignal *)fetchPlayers:(NSArray *)playerIDs;
RACSignal — , , . — , . / :
RACSignal *fetchPlayersAndRatings = [[[[API searchPlayers:@"" limit:0] flattenMap:^RACStream *(id players) { // skipping data processing return [API fetchPlayers:@[]]; }] flattenMap:^RACStream *(id fullPlayers) { // Skipping data processing return [API statsSliceForPlayer:nil hoursAgo:@[] fields:nil]; }] flattenMap:^RACStream *(id value) { // Compose all responses here id composedResponse; return [RACSignal return:composedResponse]; }]; [fetchPlayersAndRatings subscribeNext:^(id x) { // Fully composed response } error:^(NSError *error) { // All erros go to one place }];
ReactiveCocoa — , . , Promises Futures.
:
Core ( , ); WOT ( World of Tanks–); WOWP ( World of Warplanes–)
, , — . , ( ) CocoaPods , . subspecs , :
# Subspecs s.subspec 'Core' do |cs| cs.source_files = 'WGNAPIClient/Core/**/*.{h,m}' end s.subspec 'WOT' do |cs| cs.dependency 'WGNAPIClient/Core' cs.source_files = 'WGNAPIClient/WOT/**/*.{h,m}' end s.subspec 'WOWP' do |cs| cs.dependency 'WGNAPIClient/Core' cs.source_files = 'WGNAPIClient/WOWP/**/*.{h,m}' end
"" "" , :
pod 'WGNAPIClient/WOT' pod 'WGNAPIClient/WOWP'
. .
(98%). :
; .
:
context(@"API works with players data", ^{ it (@"should search players", ^{ stubResponse(@"/wot/account/list", @"players-search.json"); RACSignal *searchRequest = [client searchPlayers:@"garnett" limit:0]; NSArray *response = [searchRequest asynchronousFirstOrDefault:nil success:NULL error:&error]; [[error should] beNil]; [[response should] beKindOfClass:NSArray.class]; [[response.lastObject should] beKindOfClass:WOTSearchPlayer.class]; WOTSearchPlayer *player = response[0]; [[player.ID should] equal:@"1785514"]; }); });
, / , RACSignal , :
(id)asynchronousFirstOrDefault:(id)defaultValue success:(BOOL *)success error:(NSError **)error;
describe(@"WOTRatingType", ^{ it(@"should parse json to model object", ^{ NSDictionary *json = @{ @"threshold": @5, @"type": @"1", @"rank_fields": @[ @"xp_amount_rank", @"xp_max_rank", @"battles_count_rank", @"spotted_count_rank", @"damage_dealt_rank", ] }; NSError *error; WOTRatingType *ratingType = [MTLJSONAdapter modelOfClass:WOTRatingType.class fromJSONDictionary:json error:&error]; [[error should] beNil]; [[ratingType should] beKindOfClass:WOTRatingType.class]; [[json[@"threshold"] should] equal:theValue(ratingType.threshold)]; * [[json[@"type"] should] equal:ratingType.type]; [[ratingType.rankFields shouldNot] beNil]; [[ratingType.rankFields should] beKindOfClass:NSArray.class]; [[ratingType.rankFields should] haveCountOf:5]; [[@"maximumXP" should] equal:ratingType.rankFields[1]]; }); });
* — - - Kiwi, , nil .
API :
; .
, , : query http-. 
, ReactiveCocoa , ( ). ( ) 2k LOC ( ~1k - , ~700 - ), : — .     . 
 
 Kiwi 
  BDD-    RSpec -  ,   .   «--». 
 
 OHTTPStubs 
   ,      web-   .      "",      - . 
 
     . 
 
    
 There are only two hard things in Computer Science: cache invalidation and naming things. 
 
 -- Phil Karlton 
  « »  : 
 
                .. 
 ,            . 
       -     inMemory CoreData ,       NSCache .   ,       ,         —         .         NSURLConnection . 
 
 NSURLCache 
 NSURLCache        NSURLRequest   .      ,    . 
 
   : 
 NSURLCache *urlCache = [[NSURLCache alloc] initWithMemoryCapacity:kInMemoryCacheSize diskCapacity:kOnDiskCacheSize diskPath:path]; [NSURLCache setSharedURLCache:urlCache]; 
     ,           . 
 
  NSURLConnectionDelegate   ,     ""    : 
 - (NSCachedURLResponse *)connection:(NSURLConnection *)connection willCacheResponse:(NSCachedURLResponse *)cachedResponse; 
    ? 
 
  NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse*)[cachedResponse response]; NSDictionary *headers = [httpResponse allHeaderFields]; NSString *cacheControl = [headers valueForKey:@"Cache-Control"]; NSString *expires = [headers valueForKey:@"Expires"]; if((cacheControl == nil) && (expires == nil)) { NSMutableDictionary *modifiedHeaders = [httpResponse.allHeaderFields mutableCopy]; NSString *expireDate = [NSDate RFC822StringFromDate:[[NSDate date] dateByAddingTimeInterval:cacheTime]]; NSDictionary *values = @{@"Expires": expireDate, @"Cache-Control": @"public"}; [modifiedHeaders addEntriesFromDictionary:values]; NSHTTPURLResponse *response = [[NSHTTPURLResponse alloc] initWithURL:httpResponse.URL statusCode:httpResponse.statusCode HTTPVersion:@"HTTP/1.1" headerFields:modifiedHeaders]; NSCachedURLResponse *modifiedCachedResponse = [[NSCachedURLResponse alloc] initWithResponse:response data:cachedResponse.data userInfo:cachedResponse.userInfo storagePolicy:NSURLCacheStorageAllowed]; return modifiedCachedResponse; } return cachedResponse; 
      HTTP-      . 
 
 ,     (    ,    ,            ).   ,     ( cacheTime )   . 
     —        (      ).    ,      ,    . 
 
 : 
 
        (, ,   ..);  -   NSURLCache ,   . 
 : 
     30  ;       ; :         200- , NSURLConnection   . 
  ""  
          API (,    ),     fields : 
 
   .   .    .    ,   . 
  ,   Player ,      JSON-,   .        ,       fields ,        NSDictionary .         ,  -       .          ,           . 
   JSON -> NSObject    Mantle ,     API      ( RACSignal   API      ): 
 
 - (RACSignal *)searchPlayers:(NSString *)query limit:(NSUInteger)limit { NSMutableDictionary *parameters = [NSMutableDictionary dictionary]; parameters[@"search"] = query; if (limit) parameters[@"limit"] = @(limit); return [self getPath:@"account/list" parameters:parameters resultClass:WOTSearchPlayer.class]; } 
  ,       resultClass ,         ? : 
 
  - (RACSignal *)searchPlayers:(NSString *)query limit:(NSUInteger)limit resultClass:(Class)resultClass 
 ,     API,           . 
 
       
     API     ,               : ,   ,     ""    . (    ).    ,       (   AFJSONRequestOperation ): 
 
  op1 = [AFJSONRequestOperation JSONRequestOperationWithRequest:nil success:^(id JSON) { op2 = [AFJSONRequestOperation JSONRequestOperationWithRequest:nil success:^(id JSON) { op3 = [AFJSONRequestOperation JSONRequestOperationWithRequest:nil success:^(id JSON) { // Combine all the responses and return } failure:^(NSError *error) { // Handle error }]; } failure:^(NSError *error) { // Handle error }]; } failure:^(NSError *error) { // Handle error }]; 
  ,    ,          .         ReactiveCocoa  ,       . 
  API    : 
 
 - (RACSignal *)searchPlayers:(NSString *)query limit:(NSUInteger)limit; - (RACSignal *)fetchPlayers:(NSArray *)playerIDs; 
 RACSignal —  ,       ,     .    —       ,    .          / : 
 
  RACSignal *fetchPlayersAndRatings = [[[[API searchPlayers:@"" limit:0] flattenMap:^RACStream *(id players) { // skipping data processing return [API fetchPlayers:@[]]; }] flattenMap:^RACStream *(id fullPlayers) { // Skipping data processing return [API statsSliceForPlayer:nil hoursAgo:@[] fields:nil]; }] flattenMap:^RACStream *(id value) { // Compose all responses here id composedResponse; return [RACSignal return:composedResponse]; }]; [fetchPlayersAndRatings subscribeNext:^(id x) { // Fully composed response } error:^(NSError *error) { // All erros go to one place }]; 
  ReactiveCocoa   —      ,      .        ,         Promises  Futures. 
 
      
        : 
 Core ( , ); WOT (    World of Tanks–); WOWP (    World of Warplanes–) 
  ,         ,      — . ,    (    )       CocoaPods ,       .       subspecs ,        : 
 
  # Subspecs s.subspec 'Core' do |cs| cs.source_files = 'WGNAPIClient/Core/**/*.{h,m}' end s.subspec 'WOT' do |cs| cs.dependency 'WGNAPIClient/Core' cs.source_files = 'WGNAPIClient/WOT/**/*.{h,m}' end s.subspec 'WOWP' do |cs| cs.dependency 'WGNAPIClient/Core' cs.source_files = 'WGNAPIClient/WOWP/**/*.{h,m}' end 
 
    ""  ""  ,      : 
 pod 'WGNAPIClient/WOT' pod 'WGNAPIClient/WOWP' 
     
              .        . 
         (98%).           : 
 
   ;    . 
   
     : 
  context(@"API works with players data", ^{ it (@"should search players", ^{ stubResponse(@"/wot/account/list", @"players-search.json"); RACSignal *searchRequest = [client searchPlayers:@"garnett" limit:0]; NSArray *response = [searchRequest asynchronousFirstOrDefault:nil success:NULL error:&error]; [[error should] beNil]; [[response should] beKindOfClass:NSArray.class]; [[response.lastObject should] beKindOfClass:WOTSearchPlayer.class]; WOTSearchPlayer *player = response[0]; [[player.ID should] equal:@"1785514"]; }); }); 
 ,     / ,    RACSignal      ,       : 
 (id)asynchronousFirstOrDefault:(id)defaultValue success:(BOOL *)success error:(NSError **)error; 
   
 describe(@"WOTRatingType", ^{ it(@"should parse json to model object", ^{ NSDictionary *json = @{ @"threshold": @5, @"type": @"1", @"rank_fields": @[ @"xp_amount_rank", @"xp_max_rank", @"battles_count_rank", @"spotted_count_rank", @"damage_dealt_rank", ] }; NSError *error; WOTRatingType *ratingType = [MTLJSONAdapter modelOfClass:WOTRatingType.class fromJSONDictionary:json error:&error]; [[error should] beNil]; [[ratingType should] beKindOfClass:WOTRatingType.class]; [[json[@"threshold"] should] equal:theValue(ratingType.threshold)]; * [[json[@"type"] should] equal:ratingType.type]; [[ratingType.rankFields shouldNot] beNil]; [[ratingType.rankFields should] beKindOfClass:NSArray.class]; [[ratingType.rankFields should] haveCountOf:5]; [[@"maximumXP" should] equal:ratingType.rankFields[1]]; }); }); 
 
 * —    - -    Kiwi,      ,     nil . 
 
       API    : 
    ;   . 
  ,  ,       :  query  http-.   
 
  
    ,      ReactiveCocoa ,         (    ).    ( )   2k LOC (  ~1k -  ,   ~700 -     ),     :        —         . .
Kiwi
BDD- RSpec - , . «--».
OHTTPStubs
, web- . "", - .
.
There are only two hard things in Computer Science: cache invalidation and naming things.
-- Phil Karlton
« » :
..
, .
- inMemory CoreData , NSCache . , , — . NSURLConnection .
NSURLCache
NSURLCache NSURLRequest . , .
:
NSURLCache *urlCache = [[NSURLCache alloc] initWithMemoryCapacity:kInMemoryCacheSize diskCapacity:kOnDiskCacheSize diskPath:path]; [NSURLCache setSharedURLCache:urlCache];
, .
NSURLConnectionDelegate , "" :
- (NSCachedURLResponse *)connection:(NSURLConnection *)connection willCacheResponse:(NSCachedURLResponse *)cachedResponse;
?
NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse*)[cachedResponse response]; NSDictionary *headers = [httpResponse allHeaderFields]; NSString *cacheControl = [headers valueForKey:@"Cache-Control"]; NSString *expires = [headers valueForKey:@"Expires"]; if((cacheControl == nil) && (expires == nil)) { NSMutableDictionary *modifiedHeaders = [httpResponse.allHeaderFields mutableCopy]; NSString *expireDate = [NSDate RFC822StringFromDate:[[NSDate date] dateByAddingTimeInterval:cacheTime]]; NSDictionary *values = @{@"Expires": expireDate, @"Cache-Control": @"public"}; [modifiedHeaders addEntriesFromDictionary:values]; NSHTTPURLResponse *response = [[NSHTTPURLResponse alloc] initWithURL:httpResponse.URL statusCode:httpResponse.statusCode HTTPVersion:@"HTTP/1.1" headerFields:modifiedHeaders]; NSCachedURLResponse *modifiedCachedResponse = [[NSCachedURLResponse alloc] initWithResponse:response data:cachedResponse.data userInfo:cachedResponse.userInfo storagePolicy:NSURLCacheStorageAllowed]; return modifiedCachedResponse; } return cachedResponse;
HTTP- .
, ( , , ). , ( cacheTime ) .
— ( ). , , .
:
(, , ..); - NSURLCache , .
:
30 ; ; : 200- , NSURLConnection .
""
API (, ), fields :
. . . , .
, Player , JSON-, . , fields , NSDictionary . , - . , .
JSON -> NSObject Mantle , API ( RACSignal API ):
- (RACSignal *)searchPlayers:(NSString *)query limit:(NSUInteger)limit { NSMutableDictionary *parameters = [NSMutableDictionary dictionary]; parameters[@"search"] = query; if (limit) parameters[@"limit"] = @(limit); return [self getPath:@"account/list" parameters:parameters resultClass:WOTSearchPlayer.class]; }
, resultClass , ? :
- (RACSignal *)searchPlayers:(NSString *)query limit:(NSUInteger)limit resultClass:(Class)resultClass
, API, .
API , : , , "" . ( ). , ( AFJSONRequestOperation ):
op1 = [AFJSONRequestOperation JSONRequestOperationWithRequest:nil success:^(id JSON) { op2 = [AFJSONRequestOperation JSONRequestOperationWithRequest:nil success:^(id JSON) { op3 = [AFJSONRequestOperation JSONRequestOperationWithRequest:nil success:^(id JSON) { // Combine all the responses and return } failure:^(NSError *error) { // Handle error }]; } failure:^(NSError *error) { // Handle error }]; } failure:^(NSError *error) { // Handle error }];
, , . ReactiveCocoa , .
API :
- (RACSignal *)searchPlayers:(NSString *)query limit:(NSUInteger)limit; - (RACSignal *)fetchPlayers:(NSArray *)playerIDs;
RACSignal — , , . — , . / :
RACSignal *fetchPlayersAndRatings = [[[[API searchPlayers:@"" limit:0] flattenMap:^RACStream *(id players) { // skipping data processing return [API fetchPlayers:@[]]; }] flattenMap:^RACStream *(id fullPlayers) { // Skipping data processing return [API statsSliceForPlayer:nil hoursAgo:@[] fields:nil]; }] flattenMap:^RACStream *(id value) { // Compose all responses here id composedResponse; return [RACSignal return:composedResponse]; }]; [fetchPlayersAndRatings subscribeNext:^(id x) { // Fully composed response } error:^(NSError *error) { // All erros go to one place }];
ReactiveCocoa — , . , Promises Futures.
:
Core ( , ); WOT ( World of Tanks–); WOWP ( World of Warplanes–)
, , — . , ( ) CocoaPods , . subspecs , :
# Subspecs s.subspec 'Core' do |cs| cs.source_files = 'WGNAPIClient/Core/**/*.{h,m}' end s.subspec 'WOT' do |cs| cs.dependency 'WGNAPIClient/Core' cs.source_files = 'WGNAPIClient/WOT/**/*.{h,m}' end s.subspec 'WOWP' do |cs| cs.dependency 'WGNAPIClient/Core' cs.source_files = 'WGNAPIClient/WOWP/**/*.{h,m}' end
"" "" , :
pod 'WGNAPIClient/WOT' pod 'WGNAPIClient/WOWP'
. .
(98%). :
; .
:
context(@"API works with players data", ^{ it (@"should search players", ^{ stubResponse(@"/wot/account/list", @"players-search.json"); RACSignal *searchRequest = [client searchPlayers:@"garnett" limit:0]; NSArray *response = [searchRequest asynchronousFirstOrDefault:nil success:NULL error:&error]; [[error should] beNil]; [[response should] beKindOfClass:NSArray.class]; [[response.lastObject should] beKindOfClass:WOTSearchPlayer.class]; WOTSearchPlayer *player = response[0]; [[player.ID should] equal:@"1785514"]; }); });
, / , RACSignal , :
(id)asynchronousFirstOrDefault:(id)defaultValue success:(BOOL *)success error:(NSError **)error;
describe(@"WOTRatingType", ^{ it(@"should parse json to model object", ^{ NSDictionary *json = @{ @"threshold": @5, @"type": @"1", @"rank_fields": @[ @"xp_amount_rank", @"xp_max_rank", @"battles_count_rank", @"spotted_count_rank", @"damage_dealt_rank", ] }; NSError *error; WOTRatingType *ratingType = [MTLJSONAdapter modelOfClass:WOTRatingType.class fromJSONDictionary:json error:&error]; [[error should] beNil]; [[ratingType should] beKindOfClass:WOTRatingType.class]; [[json[@"threshold"] should] equal:theValue(ratingType.threshold)]; * [[json[@"type"] should] equal:ratingType.type]; [[ratingType.rankFields shouldNot] beNil]; [[ratingType.rankFields should] beKindOfClass:NSArray.class]; [[ratingType.rankFields should] haveCountOf:5]; [[@"maximumXP" should] equal:ratingType.rankFields[1]]; }); });
* — - - Kiwi, , nil .
API :
; .
, , : query http-. 
, ReactiveCocoa , ( ). ( ) 2k LOC ( ~1k - , ~700 - ), : — ..
Kiwi
BDD- RSpec - , . «--».
OHTTPStubs
, web- . "", - .
.
There are only two hard things in Computer Science: cache invalidation and naming things.
-- Phil Karlton
« » :
..
, .
- inMemory CoreData , NSCache . , , — . NSURLConnection .
NSURLCache
NSURLCache NSURLRequest . , .
:
NSURLCache *urlCache = [[NSURLCache alloc] initWithMemoryCapacity:kInMemoryCacheSize diskCapacity:kOnDiskCacheSize diskPath:path]; [NSURLCache setSharedURLCache:urlCache];
, .
NSURLConnectionDelegate , "" :
- (NSCachedURLResponse *)connection:(NSURLConnection *)connection willCacheResponse:(NSCachedURLResponse *)cachedResponse;
?
NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse*)[cachedResponse response]; NSDictionary *headers = [httpResponse allHeaderFields]; NSString *cacheControl = [headers valueForKey:@"Cache-Control"]; NSString *expires = [headers valueForKey:@"Expires"]; if((cacheControl == nil) && (expires == nil)) { NSMutableDictionary *modifiedHeaders = [httpResponse.allHeaderFields mutableCopy]; NSString *expireDate = [NSDate RFC822StringFromDate:[[NSDate date] dateByAddingTimeInterval:cacheTime]]; NSDictionary *values = @{@"Expires": expireDate, @"Cache-Control": @"public"}; [modifiedHeaders addEntriesFromDictionary:values]; NSHTTPURLResponse *response = [[NSHTTPURLResponse alloc] initWithURL:httpResponse.URL statusCode:httpResponse.statusCode HTTPVersion:@"HTTP/1.1" headerFields:modifiedHeaders]; NSCachedURLResponse *modifiedCachedResponse = [[NSCachedURLResponse alloc] initWithResponse:response data:cachedResponse.data userInfo:cachedResponse.userInfo storagePolicy:NSURLCacheStorageAllowed]; return modifiedCachedResponse; } return cachedResponse;
HTTP- .
, ( , , ). , ( cacheTime ) .
— ( ). , , .
:
(, , ..); - NSURLCache , .
:
30 ; ; : 200- , NSURLConnection .
""
API (, ), fields :
. . . , .
, Player , JSON-, . , fields , NSDictionary . , - . , .
JSON -> NSObject Mantle , API ( RACSignal API ):
- (RACSignal *)searchPlayers:(NSString *)query limit:(NSUInteger)limit { NSMutableDictionary *parameters = [NSMutableDictionary dictionary]; parameters[@"search"] = query; if (limit) parameters[@"limit"] = @(limit); return [self getPath:@"account/list" parameters:parameters resultClass:WOTSearchPlayer.class]; }
, resultClass , ? :
- (RACSignal *)searchPlayers:(NSString *)query limit:(NSUInteger)limit resultClass:(Class)resultClass
, API, .
API , : , , "" . ( ). , ( AFJSONRequestOperation ):
op1 = [AFJSONRequestOperation JSONRequestOperationWithRequest:nil success:^(id JSON) { op2 = [AFJSONRequestOperation JSONRequestOperationWithRequest:nil success:^(id JSON) { op3 = [AFJSONRequestOperation JSONRequestOperationWithRequest:nil success:^(id JSON) { // Combine all the responses and return } failure:^(NSError *error) { // Handle error }]; } failure:^(NSError *error) { // Handle error }]; } failure:^(NSError *error) { // Handle error }];
, , . ReactiveCocoa , .
API :
- (RACSignal *)searchPlayers:(NSString *)query limit:(NSUInteger)limit; - (RACSignal *)fetchPlayers:(NSArray *)playerIDs;
RACSignal — , , . — , . / :
RACSignal *fetchPlayersAndRatings = [[[[API searchPlayers:@"" limit:0] flattenMap:^RACStream *(id players) { // skipping data processing return [API fetchPlayers:@[]]; }] flattenMap:^RACStream *(id fullPlayers) { // Skipping data processing return [API statsSliceForPlayer:nil hoursAgo:@[] fields:nil]; }] flattenMap:^RACStream *(id value) { // Compose all responses here id composedResponse; return [RACSignal return:composedResponse]; }]; [fetchPlayersAndRatings subscribeNext:^(id x) { // Fully composed response } error:^(NSError *error) { // All erros go to one place }];
ReactiveCocoa — , . , Promises Futures.
:
Core ( , ); WOT ( World of Tanks–); WOWP ( World of Warplanes–)
, , — . , ( ) CocoaPods , . subspecs , :
# Subspecs s.subspec 'Core' do |cs| cs.source_files = 'WGNAPIClient/Core/**/*.{h,m}' end s.subspec 'WOT' do |cs| cs.dependency 'WGNAPIClient/Core' cs.source_files = 'WGNAPIClient/WOT/**/*.{h,m}' end s.subspec 'WOWP' do |cs| cs.dependency 'WGNAPIClient/Core' cs.source_files = 'WGNAPIClient/WOWP/**/*.{h,m}' end
"" "" , :
pod 'WGNAPIClient/WOT' pod 'WGNAPIClient/WOWP'
. .
(98%). :
; .
:
context(@"API works with players data", ^{ it (@"should search players", ^{ stubResponse(@"/wot/account/list", @"players-search.json"); RACSignal *searchRequest = [client searchPlayers:@"garnett" limit:0]; NSArray *response = [searchRequest asynchronousFirstOrDefault:nil success:NULL error:&error]; [[error should] beNil]; [[response should] beKindOfClass:NSArray.class]; [[response.lastObject should] beKindOfClass:WOTSearchPlayer.class]; WOTSearchPlayer *player = response[0]; [[player.ID should] equal:@"1785514"]; }); });
, / , RACSignal , :
(id)asynchronousFirstOrDefault:(id)defaultValue success:(BOOL *)success error:(NSError **)error;
describe(@"WOTRatingType", ^{ it(@"should parse json to model object", ^{ NSDictionary *json = @{ @"threshold": @5, @"type": @"1", @"rank_fields": @[ @"xp_amount_rank", @"xp_max_rank", @"battles_count_rank", @"spotted_count_rank", @"damage_dealt_rank", ] }; NSError *error; WOTRatingType *ratingType = [MTLJSONAdapter modelOfClass:WOTRatingType.class fromJSONDictionary:json error:&error]; [[error should] beNil]; [[ratingType should] beKindOfClass:WOTRatingType.class]; [[json[@"threshold"] should] equal:theValue(ratingType.threshold)]; * [[json[@"type"] should] equal:ratingType.type]; [[ratingType.rankFields shouldNot] beNil]; [[ratingType.rankFields should] beKindOfClass:NSArray.class]; [[ratingType.rankFields should] haveCountOf:5]; [[@"maximumXP" should] equal:ratingType.rankFields[1]]; }); });
* — - - Kiwi, , nil .
API :
; .
, , : query http-. 
, ReactiveCocoa , ( ). ( ) 2k LOC ( ~1k - , ~700 - ), : — ..
Kiwi
BDD- RSpec - , . «--».
OHTTPStubs
, web- . "", - .
.
There are only two hard things in Computer Science: cache invalidation and naming things.
-- Phil Karlton
« » :
..
, .
- inMemory CoreData , NSCache . , , — . NSURLConnection .
NSURLCache
NSURLCache NSURLRequest . , .
:
NSURLCache *urlCache = [[NSURLCache alloc] initWithMemoryCapacity:kInMemoryCacheSize diskCapacity:kOnDiskCacheSize diskPath:path]; [NSURLCache setSharedURLCache:urlCache];
, .
NSURLConnectionDelegate , "" :
- (NSCachedURLResponse *)connection:(NSURLConnection *)connection willCacheResponse:(NSCachedURLResponse *)cachedResponse;
?
NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse*)[cachedResponse response]; NSDictionary *headers = [httpResponse allHeaderFields]; NSString *cacheControl = [headers valueForKey:@"Cache-Control"]; NSString *expires = [headers valueForKey:@"Expires"]; if((cacheControl == nil) && (expires == nil)) { NSMutableDictionary *modifiedHeaders = [httpResponse.allHeaderFields mutableCopy]; NSString *expireDate = [NSDate RFC822StringFromDate:[[NSDate date] dateByAddingTimeInterval:cacheTime]]; NSDictionary *values = @{@"Expires": expireDate, @"Cache-Control": @"public"}; [modifiedHeaders addEntriesFromDictionary:values]; NSHTTPURLResponse *response = [[NSHTTPURLResponse alloc] initWithURL:httpResponse.URL statusCode:httpResponse.statusCode HTTPVersion:@"HTTP/1.1" headerFields:modifiedHeaders]; NSCachedURLResponse *modifiedCachedResponse = [[NSCachedURLResponse alloc] initWithResponse:response data:cachedResponse.data userInfo:cachedResponse.userInfo storagePolicy:NSURLCacheStorageAllowed]; return modifiedCachedResponse; } return cachedResponse;
HTTP- .
, ( , , ). , ( cacheTime ) .
— ( ). , , .
:
(, , ..); - NSURLCache , .
:
30 ; ; : 200- , NSURLConnection .
""
API (, ), fields :
. . . , .
, Player , JSON-, . , fields , NSDictionary . , - . , .
JSON -> NSObject Mantle , API ( RACSignal API ):
- (RACSignal *)searchPlayers:(NSString *)query limit:(NSUInteger)limit { NSMutableDictionary *parameters = [NSMutableDictionary dictionary]; parameters[@"search"] = query; if (limit) parameters[@"limit"] = @(limit); return [self getPath:@"account/list" parameters:parameters resultClass:WOTSearchPlayer.class]; }
, resultClass , ? :
- (RACSignal *)searchPlayers:(NSString *)query limit:(NSUInteger)limit resultClass:(Class)resultClass
, API, .
API , : , , "" . ( ). , ( AFJSONRequestOperation ):
op1 = [AFJSONRequestOperation JSONRequestOperationWithRequest:nil success:^(id JSON) { op2 = [AFJSONRequestOperation JSONRequestOperationWithRequest:nil success:^(id JSON) { op3 = [AFJSONRequestOperation JSONRequestOperationWithRequest:nil success:^(id JSON) { // Combine all the responses and return } failure:^(NSError *error) { // Handle error }]; } failure:^(NSError *error) { // Handle error }]; } failure:^(NSError *error) { // Handle error }];
, , . ReactiveCocoa , .
API :
- (RACSignal *)searchPlayers:(NSString *)query limit:(NSUInteger)limit; - (RACSignal *)fetchPlayers:(NSArray *)playerIDs;
RACSignal — , , . — , . / :
RACSignal *fetchPlayersAndRatings = [[[[API searchPlayers:@"" limit:0] flattenMap:^RACStream *(id players) { // skipping data processing return [API fetchPlayers:@[]]; }] flattenMap:^RACStream *(id fullPlayers) { // Skipping data processing return [API statsSliceForPlayer:nil hoursAgo:@[] fields:nil]; }] flattenMap:^RACStream *(id value) { // Compose all responses here id composedResponse; return [RACSignal return:composedResponse]; }]; [fetchPlayersAndRatings subscribeNext:^(id x) { // Fully composed response } error:^(NSError *error) { // All erros go to one place }];
ReactiveCocoa — , . , Promises Futures.
:
Core ( , ); WOT ( World of Tanks–); WOWP ( World of Warplanes–)
, , — . , ( ) CocoaPods , . subspecs , :
# Subspecs s.subspec 'Core' do |cs| cs.source_files = 'WGNAPIClient/Core/**/*.{h,m}' end s.subspec 'WOT' do |cs| cs.dependency 'WGNAPIClient/Core' cs.source_files = 'WGNAPIClient/WOT/**/*.{h,m}' end s.subspec 'WOWP' do |cs| cs.dependency 'WGNAPIClient/Core' cs.source_files = 'WGNAPIClient/WOWP/**/*.{h,m}' end
"" "" , :
pod 'WGNAPIClient/WOT' pod 'WGNAPIClient/WOWP'
. .
(98%). :
; .
:
context(@"API works with players data", ^{ it (@"should search players", ^{ stubResponse(@"/wot/account/list", @"players-search.json"); RACSignal *searchRequest = [client searchPlayers:@"garnett" limit:0]; NSArray *response = [searchRequest asynchronousFirstOrDefault:nil success:NULL error:&error]; [[error should] beNil]; [[response should] beKindOfClass:NSArray.class]; [[response.lastObject should] beKindOfClass:WOTSearchPlayer.class]; WOTSearchPlayer *player = response[0]; [[player.ID should] equal:@"1785514"]; }); });
, / , RACSignal , :
(id)asynchronousFirstOrDefault:(id)defaultValue success:(BOOL *)success error:(NSError **)error;
describe(@"WOTRatingType", ^{ it(@"should parse json to model object", ^{ NSDictionary *json = @{ @"threshold": @5, @"type": @"1", @"rank_fields": @[ @"xp_amount_rank", @"xp_max_rank", @"battles_count_rank", @"spotted_count_rank", @"damage_dealt_rank", ] }; NSError *error; WOTRatingType *ratingType = [MTLJSONAdapter modelOfClass:WOTRatingType.class fromJSONDictionary:json error:&error]; [[error should] beNil]; [[ratingType should] beKindOfClass:WOTRatingType.class]; [[json[@"threshold"] should] equal:theValue(ratingType.threshold)]; * [[json[@"type"] should] equal:ratingType.type]; [[ratingType.rankFields shouldNot] beNil]; [[ratingType.rankFields should] beKindOfClass:NSArray.class]; [[ratingType.rankFields should] haveCountOf:5]; [[@"maximumXP" should] equal:ratingType.rankFields[1]]; }); });
* — - - Kiwi, , nil .
API :
; .
, , : query http-. 
, ReactiveCocoa , ( ). ( ) 2k LOC ( ~1k - , ~700 - ), : — ..
Kiwi
BDD- RSpec - , . «--».
OHTTPStubs
, web- . "", - .
.
There are only two hard things in Computer Science: cache invalidation and naming things.
-- Phil Karlton
« » :
..
, .
- inMemory CoreData , NSCache . , , — . NSURLConnection .
NSURLCache
NSURLCache NSURLRequest . , .
:
NSURLCache *urlCache = [[NSURLCache alloc] initWithMemoryCapacity:kInMemoryCacheSize diskCapacity:kOnDiskCacheSize diskPath:path]; [NSURLCache setSharedURLCache:urlCache];
, .
NSURLConnectionDelegate , "" :
- (NSCachedURLResponse *)connection:(NSURLConnection *)connection willCacheResponse:(NSCachedURLResponse *)cachedResponse;
?
NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse*)[cachedResponse response]; NSDictionary *headers = [httpResponse allHeaderFields]; NSString *cacheControl = [headers valueForKey:@"Cache-Control"]; NSString *expires = [headers valueForKey:@"Expires"]; if((cacheControl == nil) && (expires == nil)) { NSMutableDictionary *modifiedHeaders = [httpResponse.allHeaderFields mutableCopy]; NSString *expireDate = [NSDate RFC822StringFromDate:[[NSDate date] dateByAddingTimeInterval:cacheTime]]; NSDictionary *values = @{@"Expires": expireDate, @"Cache-Control": @"public"}; [modifiedHeaders addEntriesFromDictionary:values]; NSHTTPURLResponse *response = [[NSHTTPURLResponse alloc] initWithURL:httpResponse.URL statusCode:httpResponse.statusCode HTTPVersion:@"HTTP/1.1" headerFields:modifiedHeaders]; NSCachedURLResponse *modifiedCachedResponse = [[NSCachedURLResponse alloc] initWithResponse:response data:cachedResponse.data userInfo:cachedResponse.userInfo storagePolicy:NSURLCacheStorageAllowed]; return modifiedCachedResponse; } return cachedResponse;
HTTP- .
, ( , , ). , ( cacheTime ) .
— ( ). , , .
:
(, , ..); - NSURLCache , .
:
30 ; ; : 200- , NSURLConnection .
""
API (, ), fields :
. . . , .
, Player , JSON-, . , fields , NSDictionary . , - . , .
JSON -> NSObject Mantle , API ( RACSignal API ):
- (RACSignal *)searchPlayers:(NSString *)query limit:(NSUInteger)limit { NSMutableDictionary *parameters = [NSMutableDictionary dictionary]; parameters[@"search"] = query; if (limit) parameters[@"limit"] = @(limit); return [self getPath:@"account/list" parameters:parameters resultClass:WOTSearchPlayer.class]; }
, resultClass , ? :
- (RACSignal *)searchPlayers:(NSString *)query limit:(NSUInteger)limit resultClass:(Class)resultClass
, API, .
API , : , , "" . ( ). , ( AFJSONRequestOperation ):
op1 = [AFJSONRequestOperation JSONRequestOperationWithRequest:nil success:^(id JSON) { op2 = [AFJSONRequestOperation JSONRequestOperationWithRequest:nil success:^(id JSON) { op3 = [AFJSONRequestOperation JSONRequestOperationWithRequest:nil success:^(id JSON) { // Combine all the responses and return } failure:^(NSError *error) { // Handle error }]; } failure:^(NSError *error) { // Handle error }]; } failure:^(NSError *error) { // Handle error }];
, , . ReactiveCocoa , .
API :
- (RACSignal *)searchPlayers:(NSString *)query limit:(NSUInteger)limit; - (RACSignal *)fetchPlayers:(NSArray *)playerIDs;
RACSignal — , , . — , . / :
RACSignal *fetchPlayersAndRatings = [[[[API searchPlayers:@"" limit:0] flattenMap:^RACStream *(id players) { // skipping data processing return [API fetchPlayers:@[]]; }] flattenMap:^RACStream *(id fullPlayers) { // Skipping data processing return [API statsSliceForPlayer:nil hoursAgo:@[] fields:nil]; }] flattenMap:^RACStream *(id value) { // Compose all responses here id composedResponse; return [RACSignal return:composedResponse]; }]; [fetchPlayersAndRatings subscribeNext:^(id x) { // Fully composed response } error:^(NSError *error) { // All erros go to one place }];
ReactiveCocoa — , . , Promises Futures.
:
Core ( , ); WOT ( World of Tanks–); WOWP ( World of Warplanes–)
, , — . , ( ) CocoaPods , . subspecs , :
# Subspecs s.subspec 'Core' do |cs| cs.source_files = 'WGNAPIClient/Core/**/*.{h,m}' end s.subspec 'WOT' do |cs| cs.dependency 'WGNAPIClient/Core' cs.source_files = 'WGNAPIClient/WOT/**/*.{h,m}' end s.subspec 'WOWP' do |cs| cs.dependency 'WGNAPIClient/Core' cs.source_files = 'WGNAPIClient/WOWP/**/*.{h,m}' end
"" "" , :
pod 'WGNAPIClient/WOT' pod 'WGNAPIClient/WOWP'
. .
(98%). :
; .
:
context(@"API works with players data", ^{ it (@"should search players", ^{ stubResponse(@"/wot/account/list", @"players-search.json"); RACSignal *searchRequest = [client searchPlayers:@"garnett" limit:0]; NSArray *response = [searchRequest asynchronousFirstOrDefault:nil success:NULL error:&error]; [[error should] beNil]; [[response should] beKindOfClass:NSArray.class]; [[response.lastObject should] beKindOfClass:WOTSearchPlayer.class]; WOTSearchPlayer *player = response[0]; [[player.ID should] equal:@"1785514"]; }); });
, / , RACSignal , :
(id)asynchronousFirstOrDefault:(id)defaultValue success:(BOOL *)success error:(NSError **)error;
describe(@"WOTRatingType", ^{ it(@"should parse json to model object", ^{ NSDictionary *json = @{ @"threshold": @5, @"type": @"1", @"rank_fields": @[ @"xp_amount_rank", @"xp_max_rank", @"battles_count_rank", @"spotted_count_rank", @"damage_dealt_rank", ] }; NSError *error; WOTRatingType *ratingType = [MTLJSONAdapter modelOfClass:WOTRatingType.class fromJSONDictionary:json error:&error]; [[error should] beNil]; [[ratingType should] beKindOfClass:WOTRatingType.class]; [[json[@"threshold"] should] equal:theValue(ratingType.threshold)]; * [[json[@"type"] should] equal:ratingType.type]; [[ratingType.rankFields shouldNot] beNil]; [[ratingType.rankFields should] beKindOfClass:NSArray.class]; [[ratingType.rankFields should] haveCountOf:5]; [[@"maximumXP" should] equal:ratingType.rankFields[1]]; }); });
* — - - Kiwi, , nil .
API :
; .
, , : query http-. 
, ReactiveCocoa , ( ). ( ) 2k LOC ( ~1k - , ~700 - ), : — ..
Kiwi
BDD- RSpec - , . «--».
OHTTPStubs
, web- . "", - .
.
There are only two hard things in Computer Science: cache invalidation and naming things.
-- Phil Karlton
« » :
..
, .
- inMemory CoreData , NSCache . , , — . NSURLConnection .
NSURLCache
NSURLCache NSURLRequest . , .
:
NSURLCache *urlCache = [[NSURLCache alloc] initWithMemoryCapacity:kInMemoryCacheSize diskCapacity:kOnDiskCacheSize diskPath:path]; [NSURLCache setSharedURLCache:urlCache];
, .
NSURLConnectionDelegate , "" :
- (NSCachedURLResponse *)connection:(NSURLConnection *)connection willCacheResponse:(NSCachedURLResponse *)cachedResponse;
?
NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse*)[cachedResponse response]; NSDictionary *headers = [httpResponse allHeaderFields]; NSString *cacheControl = [headers valueForKey:@"Cache-Control"]; NSString *expires = [headers valueForKey:@"Expires"]; if((cacheControl == nil) && (expires == nil)) { NSMutableDictionary *modifiedHeaders = [httpResponse.allHeaderFields mutableCopy]; NSString *expireDate = [NSDate RFC822StringFromDate:[[NSDate date] dateByAddingTimeInterval:cacheTime]]; NSDictionary *values = @{@"Expires": expireDate, @"Cache-Control": @"public"}; [modifiedHeaders addEntriesFromDictionary:values]; NSHTTPURLResponse *response = [[NSHTTPURLResponse alloc] initWithURL:httpResponse.URL statusCode:httpResponse.statusCode HTTPVersion:@"HTTP/1.1" headerFields:modifiedHeaders]; NSCachedURLResponse *modifiedCachedResponse = [[NSCachedURLResponse alloc] initWithResponse:response data:cachedResponse.data userInfo:cachedResponse.userInfo storagePolicy:NSURLCacheStorageAllowed]; return modifiedCachedResponse; } return cachedResponse;
HTTP- .
, ( , , ). , ( cacheTime ) .
— ( ). , , .
:
(, , ..); - NSURLCache , .
:
30 ; ; : 200- , NSURLConnection .
""
API (, ), fields :
. . . , .
, Player , JSON-, . , fields , NSDictionary . , - . , .
JSON -> NSObject Mantle , API ( RACSignal API ):
- (RACSignal *)searchPlayers:(NSString *)query limit:(NSUInteger)limit { NSMutableDictionary *parameters = [NSMutableDictionary dictionary]; parameters[@"search"] = query; if (limit) parameters[@"limit"] = @(limit); return [self getPath:@"account/list" parameters:parameters resultClass:WOTSearchPlayer.class]; }
, resultClass , ? :
- (RACSignal *)searchPlayers:(NSString *)query limit:(NSUInteger)limit resultClass:(Class)resultClass
, API, .
API , : , , "" . ( ). , ( AFJSONRequestOperation ):
op1 = [AFJSONRequestOperation JSONRequestOperationWithRequest:nil success:^(id JSON) { op2 = [AFJSONRequestOperation JSONRequestOperationWithRequest:nil success:^(id JSON) { op3 = [AFJSONRequestOperation JSONRequestOperationWithRequest:nil success:^(id JSON) { // Combine all the responses and return } failure:^(NSError *error) { // Handle error }]; } failure:^(NSError *error) { // Handle error }]; } failure:^(NSError *error) { // Handle error }];
, , . ReactiveCocoa , .
API :
- (RACSignal *)searchPlayers:(NSString *)query limit:(NSUInteger)limit; - (RACSignal *)fetchPlayers:(NSArray *)playerIDs;
RACSignal — , , . — , . / :
RACSignal *fetchPlayersAndRatings = [[[[API searchPlayers:@"" limit:0] flattenMap:^RACStream *(id players) { // skipping data processing return [API fetchPlayers:@[]]; }] flattenMap:^RACStream *(id fullPlayers) { // Skipping data processing return [API statsSliceForPlayer:nil hoursAgo:@[] fields:nil]; }] flattenMap:^RACStream *(id value) { // Compose all responses here id composedResponse; return [RACSignal return:composedResponse]; }]; [fetchPlayersAndRatings subscribeNext:^(id x) { // Fully composed response } error:^(NSError *error) { // All erros go to one place }];
ReactiveCocoa — , . , Promises Futures.
:
Core ( , ); WOT ( World of Tanks–); WOWP ( World of Warplanes–)
, , — . , ( ) CocoaPods , . subspecs , :
# Subspecs s.subspec 'Core' do |cs| cs.source_files = 'WGNAPIClient/Core/**/*.{h,m}' end s.subspec 'WOT' do |cs| cs.dependency 'WGNAPIClient/Core' cs.source_files = 'WGNAPIClient/WOT/**/*.{h,m}' end s.subspec 'WOWP' do |cs| cs.dependency 'WGNAPIClient/Core' cs.source_files = 'WGNAPIClient/WOWP/**/*.{h,m}' end
"" "" , :
pod 'WGNAPIClient/WOT' pod 'WGNAPIClient/WOWP'
. .
(98%). :
; .
:
context(@"API works with players data", ^{ it (@"should search players", ^{ stubResponse(@"/wot/account/list", @"players-search.json"); RACSignal *searchRequest = [client searchPlayers:@"garnett" limit:0]; NSArray *response = [searchRequest asynchronousFirstOrDefault:nil success:NULL error:&error]; [[error should] beNil]; [[response should] beKindOfClass:NSArray.class]; [[response.lastObject should] beKindOfClass:WOTSearchPlayer.class]; WOTSearchPlayer *player = response[0]; [[player.ID should] equal:@"1785514"]; }); });
, / , RACSignal , :
(id)asynchronousFirstOrDefault:(id)defaultValue success:(BOOL *)success error:(NSError **)error;
describe(@"WOTRatingType", ^{ it(@"should parse json to model object", ^{ NSDictionary *json = @{ @"threshold": @5, @"type": @"1", @"rank_fields": @[ @"xp_amount_rank", @"xp_max_rank", @"battles_count_rank", @"spotted_count_rank", @"damage_dealt_rank", ] }; NSError *error; WOTRatingType *ratingType = [MTLJSONAdapter modelOfClass:WOTRatingType.class fromJSONDictionary:json error:&error]; [[error should] beNil]; [[ratingType should] beKindOfClass:WOTRatingType.class]; [[json[@"threshold"] should] equal:theValue(ratingType.threshold)]; * [[json[@"type"] should] equal:ratingType.type]; [[ratingType.rankFields shouldNot] beNil]; [[ratingType.rankFields should] beKindOfClass:NSArray.class]; [[ratingType.rankFields should] haveCountOf:5]; [[@"maximumXP" should] equal:ratingType.rankFields[1]]; }); });
* — - - Kiwi, , nil .
API :
; .
, , : query http-. 
, ReactiveCocoa , ( ). ( ) 2k LOC ( ~1k - , ~700 - ), : — ..
Kiwi
BDD- RSpec - , . «--».
OHTTPStubs
, web- . "", - .
.
There are only two hard things in Computer Science: cache invalidation and naming things.
-- Phil Karlton
« » :
..
, .
- inMemory CoreData , NSCache . , , — . NSURLConnection .
NSURLCache
NSURLCache NSURLRequest . , .
:
NSURLCache *urlCache = [[NSURLCache alloc] initWithMemoryCapacity:kInMemoryCacheSize diskCapacity:kOnDiskCacheSize diskPath:path]; [NSURLCache setSharedURLCache:urlCache];
, .
NSURLConnectionDelegate , "" :
- (NSCachedURLResponse *)connection:(NSURLConnection *)connection willCacheResponse:(NSCachedURLResponse *)cachedResponse;
?
NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse*)[cachedResponse response]; NSDictionary *headers = [httpResponse allHeaderFields]; NSString *cacheControl = [headers valueForKey:@"Cache-Control"]; NSString *expires = [headers valueForKey:@"Expires"]; if((cacheControl == nil) && (expires == nil)) { NSMutableDictionary *modifiedHeaders = [httpResponse.allHeaderFields mutableCopy]; NSString *expireDate = [NSDate RFC822StringFromDate:[[NSDate date] dateByAddingTimeInterval:cacheTime]]; NSDictionary *values = @{@"Expires": expireDate, @"Cache-Control": @"public"}; [modifiedHeaders addEntriesFromDictionary:values]; NSHTTPURLResponse *response = [[NSHTTPURLResponse alloc] initWithURL:httpResponse.URL statusCode:httpResponse.statusCode HTTPVersion:@"HTTP/1.1" headerFields:modifiedHeaders]; NSCachedURLResponse *modifiedCachedResponse = [[NSCachedURLResponse alloc] initWithResponse:response data:cachedResponse.data userInfo:cachedResponse.userInfo storagePolicy:NSURLCacheStorageAllowed]; return modifiedCachedResponse; } return cachedResponse;
HTTP- .
, ( , , ). , ( cacheTime ) .
— ( ). , , .
:
(, , ..); - NSURLCache , .
:
30 ; ; : 200- , NSURLConnection .
""
API (, ), fields :
. . . , .
, Player , JSON-, . , fields , NSDictionary . , - . , .
JSON -> NSObject Mantle , API ( RACSignal API ):
- (RACSignal *)searchPlayers:(NSString *)query limit:(NSUInteger)limit { NSMutableDictionary *parameters = [NSMutableDictionary dictionary]; parameters[@"search"] = query; if (limit) parameters[@"limit"] = @(limit); return [self getPath:@"account/list" parameters:parameters resultClass:WOTSearchPlayer.class]; }
, resultClass , ? :
- (RACSignal *)searchPlayers:(NSString *)query limit:(NSUInteger)limit resultClass:(Class)resultClass
, API, .
API , : , , "" . ( ). , ( AFJSONRequestOperation ):
op1 = [AFJSONRequestOperation JSONRequestOperationWithRequest:nil success:^(id JSON) { op2 = [AFJSONRequestOperation JSONRequestOperationWithRequest:nil success:^(id JSON) { op3 = [AFJSONRequestOperation JSONRequestOperationWithRequest:nil success:^(id JSON) { // Combine all the responses and return } failure:^(NSError *error) { // Handle error }]; } failure:^(NSError *error) { // Handle error }]; } failure:^(NSError *error) { // Handle error }];
, , . ReactiveCocoa , .
API :
- (RACSignal *)searchPlayers:(NSString *)query limit:(NSUInteger)limit; - (RACSignal *)fetchPlayers:(NSArray *)playerIDs;
RACSignal — , , . — , . / :
RACSignal *fetchPlayersAndRatings = [[[[API searchPlayers:@"" limit:0] flattenMap:^RACStream *(id players) { // skipping data processing return [API fetchPlayers:@[]]; }] flattenMap:^RACStream *(id fullPlayers) { // Skipping data processing return [API statsSliceForPlayer:nil hoursAgo:@[] fields:nil]; }] flattenMap:^RACStream *(id value) { // Compose all responses here id composedResponse; return [RACSignal return:composedResponse]; }]; [fetchPlayersAndRatings subscribeNext:^(id x) { // Fully composed response } error:^(NSError *error) { // All erros go to one place }];
ReactiveCocoa — , . , Promises Futures.
:
Core ( , ); WOT ( World of Tanks–); WOWP ( World of Warplanes–)
, , — . , ( ) CocoaPods , . subspecs , :
# Subspecs s.subspec 'Core' do |cs| cs.source_files = 'WGNAPIClient/Core/**/*.{h,m}' end s.subspec 'WOT' do |cs| cs.dependency 'WGNAPIClient/Core' cs.source_files = 'WGNAPIClient/WOT/**/*.{h,m}' end s.subspec 'WOWP' do |cs| cs.dependency 'WGNAPIClient/Core' cs.source_files = 'WGNAPIClient/WOWP/**/*.{h,m}' end
"" "" , :
pod 'WGNAPIClient/WOT' pod 'WGNAPIClient/WOWP'
. .
(98%). :
; .
:
context(@"API works with players data", ^{ it (@"should search players", ^{ stubResponse(@"/wot/account/list", @"players-search.json"); RACSignal *searchRequest = [client searchPlayers:@"garnett" limit:0]; NSArray *response = [searchRequest asynchronousFirstOrDefault:nil success:NULL error:&error]; [[error should] beNil]; [[response should] beKindOfClass:NSArray.class]; [[response.lastObject should] beKindOfClass:WOTSearchPlayer.class]; WOTSearchPlayer *player = response[0]; [[player.ID should] equal:@"1785514"]; }); });
, / , RACSignal , :
(id)asynchronousFirstOrDefault:(id)defaultValue success:(BOOL *)success error:(NSError **)error;
describe(@"WOTRatingType", ^{ it(@"should parse json to model object", ^{ NSDictionary *json = @{ @"threshold": @5, @"type": @"1", @"rank_fields": @[ @"xp_amount_rank", @"xp_max_rank", @"battles_count_rank", @"spotted_count_rank", @"damage_dealt_rank", ] }; NSError *error; WOTRatingType *ratingType = [MTLJSONAdapter modelOfClass:WOTRatingType.class fromJSONDictionary:json error:&error]; [[error should] beNil]; [[ratingType should] beKindOfClass:WOTRatingType.class]; [[json[@"threshold"] should] equal:theValue(ratingType.threshold)]; * [[json[@"type"] should] equal:ratingType.type]; [[ratingType.rankFields shouldNot] beNil]; [[ratingType.rankFields should] beKindOfClass:NSArray.class]; [[ratingType.rankFields should] haveCountOf:5]; [[@"maximumXP" should] equal:ratingType.rankFields[1]]; }); });
* — - - Kiwi, , nil .
API :
; .
, , : query http-. 
, ReactiveCocoa , ( ). ( ) 2k LOC ( ~1k - , ~700 - ), : — ..
Kiwi
BDD- RSpec - , . «--».
OHTTPStubs
, web- . "", - .
.
There are only two hard things in Computer Science: cache invalidation and naming things.
-- Phil Karlton
« » :
..
, .
- inMemory CoreData , NSCache . , , — . NSURLConnection .
NSURLCache
NSURLCache NSURLRequest . , .
:
NSURLCache *urlCache = [[NSURLCache alloc] initWithMemoryCapacity:kInMemoryCacheSize diskCapacity:kOnDiskCacheSize diskPath:path]; [NSURLCache setSharedURLCache:urlCache];
, .
NSURLConnectionDelegate , "" :
- (NSCachedURLResponse *)connection:(NSURLConnection *)connection willCacheResponse:(NSCachedURLResponse *)cachedResponse;
?
NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse*)[cachedResponse response]; NSDictionary *headers = [httpResponse allHeaderFields]; NSString *cacheControl = [headers valueForKey:@"Cache-Control"]; NSString *expires = [headers valueForKey:@"Expires"]; if((cacheControl == nil) && (expires == nil)) { NSMutableDictionary *modifiedHeaders = [httpResponse.allHeaderFields mutableCopy]; NSString *expireDate = [NSDate RFC822StringFromDate:[[NSDate date] dateByAddingTimeInterval:cacheTime]]; NSDictionary *values = @{@"Expires": expireDate, @"Cache-Control": @"public"}; [modifiedHeaders addEntriesFromDictionary:values]; NSHTTPURLResponse *response = [[NSHTTPURLResponse alloc] initWithURL:httpResponse.URL statusCode:httpResponse.statusCode HTTPVersion:@"HTTP/1.1" headerFields:modifiedHeaders]; NSCachedURLResponse *modifiedCachedResponse = [[NSCachedURLResponse alloc] initWithResponse:response data:cachedResponse.data userInfo:cachedResponse.userInfo storagePolicy:NSURLCacheStorageAllowed]; return modifiedCachedResponse; } return cachedResponse;
HTTP- .
, ( , , ). , ( cacheTime ) .
— ( ). , , .
:
(, , ..); - NSURLCache , .
:
30 ; ; : 200- , NSURLConnection .
""
API (, ), fields :
. . . , .
, Player , JSON-, . , fields , NSDictionary . , - . , .
JSON -> NSObject Mantle , API ( RACSignal API ):
- (RACSignal *)searchPlayers:(NSString *)query limit:(NSUInteger)limit { NSMutableDictionary *parameters = [NSMutableDictionary dictionary]; parameters[@"search"] = query; if (limit) parameters[@"limit"] = @(limit); return [self getPath:@"account/list" parameters:parameters resultClass:WOTSearchPlayer.class]; }
, resultClass , ? :
- (RACSignal *)searchPlayers:(NSString *)query limit:(NSUInteger)limit resultClass:(Class)resultClass
, API, .
API , : , , "" . ( ). , ( AFJSONRequestOperation ):
op1 = [AFJSONRequestOperation JSONRequestOperationWithRequest:nil success:^(id JSON) { op2 = [AFJSONRequestOperation JSONRequestOperationWithRequest:nil success:^(id JSON) { op3 = [AFJSONRequestOperation JSONRequestOperationWithRequest:nil success:^(id JSON) { // Combine all the responses and return } failure:^(NSError *error) { // Handle error }]; } failure:^(NSError *error) { // Handle error }]; } failure:^(NSError *error) { // Handle error }];
, , . ReactiveCocoa , .
API :
- (RACSignal *)searchPlayers:(NSString *)query limit:(NSUInteger)limit; - (RACSignal *)fetchPlayers:(NSArray *)playerIDs;
RACSignal — , , . — , . / :
RACSignal *fetchPlayersAndRatings = [[[[API searchPlayers:@"" limit:0] flattenMap:^RACStream *(id players) { // skipping data processing return [API fetchPlayers:@[]]; }] flattenMap:^RACStream *(id fullPlayers) { // Skipping data processing return [API statsSliceForPlayer:nil hoursAgo:@[] fields:nil]; }] flattenMap:^RACStream *(id value) { // Compose all responses here id composedResponse; return [RACSignal return:composedResponse]; }]; [fetchPlayersAndRatings subscribeNext:^(id x) { // Fully composed response } error:^(NSError *error) { // All erros go to one place }];
ReactiveCocoa — , . , Promises Futures.
:
Core ( , ); WOT ( World of Tanks–); WOWP ( World of Warplanes–)
, , — . , ( ) CocoaPods , . subspecs , :
# Subspecs s.subspec 'Core' do |cs| cs.source_files = 'WGNAPIClient/Core/**/*.{h,m}' end s.subspec 'WOT' do |cs| cs.dependency 'WGNAPIClient/Core' cs.source_files = 'WGNAPIClient/WOT/**/*.{h,m}' end s.subspec 'WOWP' do |cs| cs.dependency 'WGNAPIClient/Core' cs.source_files = 'WGNAPIClient/WOWP/**/*.{h,m}' end
"" "" , :
pod 'WGNAPIClient/WOT' pod 'WGNAPIClient/WOWP'
. .
(98%). :
; .
:
context(@"API works with players data", ^{ it (@"should search players", ^{ stubResponse(@"/wot/account/list", @"players-search.json"); RACSignal *searchRequest = [client searchPlayers:@"garnett" limit:0]; NSArray *response = [searchRequest asynchronousFirstOrDefault:nil success:NULL error:&error]; [[error should] beNil]; [[response should] beKindOfClass:NSArray.class]; [[response.lastObject should] beKindOfClass:WOTSearchPlayer.class]; WOTSearchPlayer *player = response[0]; [[player.ID should] equal:@"1785514"]; }); });
, / , RACSignal , :
(id)asynchronousFirstOrDefault:(id)defaultValue success:(BOOL *)success error:(NSError **)error;
describe(@"WOTRatingType", ^{ it(@"should parse json to model object", ^{ NSDictionary *json = @{ @"threshold": @5, @"type": @"1", @"rank_fields": @[ @"xp_amount_rank", @"xp_max_rank", @"battles_count_rank", @"spotted_count_rank", @"damage_dealt_rank", ] }; NSError *error; WOTRatingType *ratingType = [MTLJSONAdapter modelOfClass:WOTRatingType.class fromJSONDictionary:json error:&error]; [[error should] beNil]; [[ratingType should] beKindOfClass:WOTRatingType.class]; [[json[@"threshold"] should] equal:theValue(ratingType.threshold)]; * [[json[@"type"] should] equal:ratingType.type]; [[ratingType.rankFields shouldNot] beNil]; [[ratingType.rankFields should] beKindOfClass:NSArray.class]; [[ratingType.rankFields should] haveCountOf:5]; [[@"maximumXP" should] equal:ratingType.rankFields[1]]; }); });
* — - - Kiwi, , nil .
API :
; .
, , : query http-. 
, ReactiveCocoa , ( ). ( ) 2k LOC ( ~1k - , ~700 - ), : — ..
Kiwi
BDD- RSpec - , . «--».
OHTTPStubs
, web- . "", - .
.
There are only two hard things in Computer Science: cache invalidation and naming things.
-- Phil Karlton
« » :
..
, .
- inMemory CoreData , NSCache . , , — . NSURLConnection .
NSURLCache
NSURLCache NSURLRequest . , .
:
NSURLCache *urlCache = [[NSURLCache alloc] initWithMemoryCapacity:kInMemoryCacheSize diskCapacity:kOnDiskCacheSize diskPath:path]; [NSURLCache setSharedURLCache:urlCache];
, .
NSURLConnectionDelegate , "" :
- (NSCachedURLResponse *)connection:(NSURLConnection *)connection willCacheResponse:(NSCachedURLResponse *)cachedResponse;
?
NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse*)[cachedResponse response]; NSDictionary *headers = [httpResponse allHeaderFields]; NSString *cacheControl = [headers valueForKey:@"Cache-Control"]; NSString *expires = [headers valueForKey:@"Expires"]; if((cacheControl == nil) && (expires == nil)) { NSMutableDictionary *modifiedHeaders = [httpResponse.allHeaderFields mutableCopy]; NSString *expireDate = [NSDate RFC822StringFromDate:[[NSDate date] dateByAddingTimeInterval:cacheTime]]; NSDictionary *values = @{@"Expires": expireDate, @"Cache-Control": @"public"}; [modifiedHeaders addEntriesFromDictionary:values]; NSHTTPURLResponse *response = [[NSHTTPURLResponse alloc] initWithURL:httpResponse.URL statusCode:httpResponse.statusCode HTTPVersion:@"HTTP/1.1" headerFields:modifiedHeaders]; NSCachedURLResponse *modifiedCachedResponse = [[NSCachedURLResponse alloc] initWithResponse:response data:cachedResponse.data userInfo:cachedResponse.userInfo storagePolicy:NSURLCacheStorageAllowed]; return modifiedCachedResponse; } return cachedResponse;
HTTP- .
, ( , , ). , ( cacheTime ) .
— ( ). , , .
:
(, , ..); - NSURLCache , .
:
30 ; ; : 200- , NSURLConnection .
""
API (, ), fields :
. . . , .
, Player , JSON-, . , fields , NSDictionary . , - . , .
JSON -> NSObject Mantle , API ( RACSignal API ):
- (RACSignal *)searchPlayers:(NSString *)query limit:(NSUInteger)limit { NSMutableDictionary *parameters = [NSMutableDictionary dictionary]; parameters[@"search"] = query; if (limit) parameters[@"limit"] = @(limit); return [self getPath:@"account/list" parameters:parameters resultClass:WOTSearchPlayer.class]; }
, resultClass , ? :
- (RACSignal *)searchPlayers:(NSString *)query limit:(NSUInteger)limit resultClass:(Class)resultClass
, API, .
API , : , , "" . ( ). , ( AFJSONRequestOperation ):
op1 = [AFJSONRequestOperation JSONRequestOperationWithRequest:nil success:^(id JSON) { op2 = [AFJSONRequestOperation JSONRequestOperationWithRequest:nil success:^(id JSON) { op3 = [AFJSONRequestOperation JSONRequestOperationWithRequest:nil success:^(id JSON) { // Combine all the responses and return } failure:^(NSError *error) { // Handle error }]; } failure:^(NSError *error) { // Handle error }]; } failure:^(NSError *error) { // Handle error }];
, , . ReactiveCocoa , .
API :
- (RACSignal *)searchPlayers:(NSString *)query limit:(NSUInteger)limit; - (RACSignal *)fetchPlayers:(NSArray *)playerIDs;
RACSignal — , , . — , . / :
RACSignal *fetchPlayersAndRatings = [[[[API searchPlayers:@"" limit:0] flattenMap:^RACStream *(id players) { // skipping data processing return [API fetchPlayers:@[]]; }] flattenMap:^RACStream *(id fullPlayers) { // Skipping data processing return [API statsSliceForPlayer:nil hoursAgo:@[] fields:nil]; }] flattenMap:^RACStream *(id value) { // Compose all responses here id composedResponse; return [RACSignal return:composedResponse]; }]; [fetchPlayersAndRatings subscribeNext:^(id x) { // Fully composed response } error:^(NSError *error) { // All erros go to one place }];
ReactiveCocoa — , . , Promises Futures.
:
Core ( , ); WOT ( World of Tanks–); WOWP ( World of Warplanes–)
, , — . , ( ) CocoaPods , . subspecs , :
# Subspecs s.subspec 'Core' do |cs| cs.source_files = 'WGNAPIClient/Core/**/*.{h,m}' end s.subspec 'WOT' do |cs| cs.dependency 'WGNAPIClient/Core' cs.source_files = 'WGNAPIClient/WOT/**/*.{h,m}' end s.subspec 'WOWP' do |cs| cs.dependency 'WGNAPIClient/Core' cs.source_files = 'WGNAPIClient/WOWP/**/*.{h,m}' end
"" "" , :
pod 'WGNAPIClient/WOT' pod 'WGNAPIClient/WOWP'
. .
(98%). :
; .
:
context(@"API works with players data", ^{ it (@"should search players", ^{ stubResponse(@"/wot/account/list", @"players-search.json"); RACSignal *searchRequest = [client searchPlayers:@"garnett" limit:0]; NSArray *response = [searchRequest asynchronousFirstOrDefault:nil success:NULL error:&error]; [[error should] beNil]; [[response should] beKindOfClass:NSArray.class]; [[response.lastObject should] beKindOfClass:WOTSearchPlayer.class]; WOTSearchPlayer *player = response[0]; [[player.ID should] equal:@"1785514"]; }); });
, / , RACSignal , :
(id)asynchronousFirstOrDefault:(id)defaultValue success:(BOOL *)success error:(NSError **)error;
describe(@"WOTRatingType", ^{ it(@"should parse json to model object", ^{ NSDictionary *json = @{ @"threshold": @5, @"type": @"1", @"rank_fields": @[ @"xp_amount_rank", @"xp_max_rank", @"battles_count_rank", @"spotted_count_rank", @"damage_dealt_rank", ] }; NSError *error; WOTRatingType *ratingType = [MTLJSONAdapter modelOfClass:WOTRatingType.class fromJSONDictionary:json error:&error]; [[error should] beNil]; [[ratingType should] beKindOfClass:WOTRatingType.class]; [[json[@"threshold"] should] equal:theValue(ratingType.threshold)]; * [[json[@"type"] should] equal:ratingType.type]; [[ratingType.rankFields shouldNot] beNil]; [[ratingType.rankFields should] beKindOfClass:NSArray.class]; [[ratingType.rankFields should] haveCountOf:5]; [[@"maximumXP" should] equal:ratingType.rankFields[1]]; }); });
* — - - Kiwi, , nil .
API :
; .
, , : query http-. 
, ReactiveCocoa , ( ). ( ) 2k LOC ( ~1k - , ~700 - ), : — ..
Kiwi
BDD- RSpec - , . «--».
OHTTPStubs
, web- . "", - .
.
There are only two hard things in Computer Science: cache invalidation and naming things.
-- Phil Karlton
« » :
..
, .
- inMemory CoreData , NSCache . , , — . NSURLConnection .
NSURLCache
NSURLCache NSURLRequest . , .
:
NSURLCache *urlCache = [[NSURLCache alloc] initWithMemoryCapacity:kInMemoryCacheSize diskCapacity:kOnDiskCacheSize diskPath:path]; [NSURLCache setSharedURLCache:urlCache];
, .
NSURLConnectionDelegate , "" :
- (NSCachedURLResponse *)connection:(NSURLConnection *)connection willCacheResponse:(NSCachedURLResponse *)cachedResponse;
?
NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse*)[cachedResponse response]; NSDictionary *headers = [httpResponse allHeaderFields]; NSString *cacheControl = [headers valueForKey:@"Cache-Control"]; NSString *expires = [headers valueForKey:@"Expires"]; if((cacheControl == nil) && (expires == nil)) { NSMutableDictionary *modifiedHeaders = [httpResponse.allHeaderFields mutableCopy]; NSString *expireDate = [NSDate RFC822StringFromDate:[[NSDate date] dateByAddingTimeInterval:cacheTime]]; NSDictionary *values = @{@"Expires": expireDate, @"Cache-Control": @"public"}; [modifiedHeaders addEntriesFromDictionary:values]; NSHTTPURLResponse *response = [[NSHTTPURLResponse alloc] initWithURL:httpResponse.URL statusCode:httpResponse.statusCode HTTPVersion:@"HTTP/1.1" headerFields:modifiedHeaders]; NSCachedURLResponse *modifiedCachedResponse = [[NSCachedURLResponse alloc] initWithResponse:response data:cachedResponse.data userInfo:cachedResponse.userInfo storagePolicy:NSURLCacheStorageAllowed]; return modifiedCachedResponse; } return cachedResponse;
HTTP- .
, ( , , ). , ( cacheTime ) .
— ( ). , , .
:
(, , ..); - NSURLCache , .
:
30 ; ; : 200- , NSURLConnection .
""
API (, ), fields :
. . . , .
, Player , JSON-, . , fields , NSDictionary . , - . , .
JSON -> NSObject Mantle , API ( RACSignal API ):
- (RACSignal *)searchPlayers:(NSString *)query limit:(NSUInteger)limit { NSMutableDictionary *parameters = [NSMutableDictionary dictionary]; parameters[@"search"] = query; if (limit) parameters[@"limit"] = @(limit); return [self getPath:@"account/list" parameters:parameters resultClass:WOTSearchPlayer.class]; }
, resultClass , ? :
- (RACSignal *)searchPlayers:(NSString *)query limit:(NSUInteger)limit resultClass:(Class)resultClass
, API, .
API , : , , "" . ( ). , ( AFJSONRequestOperation ):
op1 = [AFJSONRequestOperation JSONRequestOperationWithRequest:nil success:^(id JSON) { op2 = [AFJSONRequestOperation JSONRequestOperationWithRequest:nil success:^(id JSON) { op3 = [AFJSONRequestOperation JSONRequestOperationWithRequest:nil success:^(id JSON) { // Combine all the responses and return } failure:^(NSError *error) { // Handle error }]; } failure:^(NSError *error) { // Handle error }]; } failure:^(NSError *error) { // Handle error }];
, , . ReactiveCocoa , .
API :
- (RACSignal *)searchPlayers:(NSString *)query limit:(NSUInteger)limit; - (RACSignal *)fetchPlayers:(NSArray *)playerIDs;
RACSignal — , , . — , . / :
RACSignal *fetchPlayersAndRatings = [[[[API searchPlayers:@"" limit:0] flattenMap:^RACStream *(id players) { // skipping data processing return [API fetchPlayers:@[]]; }] flattenMap:^RACStream *(id fullPlayers) { // Skipping data processing return [API statsSliceForPlayer:nil hoursAgo:@[] fields:nil]; }] flattenMap:^RACStream *(id value) { // Compose all responses here id composedResponse; return [RACSignal return:composedResponse]; }]; [fetchPlayersAndRatings subscribeNext:^(id x) { // Fully composed response } error:^(NSError *error) { // All erros go to one place }];
ReactiveCocoa — , . , Promises Futures.
:
Core ( , ); WOT ( World of Tanks–); WOWP ( World of Warplanes–)
, , — . , ( ) CocoaPods , . subspecs , :
# Subspecs s.subspec 'Core' do |cs| cs.source_files = 'WGNAPIClient/Core/**/*.{h,m}' end s.subspec 'WOT' do |cs| cs.dependency 'WGNAPIClient/Core' cs.source_files = 'WGNAPIClient/WOT/**/*.{h,m}' end s.subspec 'WOWP' do |cs| cs.dependency 'WGNAPIClient/Core' cs.source_files = 'WGNAPIClient/WOWP/**/*.{h,m}' end
"" "" , :
pod 'WGNAPIClient/WOT' pod 'WGNAPIClient/WOWP'
. .
(98%). :
; .
:
context(@"API works with players data", ^{ it (@"should search players", ^{ stubResponse(@"/wot/account/list", @"players-search.json"); RACSignal *searchRequest = [client searchPlayers:@"garnett" limit:0]; NSArray *response = [searchRequest asynchronousFirstOrDefault:nil success:NULL error:&error]; [[error should] beNil]; [[response should] beKindOfClass:NSArray.class]; [[response.lastObject should] beKindOfClass:WOTSearchPlayer.class]; WOTSearchPlayer *player = response[0]; [[player.ID should] equal:@"1785514"]; }); });
, / , RACSignal , :
(id)asynchronousFirstOrDefault:(id)defaultValue success:(BOOL *)success error:(NSError **)error;
describe(@"WOTRatingType", ^{ it(@"should parse json to model object", ^{ NSDictionary *json = @{ @"threshold": @5, @"type": @"1", @"rank_fields": @[ @"xp_amount_rank", @"xp_max_rank", @"battles_count_rank", @"spotted_count_rank", @"damage_dealt_rank", ] }; NSError *error; WOTRatingType *ratingType = [MTLJSONAdapter modelOfClass:WOTRatingType.class fromJSONDictionary:json error:&error]; [[error should] beNil]; [[ratingType should] beKindOfClass:WOTRatingType.class]; [[json[@"threshold"] should] equal:theValue(ratingType.threshold)]; * [[json[@"type"] should] equal:ratingType.type]; [[ratingType.rankFields shouldNot] beNil]; [[ratingType.rankFields should] beKindOfClass:NSArray.class]; [[ratingType.rankFields should] haveCountOf:5]; [[@"maximumXP" should] equal:ratingType.rankFields[1]]; }); });
* — - - Kiwi, , nil .
API :
; .
, , : query http-. 
, ReactiveCocoa , ( ). ( ) 2k LOC ( ~1k - , ~700 - ), : — ..
Kiwi
BDD- RSpec - , . «--».
OHTTPStubs
, web- . "", - .
.
There are only two hard things in Computer Science: cache invalidation and naming things.
-- Phil Karlton
« » :
..
, .
- inMemory CoreData , NSCache . , , — . NSURLConnection .
NSURLCache
NSURLCache NSURLRequest . , .
:
NSURLCache *urlCache = [[NSURLCache alloc] initWithMemoryCapacity:kInMemoryCacheSize diskCapacity:kOnDiskCacheSize diskPath:path]; [NSURLCache setSharedURLCache:urlCache];
, .
NSURLConnectionDelegate , "" :
- (NSCachedURLResponse *)connection:(NSURLConnection *)connection willCacheResponse:(NSCachedURLResponse *)cachedResponse;
?
NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse*)[cachedResponse response]; NSDictionary *headers = [httpResponse allHeaderFields]; NSString *cacheControl = [headers valueForKey:@"Cache-Control"]; NSString *expires = [headers valueForKey:@"Expires"]; if((cacheControl == nil) && (expires == nil)) { NSMutableDictionary *modifiedHeaders = [httpResponse.allHeaderFields mutableCopy]; NSString *expireDate = [NSDate RFC822StringFromDate:[[NSDate date] dateByAddingTimeInterval:cacheTime]]; NSDictionary *values = @{@"Expires": expireDate, @"Cache-Control": @"public"}; [modifiedHeaders addEntriesFromDictionary:values]; NSHTTPURLResponse *response = [[NSHTTPURLResponse alloc] initWithURL:httpResponse.URL statusCode:httpResponse.statusCode HTTPVersion:@"HTTP/1.1" headerFields:modifiedHeaders]; NSCachedURLResponse *modifiedCachedResponse = [[NSCachedURLResponse alloc] initWithResponse:response data:cachedResponse.data userInfo:cachedResponse.userInfo storagePolicy:NSURLCacheStorageAllowed]; return modifiedCachedResponse; } return cachedResponse;
HTTP- .
, ( , , ). , ( cacheTime ) .
— ( ). , , .
:
(, , ..); - NSURLCache , .
:
30 ; ; : 200- , NSURLConnection .
""
API (, ), fields :
. . . , .
, Player , JSON-, . , fields , NSDictionary . , - . , .
JSON -> NSObject Mantle , API ( RACSignal API ):
- (RACSignal *)searchPlayers:(NSString *)query limit:(NSUInteger)limit { NSMutableDictionary *parameters = [NSMutableDictionary dictionary]; parameters[@"search"] = query; if (limit) parameters[@"limit"] = @(limit); return [self getPath:@"account/list" parameters:parameters resultClass:WOTSearchPlayer.class]; }
, resultClass , ? :
- (RACSignal *)searchPlayers:(NSString *)query limit:(NSUInteger)limit resultClass:(Class)resultClass
, API, .
API , : , , "" . ( ). , ( AFJSONRequestOperation ):
op1 = [AFJSONRequestOperation JSONRequestOperationWithRequest:nil success:^(id JSON) { op2 = [AFJSONRequestOperation JSONRequestOperationWithRequest:nil success:^(id JSON) { op3 = [AFJSONRequestOperation JSONRequestOperationWithRequest:nil success:^(id JSON) { // Combine all the responses and return } failure:^(NSError *error) { // Handle error }]; } failure:^(NSError *error) { // Handle error }]; } failure:^(NSError *error) { // Handle error }];
, , . ReactiveCocoa , .
API :
- (RACSignal *)searchPlayers:(NSString *)query limit:(NSUInteger)limit; - (RACSignal *)fetchPlayers:(NSArray *)playerIDs;
RACSignal — , , . — , . / :
RACSignal *fetchPlayersAndRatings = [[[[API searchPlayers:@"" limit:0] flattenMap:^RACStream *(id players) { // skipping data processing return [API fetchPlayers:@[]]; }] flattenMap:^RACStream *(id fullPlayers) { // Skipping data processing return [API statsSliceForPlayer:nil hoursAgo:@[] fields:nil]; }] flattenMap:^RACStream *(id value) { // Compose all responses here id composedResponse; return [RACSignal return:composedResponse]; }]; [fetchPlayersAndRatings subscribeNext:^(id x) { // Fully composed response } error:^(NSError *error) { // All erros go to one place }];
ReactiveCocoa — , . , Promises Futures.
:
Core ( , ); WOT ( World of Tanks–); WOWP ( World of Warplanes–)
, , — . , ( ) CocoaPods , . subspecs , :
# Subspecs s.subspec 'Core' do |cs| cs.source_files = 'WGNAPIClient/Core/**/*.{h,m}' end s.subspec 'WOT' do |cs| cs.dependency 'WGNAPIClient/Core' cs.source_files = 'WGNAPIClient/WOT/**/*.{h,m}' end s.subspec 'WOWP' do |cs| cs.dependency 'WGNAPIClient/Core' cs.source_files = 'WGNAPIClient/WOWP/**/*.{h,m}' end
"" "" , :
pod 'WGNAPIClient/WOT' pod 'WGNAPIClient/WOWP'
. .
(98%). :
; .
:
context(@"API works with players data", ^{ it (@"should search players", ^{ stubResponse(@"/wot/account/list", @"players-search.json"); RACSignal *searchRequest = [client searchPlayers:@"garnett" limit:0]; NSArray *response = [searchRequest asynchronousFirstOrDefault:nil success:NULL error:&error]; [[error should] beNil]; [[response should] beKindOfClass:NSArray.class]; [[response.lastObject should] beKindOfClass:WOTSearchPlayer.class]; WOTSearchPlayer *player = response[0]; [[player.ID should] equal:@"1785514"]; }); });
, / , RACSignal , :
(id)asynchronousFirstOrDefault:(id)defaultValue success:(BOOL *)success error:(NSError **)error;
describe(@"WOTRatingType", ^{ it(@"should parse json to model object", ^{ NSDictionary *json = @{ @"threshold": @5, @"type": @"1", @"rank_fields": @[ @"xp_amount_rank", @"xp_max_rank", @"battles_count_rank", @"spotted_count_rank", @"damage_dealt_rank", ] }; NSError *error; WOTRatingType *ratingType = [MTLJSONAdapter modelOfClass:WOTRatingType.class fromJSONDictionary:json error:&error]; [[error should] beNil]; [[ratingType should] beKindOfClass:WOTRatingType.class]; [[json[@"threshold"] should] equal:theValue(ratingType.threshold)]; * [[json[@"type"] should] equal:ratingType.type]; [[ratingType.rankFields shouldNot] beNil]; [[ratingType.rankFields should] beKindOfClass:NSArray.class]; [[ratingType.rankFields should] haveCountOf:5]; [[@"maximumXP" should] equal:ratingType.rankFields[1]]; }); });
* — - - Kiwi, , nil .
API :
; .
, , : query http-. 
, ReactiveCocoa , ( ). ( ) 2k LOC ( ~1k - , ~700 - ), : — ..
Kiwi
BDD- RSpec - , . «--».
OHTTPStubs
, web- . "", - .
.
There are only two hard things in Computer Science: cache invalidation and naming things.
-- Phil Karlton
« » :
..
, .
- inMemory CoreData , NSCache . , , — . NSURLConnection .
NSURLCache
NSURLCache NSURLRequest . , .
:
NSURLCache *urlCache = [[NSURLCache alloc] initWithMemoryCapacity:kInMemoryCacheSize diskCapacity:kOnDiskCacheSize diskPath:path]; [NSURLCache setSharedURLCache:urlCache];
, .
NSURLConnectionDelegate , "" :
- (NSCachedURLResponse *)connection:(NSURLConnection *)connection willCacheResponse:(NSCachedURLResponse *)cachedResponse;
?
NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse*)[cachedResponse response]; NSDictionary *headers = [httpResponse allHeaderFields]; NSString *cacheControl = [headers valueForKey:@"Cache-Control"]; NSString *expires = [headers valueForKey:@"Expires"]; if((cacheControl == nil) && (expires == nil)) { NSMutableDictionary *modifiedHeaders = [httpResponse.allHeaderFields mutableCopy]; NSString *expireDate = [NSDate RFC822StringFromDate:[[NSDate date] dateByAddingTimeInterval:cacheTime]]; NSDictionary *values = @{@"Expires": expireDate, @"Cache-Control": @"public"}; [modifiedHeaders addEntriesFromDictionary:values]; NSHTTPURLResponse *response = [[NSHTTPURLResponse alloc] initWithURL:httpResponse.URL statusCode:httpResponse.statusCode HTTPVersion:@"HTTP/1.1" headerFields:modifiedHeaders]; NSCachedURLResponse *modifiedCachedResponse = [[NSCachedURLResponse alloc] initWithResponse:response data:cachedResponse.data userInfo:cachedResponse.userInfo storagePolicy:NSURLCacheStorageAllowed]; return modifiedCachedResponse; } return cachedResponse;
HTTP- .
, ( , , ). , ( cacheTime ) .
— ( ). , , .
:
(, , ..); - NSURLCache , .
:
30 ; ; : 200- , NSURLConnection .
""
API (, ), fields :
. . . , .
, Player , JSON-, . , fields , NSDictionary . , - . , .
JSON -> NSObject Mantle , API ( RACSignal API ):
- (RACSignal *)searchPlayers:(NSString *)query limit:(NSUInteger)limit { NSMutableDictionary *parameters = [NSMutableDictionary dictionary]; parameters[@"search"] = query; if (limit) parameters[@"limit"] = @(limit); return [self getPath:@"account/list" parameters:parameters resultClass:WOTSearchPlayer.class]; }
, resultClass , ? :
- (RACSignal *)searchPlayers:(NSString *)query limit:(NSUInteger)limit resultClass:(Class)resultClass
, API, .
API , : , , "" . ( ). , ( AFJSONRequestOperation ):
op1 = [AFJSONRequestOperation JSONRequestOperationWithRequest:nil success:^(id JSON) { op2 = [AFJSONRequestOperation JSONRequestOperationWithRequest:nil success:^(id JSON) { op3 = [AFJSONRequestOperation JSONRequestOperationWithRequest:nil success:^(id JSON) { // Combine all the responses and return } failure:^(NSError *error) { // Handle error }]; } failure:^(NSError *error) { // Handle error }]; } failure:^(NSError *error) { // Handle error }];
, , . ReactiveCocoa , .
API :
- (RACSignal *)searchPlayers:(NSString *)query limit:(NSUInteger)limit; - (RACSignal *)fetchPlayers:(NSArray *)playerIDs;
RACSignal — , , . — , . / :
RACSignal *fetchPlayersAndRatings = [[[[API searchPlayers:@"" limit:0] flattenMap:^RACStream *(id players) { // skipping data processing return [API fetchPlayers:@[]]; }] flattenMap:^RACStream *(id fullPlayers) { // Skipping data processing return [API statsSliceForPlayer:nil hoursAgo:@[] fields:nil]; }] flattenMap:^RACStream *(id value) { // Compose all responses here id composedResponse; return [RACSignal return:composedResponse]; }]; [fetchPlayersAndRatings subscribeNext:^(id x) { // Fully composed response } error:^(NSError *error) { // All erros go to one place }];
ReactiveCocoa — , . , Promises Futures.
:
Core ( , ); WOT ( World of Tanks–); WOWP ( World of Warplanes–)
, , — . , ( ) CocoaPods , . subspecs , :
# Subspecs s.subspec 'Core' do |cs| cs.source_files = 'WGNAPIClient/Core/**/*.{h,m}' end s.subspec 'WOT' do |cs| cs.dependency 'WGNAPIClient/Core' cs.source_files = 'WGNAPIClient/WOT/**/*.{h,m}' end s.subspec 'WOWP' do |cs| cs.dependency 'WGNAPIClient/Core' cs.source_files = 'WGNAPIClient/WOWP/**/*.{h,m}' end
"" "" , :
pod 'WGNAPIClient/WOT' pod 'WGNAPIClient/WOWP'
. .
(98%). :
; .
:
context(@"API works with players data", ^{ it (@"should search players", ^{ stubResponse(@"/wot/account/list", @"players-search.json"); RACSignal *searchRequest = [client searchPlayers:@"garnett" limit:0]; NSArray *response = [searchRequest asynchronousFirstOrDefault:nil success:NULL error:&error]; [[error should] beNil]; [[response should] beKindOfClass:NSArray.class]; [[response.lastObject should] beKindOfClass:WOTSearchPlayer.class]; WOTSearchPlayer *player = response[0]; [[player.ID should] equal:@"1785514"]; }); });
, / , RACSignal , :
(id)asynchronousFirstOrDefault:(id)defaultValue success:(BOOL *)success error:(NSError **)error;
describe(@"WOTRatingType", ^{ it(@"should parse json to model object", ^{ NSDictionary *json = @{ @"threshold": @5, @"type": @"1", @"rank_fields": @[ @"xp_amount_rank", @"xp_max_rank", @"battles_count_rank", @"spotted_count_rank", @"damage_dealt_rank", ] }; NSError *error; WOTRatingType *ratingType = [MTLJSONAdapter modelOfClass:WOTRatingType.class fromJSONDictionary:json error:&error]; [[error should] beNil]; [[ratingType should] beKindOfClass:WOTRatingType.class]; [[json[@"threshold"] should] equal:theValue(ratingType.threshold)]; * [[json[@"type"] should] equal:ratingType.type]; [[ratingType.rankFields shouldNot] beNil]; [[ratingType.rankFields should] beKindOfClass:NSArray.class]; [[ratingType.rankFields should] haveCountOf:5]; [[@"maximumXP" should] equal:ratingType.rankFields[1]]; }); });
* — - - Kiwi, , nil .
API :
; .
, , : query http-. 
, ReactiveCocoa , ( ). ( ) 2k LOC ( ~1k - , ~700 - ), : — ..
Kiwi
BDD- RSpec - , . «--».
OHTTPStubs
, web- . "", - .
.
There are only two hard things in Computer Science: cache invalidation and naming things.
-- Phil Karlton
« » :
..
, .
- inMemory CoreData , NSCache . , , — . NSURLConnection .
NSURLCache
NSURLCache NSURLRequest . , .
:
NSURLCache *urlCache = [[NSURLCache alloc] initWithMemoryCapacity:kInMemoryCacheSize diskCapacity:kOnDiskCacheSize diskPath:path]; [NSURLCache setSharedURLCache:urlCache];
, .
NSURLConnectionDelegate , "" :
- (NSCachedURLResponse *)connection:(NSURLConnection *)connection willCacheResponse:(NSCachedURLResponse *)cachedResponse;
?
NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse*)[cachedResponse response]; NSDictionary *headers = [httpResponse allHeaderFields]; NSString *cacheControl = [headers valueForKey:@"Cache-Control"]; NSString *expires = [headers valueForKey:@"Expires"]; if((cacheControl == nil) && (expires == nil)) { NSMutableDictionary *modifiedHeaders = [httpResponse.allHeaderFields mutableCopy]; NSString *expireDate = [NSDate RFC822StringFromDate:[[NSDate date] dateByAddingTimeInterval:cacheTime]]; NSDictionary *values = @{@"Expires": expireDate, @"Cache-Control": @"public"}; [modifiedHeaders addEntriesFromDictionary:values]; NSHTTPURLResponse *response = [[NSHTTPURLResponse alloc] initWithURL:httpResponse.URL statusCode:httpResponse.statusCode HTTPVersion:@"HTTP/1.1" headerFields:modifiedHeaders]; NSCachedURLResponse *modifiedCachedResponse = [[NSCachedURLResponse alloc] initWithResponse:response data:cachedResponse.data userInfo:cachedResponse.userInfo storagePolicy:NSURLCacheStorageAllowed]; return modifiedCachedResponse; } return cachedResponse;
HTTP- .
, ( , , ). , ( cacheTime ) .
— ( ). , , .
:
(, , ..); - NSURLCache , .
:
30 ; ; : 200- , NSURLConnection .
""
API (, ), fields :
. . . , .
, Player , JSON-, . , fields , NSDictionary . , - . , .
JSON -> NSObject Mantle , API ( RACSignal API ):
- (RACSignal *)searchPlayers:(NSString *)query limit:(NSUInteger)limit { NSMutableDictionary *parameters = [NSMutableDictionary dictionary]; parameters[@"search"] = query; if (limit) parameters[@"limit"] = @(limit); return [self getPath:@"account/list" parameters:parameters resultClass:WOTSearchPlayer.class]; }
, resultClass , ? :
- (RACSignal *)searchPlayers:(NSString *)query limit:(NSUInteger)limit resultClass:(Class)resultClass
, API, .
API , : , , "" . ( ). , ( AFJSONRequestOperation ):
op1 = [AFJSONRequestOperation JSONRequestOperationWithRequest:nil success:^(id JSON) { op2 = [AFJSONRequestOperation JSONRequestOperationWithRequest:nil success:^(id JSON) { op3 = [AFJSONRequestOperation JSONRequestOperationWithRequest:nil success:^(id JSON) { // Combine all the responses and return } failure:^(NSError *error) { // Handle error }]; } failure:^(NSError *error) { // Handle error }]; } failure:^(NSError *error) { // Handle error }];
, , . ReactiveCocoa , .
API :
- (RACSignal *)searchPlayers:(NSString *)query limit:(NSUInteger)limit; - (RACSignal *)fetchPlayers:(NSArray *)playerIDs;
RACSignal — , , . — , . / :
RACSignal *fetchPlayersAndRatings = [[[[API searchPlayers:@"" limit:0] flattenMap:^RACStream *(id players) { // skipping data processing return [API fetchPlayers:@[]]; }] flattenMap:^RACStream *(id fullPlayers) { // Skipping data processing return [API statsSliceForPlayer:nil hoursAgo:@[] fields:nil]; }] flattenMap:^RACStream *(id value) { // Compose all responses here id composedResponse; return [RACSignal return:composedResponse]; }]; [fetchPlayersAndRatings subscribeNext:^(id x) { // Fully composed response } error:^(NSError *error) { // All erros go to one place }];
ReactiveCocoa — , . , Promises Futures.
:
Core ( , ); WOT ( World of Tanks–); WOWP ( World of Warplanes–)
, , — . , ( ) CocoaPods , . subspecs , :
# Subspecs s.subspec 'Core' do |cs| cs.source_files = 'WGNAPIClient/Core/**/*.{h,m}' end s.subspec 'WOT' do |cs| cs.dependency 'WGNAPIClient/Core' cs.source_files = 'WGNAPIClient/WOT/**/*.{h,m}' end s.subspec 'WOWP' do |cs| cs.dependency 'WGNAPIClient/Core' cs.source_files = 'WGNAPIClient/WOWP/**/*.{h,m}' end
"" "" , :
pod 'WGNAPIClient/WOT' pod 'WGNAPIClient/WOWP'
. .
(98%). :
; .
:
context(@"API works with players data", ^{ it (@"should search players", ^{ stubResponse(@"/wot/account/list", @"players-search.json"); RACSignal *searchRequest = [client searchPlayers:@"garnett" limit:0]; NSArray *response = [searchRequest asynchronousFirstOrDefault:nil success:NULL error:&error]; [[error should] beNil]; [[response should] beKindOfClass:NSArray.class]; [[response.lastObject should] beKindOfClass:WOTSearchPlayer.class]; WOTSearchPlayer *player = response[0]; [[player.ID should] equal:@"1785514"]; }); });
, / , RACSignal , :
(id)asynchronousFirstOrDefault:(id)defaultValue success:(BOOL *)success error:(NSError **)error;
describe(@"WOTRatingType", ^{ it(@"should parse json to model object", ^{ NSDictionary *json = @{ @"threshold": @5, @"type": @"1", @"rank_fields": @[ @"xp_amount_rank", @"xp_max_rank", @"battles_count_rank", @"spotted_count_rank", @"damage_dealt_rank", ] }; NSError *error; WOTRatingType *ratingType = [MTLJSONAdapter modelOfClass:WOTRatingType.class fromJSONDictionary:json error:&error]; [[error should] beNil]; [[ratingType should] beKindOfClass:WOTRatingType.class]; [[json[@"threshold"] should] equal:theValue(ratingType.threshold)]; * [[json[@"type"] should] equal:ratingType.type]; [[ratingType.rankFields shouldNot] beNil]; [[ratingType.rankFields should] beKindOfClass:NSArray.class]; [[ratingType.rankFields should] haveCountOf:5]; [[@"maximumXP" should] equal:ratingType.rankFields[1]]; }); });
* — - - Kiwi, , nil .
API :
; .
, , : query http-. 
, ReactiveCocoa , ( ). ( ) 2k LOC ( ~1k - , ~700 - ), : — ..
Kiwi
BDD- RSpec - , . «--».
OHTTPStubs
, web- . "", - .
.
There are only two hard things in Computer Science: cache invalidation and naming things.
-- Phil Karlton
« » :
..
, .
- inMemory CoreData , NSCache . , , — . NSURLConnection .
NSURLCache
NSURLCache NSURLRequest . , .
:
NSURLCache *urlCache = [[NSURLCache alloc] initWithMemoryCapacity:kInMemoryCacheSize diskCapacity:kOnDiskCacheSize diskPath:path]; [NSURLCache setSharedURLCache:urlCache];
, .
NSURLConnectionDelegate , "" :
- (NSCachedURLResponse *)connection:(NSURLConnection *)connection willCacheResponse:(NSCachedURLResponse *)cachedResponse;
?
NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse*)[cachedResponse response]; NSDictionary *headers = [httpResponse allHeaderFields]; NSString *cacheControl = [headers valueForKey:@"Cache-Control"]; NSString *expires = [headers valueForKey:@"Expires"]; if((cacheControl == nil) && (expires == nil)) { NSMutableDictionary *modifiedHeaders = [httpResponse.allHeaderFields mutableCopy]; NSString *expireDate = [NSDate RFC822StringFromDate:[[NSDate date] dateByAddingTimeInterval:cacheTime]]; NSDictionary *values = @{@"Expires": expireDate, @"Cache-Control": @"public"}; [modifiedHeaders addEntriesFromDictionary:values]; NSHTTPURLResponse *response = [[NSHTTPURLResponse alloc] initWithURL:httpResponse.URL statusCode:httpResponse.statusCode HTTPVersion:@"HTTP/1.1" headerFields:modifiedHeaders]; NSCachedURLResponse *modifiedCachedResponse = [[NSCachedURLResponse alloc] initWithResponse:response data:cachedResponse.data userInfo:cachedResponse.userInfo storagePolicy:NSURLCacheStorageAllowed]; return modifiedCachedResponse; } return cachedResponse;
HTTP- .
, ( , , ). , ( cacheTime ) .
— ( ). , , .
:
(, , ..); - NSURLCache , .
:
30 ; ; : 200- , NSURLConnection .
""
API (, ), fields :
. . . , .
, Player , JSON-, . , fields , NSDictionary . , - . , .
JSON -> NSObject Mantle , API ( RACSignal API ):
- (RACSignal *)searchPlayers:(NSString *)query limit:(NSUInteger)limit { NSMutableDictionary *parameters = [NSMutableDictionary dictionary]; parameters[@"search"] = query; if (limit) parameters[@"limit"] = @(limit); return [self getPath:@"account/list" parameters:parameters resultClass:WOTSearchPlayer.class]; }
, resultClass , ? :
- (RACSignal *)searchPlayers:(NSString *)query limit:(NSUInteger)limit resultClass:(Class)resultClass
, API, .
API , : , , "" . ( ). , ( AFJSONRequestOperation ):
op1 = [AFJSONRequestOperation JSONRequestOperationWithRequest:nil success:^(id JSON) { op2 = [AFJSONRequestOperation JSONRequestOperationWithRequest:nil success:^(id JSON) { op3 = [AFJSONRequestOperation JSONRequestOperationWithRequest:nil success:^(id JSON) { // Combine all the responses and return } failure:^(NSError *error) { // Handle error }]; } failure:^(NSError *error) { // Handle error }]; } failure:^(NSError *error) { // Handle error }];
, , . ReactiveCocoa , .
API :
- (RACSignal *)searchPlayers:(NSString *)query limit:(NSUInteger)limit; - (RACSignal *)fetchPlayers:(NSArray *)playerIDs;
RACSignal — , , . — , . / :
RACSignal *fetchPlayersAndRatings = [[[[API searchPlayers:@"" limit:0] flattenMap:^RACStream *(id players) { // skipping data processing return [API fetchPlayers:@[]]; }] flattenMap:^RACStream *(id fullPlayers) { // Skipping data processing return [API statsSliceForPlayer:nil hoursAgo:@[] fields:nil]; }] flattenMap:^RACStream *(id value) { // Compose all responses here id composedResponse; return [RACSignal return:composedResponse]; }]; [fetchPlayersAndRatings subscribeNext:^(id x) { // Fully composed response } error:^(NSError *error) { // All erros go to one place }];
ReactiveCocoa — , . , Promises Futures.
:
Core ( , ); WOT ( World of Tanks–); WOWP ( World of Warplanes–)
, , — . , ( ) CocoaPods , . subspecs , :
# Subspecs s.subspec 'Core' do |cs| cs.source_files = 'WGNAPIClient/Core/**/*.{h,m}' end s.subspec 'WOT' do |cs| cs.dependency 'WGNAPIClient/Core' cs.source_files = 'WGNAPIClient/WOT/**/*.{h,m}' end s.subspec 'WOWP' do |cs| cs.dependency 'WGNAPIClient/Core' cs.source_files = 'WGNAPIClient/WOWP/**/*.{h,m}' end
"" "" , :
pod 'WGNAPIClient/WOT' pod 'WGNAPIClient/WOWP'
. .
(98%). :
; .
:
context(@"API works with players data", ^{ it (@"should search players", ^{ stubResponse(@"/wot/account/list", @"players-search.json"); RACSignal *searchRequest = [client searchPlayers:@"garnett" limit:0]; NSArray *response = [searchRequest asynchronousFirstOrDefault:nil success:NULL error:&error]; [[error should] beNil]; [[response should] beKindOfClass:NSArray.class]; [[response.lastObject should] beKindOfClass:WOTSearchPlayer.class]; WOTSearchPlayer *player = response[0]; [[player.ID should] equal:@"1785514"]; }); });
, / , RACSignal , :
(id)asynchronousFirstOrDefault:(id)defaultValue success:(BOOL *)success error:(NSError **)error;
describe(@"WOTRatingType", ^{ it(@"should parse json to model object", ^{ NSDictionary *json = @{ @"threshold": @5, @"type": @"1", @"rank_fields": @[ @"xp_amount_rank", @"xp_max_rank", @"battles_count_rank", @"spotted_count_rank", @"damage_dealt_rank", ] }; NSError *error; WOTRatingType *ratingType = [MTLJSONAdapter modelOfClass:WOTRatingType.class fromJSONDictionary:json error:&error]; [[error should] beNil]; [[ratingType should] beKindOfClass:WOTRatingType.class]; [[json[@"threshold"] should] equal:theValue(ratingType.threshold)]; * [[json[@"type"] should] equal:ratingType.type]; [[ratingType.rankFields shouldNot] beNil]; [[ratingType.rankFields should] beKindOfClass:NSArray.class]; [[ratingType.rankFields should] haveCountOf:5]; [[@"maximumXP" should] equal:ratingType.rankFields[1]]; }); });
* — - - Kiwi, , nil .
API :
; .
, , : query http-. 
, ReactiveCocoa , ( ). ( ) 2k LOC ( ~1k - , ~700 - ), : — ..
Kiwi
BDD- RSpec - , . «--».
OHTTPStubs
, web- . "", - .
.
There are only two hard things in Computer Science: cache invalidation and naming things.
-- Phil Karlton
« » :
..
, .
- inMemory CoreData , NSCache . , , — . NSURLConnection .
NSURLCache
NSURLCache NSURLRequest . , .
:
NSURLCache *urlCache = [[NSURLCache alloc] initWithMemoryCapacity:kInMemoryCacheSize diskCapacity:kOnDiskCacheSize diskPath:path]; [NSURLCache setSharedURLCache:urlCache];
, .
NSURLConnectionDelegate , "" :
- (NSCachedURLResponse *)connection:(NSURLConnection *)connection willCacheResponse:(NSCachedURLResponse *)cachedResponse;
?
NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse*)[cachedResponse response]; NSDictionary *headers = [httpResponse allHeaderFields]; NSString *cacheControl = [headers valueForKey:@"Cache-Control"]; NSString *expires = [headers valueForKey:@"Expires"]; if((cacheControl == nil) && (expires == nil)) { NSMutableDictionary *modifiedHeaders = [httpResponse.allHeaderFields mutableCopy]; NSString *expireDate = [NSDate RFC822StringFromDate:[[NSDate date] dateByAddingTimeInterval:cacheTime]]; NSDictionary *values = @{@"Expires": expireDate, @"Cache-Control": @"public"}; [modifiedHeaders addEntriesFromDictionary:values]; NSHTTPURLResponse *response = [[NSHTTPURLResponse alloc] initWithURL:httpResponse.URL statusCode:httpResponse.statusCode HTTPVersion:@"HTTP/1.1" headerFields:modifiedHeaders]; NSCachedURLResponse *modifiedCachedResponse = [[NSCachedURLResponse alloc] initWithResponse:response data:cachedResponse.data userInfo:cachedResponse.userInfo storagePolicy:NSURLCacheStorageAllowed]; return modifiedCachedResponse; } return cachedResponse;
HTTP- .
, ( , , ). , ( cacheTime ) .
— ( ). , , .
:
(, , ..); - NSURLCache , .
:
30 ; ; : 200- , NSURLConnection .
""
API (, ), fields :
. . . , .
, Player , JSON-, . , fields , NSDictionary . , - . , .
JSON -> NSObject Mantle , API ( RACSignal API ):
- (RACSignal *)searchPlayers:(NSString *)query limit:(NSUInteger)limit { NSMutableDictionary *parameters = [NSMutableDictionary dictionary]; parameters[@"search"] = query; if (limit) parameters[@"limit"] = @(limit); return [self getPath:@"account/list" parameters:parameters resultClass:WOTSearchPlayer.class]; }
, resultClass , ? :
- (RACSignal *)searchPlayers:(NSString *)query limit:(NSUInteger)limit resultClass:(Class)resultClass
, API, .
API , : , , "" . ( ). , ( AFJSONRequestOperation ):
op1 = [AFJSONRequestOperation JSONRequestOperationWithRequest:nil success:^(id JSON) { op2 = [AFJSONRequestOperation JSONRequestOperationWithRequest:nil success:^(id JSON) { op3 = [AFJSONRequestOperation JSONRequestOperationWithRequest:nil success:^(id JSON) { // Combine all the responses and return } failure:^(NSError *error) { // Handle error }]; } failure:^(NSError *error) { // Handle error }]; } failure:^(NSError *error) { // Handle error }];
, , . ReactiveCocoa , .
API :
- (RACSignal *)searchPlayers:(NSString *)query limit:(NSUInteger)limit; - (RACSignal *)fetchPlayers:(NSArray *)playerIDs;
RACSignal — , , . — , . / :
RACSignal *fetchPlayersAndRatings = [[[[API searchPlayers:@"" limit:0] flattenMap:^RACStream *(id players) { // skipping data processing return [API fetchPlayers:@[]]; }] flattenMap:^RACStream *(id fullPlayers) { // Skipping data processing return [API statsSliceForPlayer:nil hoursAgo:@[] fields:nil]; }] flattenMap:^RACStream *(id value) { // Compose all responses here id composedResponse; return [RACSignal return:composedResponse]; }]; [fetchPlayersAndRatings subscribeNext:^(id x) { // Fully composed response } error:^(NSError *error) { // All erros go to one place }];
ReactiveCocoa — , . , Promises Futures.
:
Core ( , ); WOT ( World of Tanks–); WOWP ( World of Warplanes–)
, , — . , ( ) CocoaPods , . subspecs , :
# Subspecs s.subspec 'Core' do |cs| cs.source_files = 'WGNAPIClient/Core/**/*.{h,m}' end s.subspec 'WOT' do |cs| cs.dependency 'WGNAPIClient/Core' cs.source_files = 'WGNAPIClient/WOT/**/*.{h,m}' end s.subspec 'WOWP' do |cs| cs.dependency 'WGNAPIClient/Core' cs.source_files = 'WGNAPIClient/WOWP/**/*.{h,m}' end
"" "" , :
pod 'WGNAPIClient/WOT' pod 'WGNAPIClient/WOWP'
. .
(98%). :
; .
:
context(@"API works with players data", ^{ it (@"should search players", ^{ stubResponse(@"/wot/account/list", @"players-search.json"); RACSignal *searchRequest = [client searchPlayers:@"garnett" limit:0]; NSArray *response = [searchRequest asynchronousFirstOrDefault:nil success:NULL error:&error]; [[error should] beNil]; [[response should] beKindOfClass:NSArray.class]; [[response.lastObject should] beKindOfClass:WOTSearchPlayer.class]; WOTSearchPlayer *player = response[0]; [[player.ID should] equal:@"1785514"]; }); });
, / , RACSignal , :
(id)asynchronousFirstOrDefault:(id)defaultValue success:(BOOL *)success error:(NSError **)error;
describe(@"WOTRatingType", ^{ it(@"should parse json to model object", ^{ NSDictionary *json = @{ @"threshold": @5, @"type": @"1", @"rank_fields": @[ @"xp_amount_rank", @"xp_max_rank", @"battles_count_rank", @"spotted_count_rank", @"damage_dealt_rank", ] }; NSError *error; WOTRatingType *ratingType = [MTLJSONAdapter modelOfClass:WOTRatingType.class fromJSONDictionary:json error:&error]; [[error should] beNil]; [[ratingType should] beKindOfClass:WOTRatingType.class]; [[json[@"threshold"] should] equal:theValue(ratingType.threshold)]; * [[json[@"type"] should] equal:ratingType.type]; [[ratingType.rankFields shouldNot] beNil]; [[ratingType.rankFields should] beKindOfClass:NSArray.class]; [[ratingType.rankFields should] haveCountOf:5]; [[@"maximumXP" should] equal:ratingType.rankFields[1]]; }); });
* — - - Kiwi, , nil .
API :
; .
, , : query http-. 
, ReactiveCocoa , ( ). ( ) 2k LOC ( ~1k - , ~700 - ), : — ..
Kiwi
BDD- RSpec - , . «--».
OHTTPStubs
, web- . "", - .
.
There are only two hard things in Computer Science: cache invalidation and naming things.
-- Phil Karlton
« » :
..
, .
- inMemory CoreData , NSCache . , , — . NSURLConnection .
NSURLCache
NSURLCache NSURLRequest . , .
:
NSURLCache *urlCache = [[NSURLCache alloc] initWithMemoryCapacity:kInMemoryCacheSize diskCapacity:kOnDiskCacheSize diskPath:path]; [NSURLCache setSharedURLCache:urlCache];
, .
NSURLConnectionDelegate , "" :
- (NSCachedURLResponse *)connection:(NSURLConnection *)connection willCacheResponse:(NSCachedURLResponse *)cachedResponse;
?
NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse*)[cachedResponse response]; NSDictionary *headers = [httpResponse allHeaderFields]; NSString *cacheControl = [headers valueForKey:@"Cache-Control"]; NSString *expires = [headers valueForKey:@"Expires"]; if((cacheControl == nil) && (expires == nil)) { NSMutableDictionary *modifiedHeaders = [httpResponse.allHeaderFields mutableCopy]; NSString *expireDate = [NSDate RFC822StringFromDate:[[NSDate date] dateByAddingTimeInterval:cacheTime]]; NSDictionary *values = @{@"Expires": expireDate, @"Cache-Control": @"public"}; [modifiedHeaders addEntriesFromDictionary:values]; NSHTTPURLResponse *response = [[NSHTTPURLResponse alloc] initWithURL:httpResponse.URL statusCode:httpResponse.statusCode HTTPVersion:@"HTTP/1.1" headerFields:modifiedHeaders]; NSCachedURLResponse *modifiedCachedResponse = [[NSCachedURLResponse alloc] initWithResponse:response data:cachedResponse.data userInfo:cachedResponse.userInfo storagePolicy:NSURLCacheStorageAllowed]; return modifiedCachedResponse; } return cachedResponse;
HTTP- .
, ( , , ). , ( cacheTime ) .
— ( ). , , .
:
(, , ..); - NSURLCache , .
:
30 ; ; : 200- , NSURLConnection .
""
API (, ), fields :
. . . , .
, Player , JSON-, . , fields , NSDictionary . , - . , .
JSON -> NSObject Mantle , API ( RACSignal API ):
- (RACSignal *)searchPlayers:(NSString *)query limit:(NSUInteger)limit { NSMutableDictionary *parameters = [NSMutableDictionary dictionary]; parameters[@"search"] = query; if (limit) parameters[@"limit"] = @(limit); return [self getPath:@"account/list" parameters:parameters resultClass:WOTSearchPlayer.class]; }
, resultClass , ? :
- (RACSignal *)searchPlayers:(NSString *)query limit:(NSUInteger)limit resultClass:(Class)resultClass
, API, .
API , : , , "" . ( ). , ( AFJSONRequestOperation ):
op1 = [AFJSONRequestOperation JSONRequestOperationWithRequest:nil success:^(id JSON) { op2 = [AFJSONRequestOperation JSONRequestOperationWithRequest:nil success:^(id JSON) { op3 = [AFJSONRequestOperation JSONRequestOperationWithRequest:nil success:^(id JSON) { // Combine all the responses and return } failure:^(NSError *error) { // Handle error }]; } failure:^(NSError *error) { // Handle error }]; } failure:^(NSError *error) { // Handle error }];
, , . ReactiveCocoa , .
API :
- (RACSignal *)searchPlayers:(NSString *)query limit:(NSUInteger)limit; - (RACSignal *)fetchPlayers:(NSArray *)playerIDs;
RACSignal — , , . — , . / :
RACSignal *fetchPlayersAndRatings = [[[[API searchPlayers:@"" limit:0] flattenMap:^RACStream *(id players) { // skipping data processing return [API fetchPlayers:@[]]; }] flattenMap:^RACStream *(id fullPlayers) { // Skipping data processing return [API statsSliceForPlayer:nil hoursAgo:@[] fields:nil]; }] flattenMap:^RACStream *(id value) { // Compose all responses here id composedResponse; return [RACSignal return:composedResponse]; }]; [fetchPlayersAndRatings subscribeNext:^(id x) { // Fully composed response } error:^(NSError *error) { // All erros go to one place }];
ReactiveCocoa — , . , Promises Futures.
:
Core ( , ); WOT ( World of Tanks–); WOWP ( World of Warplanes–)
, , — . , ( ) CocoaPods , . subspecs , :
# Subspecs s.subspec 'Core' do |cs| cs.source_files = 'WGNAPIClient/Core/**/*.{h,m}' end s.subspec 'WOT' do |cs| cs.dependency 'WGNAPIClient/Core' cs.source_files = 'WGNAPIClient/WOT/**/*.{h,m}' end s.subspec 'WOWP' do |cs| cs.dependency 'WGNAPIClient/Core' cs.source_files = 'WGNAPIClient/WOWP/**/*.{h,m}' end
"" "" , :
pod 'WGNAPIClient/WOT' pod 'WGNAPIClient/WOWP'
. .
(98%). :
; .
:
context(@"API works with players data", ^{ it (@"should search players", ^{ stubResponse(@"/wot/account/list", @"players-search.json"); RACSignal *searchRequest = [client searchPlayers:@"garnett" limit:0]; NSArray *response = [searchRequest asynchronousFirstOrDefault:nil success:NULL error:&error]; [[error should] beNil]; [[response should] beKindOfClass:NSArray.class]; [[response.lastObject should] beKindOfClass:WOTSearchPlayer.class]; WOTSearchPlayer *player = response[0]; [[player.ID should] equal:@"1785514"]; }); });
, / , RACSignal , :
(id)asynchronousFirstOrDefault:(id)defaultValue success:(BOOL *)success error:(NSError **)error;
describe(@"WOTRatingType", ^{ it(@"should parse json to model object", ^{ NSDictionary *json = @{ @"threshold": @5, @"type": @"1", @"rank_fields": @[ @"xp_amount_rank", @"xp_max_rank", @"battles_count_rank", @"spotted_count_rank", @"damage_dealt_rank", ] }; NSError *error; WOTRatingType *ratingType = [MTLJSONAdapter modelOfClass:WOTRatingType.class fromJSONDictionary:json error:&error]; [[error should] beNil]; [[ratingType should] beKindOfClass:WOTRatingType.class]; [[json[@"threshold"] should] equal:theValue(ratingType.threshold)]; * [[json[@"type"] should] equal:ratingType.type]; [[ratingType.rankFields shouldNot] beNil]; [[ratingType.rankFields should] beKindOfClass:NSArray.class]; [[ratingType.rankFields should] haveCountOf:5]; [[@"maximumXP" should] equal:ratingType.rankFields[1]]; }); });
* — - - Kiwi, , nil .
API :
; .
, , : query http-. 
, ReactiveCocoa , ( ). ( ) 2k LOC ( ~1k - , ~700 - ), : — .     . 
 
 Kiwi 
  BDD-    RSpec -  ,   .   «--». 
 
 OHTTPStubs 
   ,      web-   .      "",      - . 
 
     . 
 
    
 There are only two hard things in Computer Science: cache invalidation and naming things. 
 
 -- Phil Karlton 
  « »  : 
 
                .. 
 ,            . 
       -     inMemory CoreData ,       NSCache .   ,       ,         —         .         NSURLConnection . 
 
 NSURLCache 
 NSURLCache        NSURLRequest   .      ,    . 
 
   : 
 NSURLCache *urlCache = [[NSURLCache alloc] initWithMemoryCapacity:kInMemoryCacheSize diskCapacity:kOnDiskCacheSize diskPath:path]; [NSURLCache setSharedURLCache:urlCache]; 
     ,           . 
 
  NSURLConnectionDelegate   ,     ""    : 
 - (NSCachedURLResponse *)connection:(NSURLConnection *)connection willCacheResponse:(NSCachedURLResponse *)cachedResponse; 
    ? 
 
  NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse*)[cachedResponse response]; NSDictionary *headers = [httpResponse allHeaderFields]; NSString *cacheControl = [headers valueForKey:@"Cache-Control"]; NSString *expires = [headers valueForKey:@"Expires"]; if((cacheControl == nil) && (expires == nil)) { NSMutableDictionary *modifiedHeaders = [httpResponse.allHeaderFields mutableCopy]; NSString *expireDate = [NSDate RFC822StringFromDate:[[NSDate date] dateByAddingTimeInterval:cacheTime]]; NSDictionary *values = @{@"Expires": expireDate, @"Cache-Control": @"public"}; [modifiedHeaders addEntriesFromDictionary:values]; NSHTTPURLResponse *response = [[NSHTTPURLResponse alloc] initWithURL:httpResponse.URL statusCode:httpResponse.statusCode HTTPVersion:@"HTTP/1.1" headerFields:modifiedHeaders]; NSCachedURLResponse *modifiedCachedResponse = [[NSCachedURLResponse alloc] initWithResponse:response data:cachedResponse.data userInfo:cachedResponse.userInfo storagePolicy:NSURLCacheStorageAllowed]; return modifiedCachedResponse; } return cachedResponse; 
      HTTP-      . 
 
 ,     (    ,    ,            ).   ,     ( cacheTime )   . 
     —        (      ).    ,      ,    . 
 
 : 
 
        (, ,   ..);  -   NSURLCache ,   . 
 : 
     30  ;       ; :         200- , NSURLConnection   . 
  ""  
          API (,    ),     fields : 
 
   .   .    .    ,   . 
  ,   Player ,      JSON-,   .        ,       fields ,        NSDictionary .         ,  -       .          ,           . 
   JSON -> NSObject    Mantle ,     API      ( RACSignal   API      ): 
 
 - (RACSignal *)searchPlayers:(NSString *)query limit:(NSUInteger)limit { NSMutableDictionary *parameters = [NSMutableDictionary dictionary]; parameters[@"search"] = query; if (limit) parameters[@"limit"] = @(limit); return [self getPath:@"account/list" parameters:parameters resultClass:WOTSearchPlayer.class]; } 
  ,       resultClass ,         ? : 
 
  - (RACSignal *)searchPlayers:(NSString *)query limit:(NSUInteger)limit resultClass:(Class)resultClass 
 ,     API,           . 
 
       
     API     ,               : ,   ,     ""    . (    ).    ,       (   AFJSONRequestOperation ): 
 
  op1 = [AFJSONRequestOperation JSONRequestOperationWithRequest:nil success:^(id JSON) { op2 = [AFJSONRequestOperation JSONRequestOperationWithRequest:nil success:^(id JSON) { op3 = [AFJSONRequestOperation JSONRequestOperationWithRequest:nil success:^(id JSON) { // Combine all the responses and return } failure:^(NSError *error) { // Handle error }]; } failure:^(NSError *error) { // Handle error }]; } failure:^(NSError *error) { // Handle error }]; 
  ,    ,          .         ReactiveCocoa  ,       . 
  API    : 
 
 - (RACSignal *)searchPlayers:(NSString *)query limit:(NSUInteger)limit; - (RACSignal *)fetchPlayers:(NSArray *)playerIDs; 
 RACSignal —  ,       ,     .    —       ,    .          / : 
 
  RACSignal *fetchPlayersAndRatings = [[[[API searchPlayers:@"" limit:0] flattenMap:^RACStream *(id players) { // skipping data processing return [API fetchPlayers:@[]]; }] flattenMap:^RACStream *(id fullPlayers) { // Skipping data processing return [API statsSliceForPlayer:nil hoursAgo:@[] fields:nil]; }] flattenMap:^RACStream *(id value) { // Compose all responses here id composedResponse; return [RACSignal return:composedResponse]; }]; [fetchPlayersAndRatings subscribeNext:^(id x) { // Fully composed response } error:^(NSError *error) { // All erros go to one place }]; 
  ReactiveCocoa   —      ,      .        ,         Promises  Futures. 
 
      
        : 
 Core ( , ); WOT (    World of Tanks–); WOWP (    World of Warplanes–) 
  ,         ,      — . ,    (    )       CocoaPods ,       .       subspecs ,        : 
 
  # Subspecs s.subspec 'Core' do |cs| cs.source_files = 'WGNAPIClient/Core/**/*.{h,m}' end s.subspec 'WOT' do |cs| cs.dependency 'WGNAPIClient/Core' cs.source_files = 'WGNAPIClient/WOT/**/*.{h,m}' end s.subspec 'WOWP' do |cs| cs.dependency 'WGNAPIClient/Core' cs.source_files = 'WGNAPIClient/WOWP/**/*.{h,m}' end 
 
    ""  ""  ,      : 
 pod 'WGNAPIClient/WOT' pod 'WGNAPIClient/WOWP' 
     
              .        . 
         (98%).           : 
 
   ;    . 
   
     : 
  context(@"API works with players data", ^{ it (@"should search players", ^{ stubResponse(@"/wot/account/list", @"players-search.json"); RACSignal *searchRequest = [client searchPlayers:@"garnett" limit:0]; NSArray *response = [searchRequest asynchronousFirstOrDefault:nil success:NULL error:&error]; [[error should] beNil]; [[response should] beKindOfClass:NSArray.class]; [[response.lastObject should] beKindOfClass:WOTSearchPlayer.class]; WOTSearchPlayer *player = response[0]; [[player.ID should] equal:@"1785514"]; }); }); 
 ,     / ,    RACSignal      ,       : 
 (id)asynchronousFirstOrDefault:(id)defaultValue success:(BOOL *)success error:(NSError **)error; 
   
 describe(@"WOTRatingType", ^{ it(@"should parse json to model object", ^{ NSDictionary *json = @{ @"threshold": @5, @"type": @"1", @"rank_fields": @[ @"xp_amount_rank", @"xp_max_rank", @"battles_count_rank", @"spotted_count_rank", @"damage_dealt_rank", ] }; NSError *error; WOTRatingType *ratingType = [MTLJSONAdapter modelOfClass:WOTRatingType.class fromJSONDictionary:json error:&error]; [[error should] beNil]; [[ratingType should] beKindOfClass:WOTRatingType.class]; [[json[@"threshold"] should] equal:theValue(ratingType.threshold)]; * [[json[@"type"] should] equal:ratingType.type]; [[ratingType.rankFields shouldNot] beNil]; [[ratingType.rankFields should] beKindOfClass:NSArray.class]; [[ratingType.rankFields should] haveCountOf:5]; [[@"maximumXP" should] equal:ratingType.rankFields[1]]; }); }); 
 
 * —    - -    Kiwi,      ,     nil . 
 
       API    : 
    ;   . 
  ,  ,       :  query  http-.   
 
  
    ,      ReactiveCocoa ,         (    ).    ( )   2k LOC (  ~1k -  ,   ~700 -     ),     :        —         . .
Kiwi
BDD- RSpec - , . «--».
OHTTPStubs
, web- . "", - .
.
There are only two hard things in Computer Science: cache invalidation and naming things.
-- Phil Karlton
« » :
..
, .
- inMemory CoreData , NSCache . , , — . NSURLConnection .
NSURLCache
NSURLCache NSURLRequest . , .
:
NSURLCache *urlCache = [[NSURLCache alloc] initWithMemoryCapacity:kInMemoryCacheSize diskCapacity:kOnDiskCacheSize diskPath:path]; [NSURLCache setSharedURLCache:urlCache];
, .
NSURLConnectionDelegate , "" :
- (NSCachedURLResponse *)connection:(NSURLConnection *)connection willCacheResponse:(NSCachedURLResponse *)cachedResponse;
?
NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse*)[cachedResponse response]; NSDictionary *headers = [httpResponse allHeaderFields]; NSString *cacheControl = [headers valueForKey:@"Cache-Control"]; NSString *expires = [headers valueForKey:@"Expires"]; if((cacheControl == nil) && (expires == nil)) { NSMutableDictionary *modifiedHeaders = [httpResponse.allHeaderFields mutableCopy]; NSString *expireDate = [NSDate RFC822StringFromDate:[[NSDate date] dateByAddingTimeInterval:cacheTime]]; NSDictionary *values = @{@"Expires": expireDate, @"Cache-Control": @"public"}; [modifiedHeaders addEntriesFromDictionary:values]; NSHTTPURLResponse *response = [[NSHTTPURLResponse alloc] initWithURL:httpResponse.URL statusCode:httpResponse.statusCode HTTPVersion:@"HTTP/1.1" headerFields:modifiedHeaders]; NSCachedURLResponse *modifiedCachedResponse = [[NSCachedURLResponse alloc] initWithResponse:response data:cachedResponse.data userInfo:cachedResponse.userInfo storagePolicy:NSURLCacheStorageAllowed]; return modifiedCachedResponse; } return cachedResponse;
HTTP- .
, ( , , ). , ( cacheTime ) .
— ( ). , , .
:
(, , ..); - NSURLCache , .
:
30 ; ; : 200- , NSURLConnection .
""
API (, ), fields :
. . . , .
, Player , JSON-, . , fields , NSDictionary . , - . , .
JSON -> NSObject Mantle , API ( RACSignal API ):
- (RACSignal *)searchPlayers:(NSString *)query limit:(NSUInteger)limit { NSMutableDictionary *parameters = [NSMutableDictionary dictionary]; parameters[@"search"] = query; if (limit) parameters[@"limit"] = @(limit); return [self getPath:@"account/list" parameters:parameters resultClass:WOTSearchPlayer.class]; }
, resultClass , ? :
- (RACSignal *)searchPlayers:(NSString *)query limit:(NSUInteger)limit resultClass:(Class)resultClass
, API, .
API , : , , "" . ( ). , ( AFJSONRequestOperation ):
op1 = [AFJSONRequestOperation JSONRequestOperationWithRequest:nil success:^(id JSON) { op2 = [AFJSONRequestOperation JSONRequestOperationWithRequest:nil success:^(id JSON) { op3 = [AFJSONRequestOperation JSONRequestOperationWithRequest:nil success:^(id JSON) { // Combine all the responses and return } failure:^(NSError *error) { // Handle error }]; } failure:^(NSError *error) { // Handle error }]; } failure:^(NSError *error) { // Handle error }];
, , . ReactiveCocoa , .
API :
- (RACSignal *)searchPlayers:(NSString *)query limit:(NSUInteger)limit; - (RACSignal *)fetchPlayers:(NSArray *)playerIDs;
RACSignal — , , . — , . / :
RACSignal *fetchPlayersAndRatings = [[[[API searchPlayers:@"" limit:0] flattenMap:^RACStream *(id players) { // skipping data processing return [API fetchPlayers:@[]]; }] flattenMap:^RACStream *(id fullPlayers) { // Skipping data processing return [API statsSliceForPlayer:nil hoursAgo:@[] fields:nil]; }] flattenMap:^RACStream *(id value) { // Compose all responses here id composedResponse; return [RACSignal return:composedResponse]; }]; [fetchPlayersAndRatings subscribeNext:^(id x) { // Fully composed response } error:^(NSError *error) { // All erros go to one place }];
ReactiveCocoa — , . , Promises Futures.
:
Core ( , ); WOT ( World of Tanks–); WOWP ( World of Warplanes–)
, , — . , ( ) CocoaPods , . subspecs , :
# Subspecs s.subspec 'Core' do |cs| cs.source_files = 'WGNAPIClient/Core/**/*.{h,m}' end s.subspec 'WOT' do |cs| cs.dependency 'WGNAPIClient/Core' cs.source_files = 'WGNAPIClient/WOT/**/*.{h,m}' end s.subspec 'WOWP' do |cs| cs.dependency 'WGNAPIClient/Core' cs.source_files = 'WGNAPIClient/WOWP/**/*.{h,m}' end
"" "" , :
pod 'WGNAPIClient/WOT' pod 'WGNAPIClient/WOWP'
. .
(98%). :
; .
:
context(@"API works with players data", ^{ it (@"should search players", ^{ stubResponse(@"/wot/account/list", @"players-search.json"); RACSignal *searchRequest = [client searchPlayers:@"garnett" limit:0]; NSArray *response = [searchRequest asynchronousFirstOrDefault:nil success:NULL error:&error]; [[error should] beNil]; [[response should] beKindOfClass:NSArray.class]; [[response.lastObject should] beKindOfClass:WOTSearchPlayer.class]; WOTSearchPlayer *player = response[0]; [[player.ID should] equal:@"1785514"]; }); });
, / , RACSignal , :
(id)asynchronousFirstOrDefault:(id)defaultValue success:(BOOL *)success error:(NSError **)error;
describe(@"WOTRatingType", ^{ it(@"should parse json to model object", ^{ NSDictionary *json = @{ @"threshold": @5, @"type": @"1", @"rank_fields": @[ @"xp_amount_rank", @"xp_max_rank", @"battles_count_rank", @"spotted_count_rank", @"damage_dealt_rank", ] }; NSError *error; WOTRatingType *ratingType = [MTLJSONAdapter modelOfClass:WOTRatingType.class fromJSONDictionary:json error:&error]; [[error should] beNil]; [[ratingType should] beKindOfClass:WOTRatingType.class]; [[json[@"threshold"] should] equal:theValue(ratingType.threshold)]; * [[json[@"type"] should] equal:ratingType.type]; [[ratingType.rankFields shouldNot] beNil]; [[ratingType.rankFields should] beKindOfClass:NSArray.class]; [[ratingType.rankFields should] haveCountOf:5]; [[@"maximumXP" should] equal:ratingType.rankFields[1]]; }); });
* — - - Kiwi, , nil .
API :
; .
, , : query http-. 
, ReactiveCocoa , ( ). ( ) 2k LOC ( ~1k - , ~700 - ), : — .     . 
 
 Kiwi 
  BDD-    RSpec -  ,   .   «--». 
 
 OHTTPStubs 
   ,      web-   .      "",      - . 
 
     . 
 
    
 There are only two hard things in Computer Science: cache invalidation and naming things. 
 
 -- Phil Karlton 
  « »  : 
 
                .. 
 ,            . 
       -     inMemory CoreData ,       NSCache .   ,       ,         —         .         NSURLConnection . 
 
 NSURLCache 
 NSURLCache        NSURLRequest   .      ,    . 
 
   : 
 NSURLCache *urlCache = [[NSURLCache alloc] initWithMemoryCapacity:kInMemoryCacheSize diskCapacity:kOnDiskCacheSize diskPath:path]; [NSURLCache setSharedURLCache:urlCache]; 
     ,           . 
 
  NSURLConnectionDelegate   ,     ""    : 
 - (NSCachedURLResponse *)connection:(NSURLConnection *)connection willCacheResponse:(NSCachedURLResponse *)cachedResponse; 
    ? 
 
  NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse*)[cachedResponse response]; NSDictionary *headers = [httpResponse allHeaderFields]; NSString *cacheControl = [headers valueForKey:@"Cache-Control"]; NSString *expires = [headers valueForKey:@"Expires"]; if((cacheControl == nil) && (expires == nil)) { NSMutableDictionary *modifiedHeaders = [httpResponse.allHeaderFields mutableCopy]; NSString *expireDate = [NSDate RFC822StringFromDate:[[NSDate date] dateByAddingTimeInterval:cacheTime]]; NSDictionary *values = @{@"Expires": expireDate, @"Cache-Control": @"public"}; [modifiedHeaders addEntriesFromDictionary:values]; NSHTTPURLResponse *response = [[NSHTTPURLResponse alloc] initWithURL:httpResponse.URL statusCode:httpResponse.statusCode HTTPVersion:@"HTTP/1.1" headerFields:modifiedHeaders]; NSCachedURLResponse *modifiedCachedResponse = [[NSCachedURLResponse alloc] initWithResponse:response data:cachedResponse.data userInfo:cachedResponse.userInfo storagePolicy:NSURLCacheStorageAllowed]; return modifiedCachedResponse; } return cachedResponse; 
      HTTP-      . 
 
 ,     (    ,    ,            ).   ,     ( cacheTime )   . 
     —        (      ).    ,      ,    . 
 
 : 
 
        (, ,   ..);  -   NSURLCache ,   . 
 : 
     30  ;       ; :         200- , NSURLConnection   . 
  ""  
          API (,    ),     fields : 
 
   .   .    .    ,   . 
  ,   Player ,      JSON-,   .        ,       fields ,        NSDictionary .         ,  -       .          ,           . 
   JSON -> NSObject    Mantle ,     API      ( RACSignal   API      ): 
 
 - (RACSignal *)searchPlayers:(NSString *)query limit:(NSUInteger)limit { NSMutableDictionary *parameters = [NSMutableDictionary dictionary]; parameters[@"search"] = query; if (limit) parameters[@"limit"] = @(limit); return [self getPath:@"account/list" parameters:parameters resultClass:WOTSearchPlayer.class]; } 
  ,       resultClass ,         ? : 
 
 - (RACSignal *)searchPlayers:(NSString *)query limit:(NSUInteger)limit resultClass:(Class)resultClass 
 ,     API,           . 
 
       
     API     ,               : ,   ,     ""    . (    ).    ,       (   AFJSONRequestOperation ): 
 
  op1 = [AFJSONRequestOperation JSONRequestOperationWithRequest:nil success:^(id JSON) { op2 = [AFJSONRequestOperation JSONRequestOperationWithRequest:nil success:^(id JSON) { op3 = [AFJSONRequestOperation JSONRequestOperationWithRequest:nil success:^(id JSON) { // Combine all the responses and return } failure:^(NSError *error) { // Handle error }]; } failure:^(NSError *error) { // Handle error }]; } failure:^(NSError *error) { // Handle error }]; 
  ,    ,          .         ReactiveCocoa  ,       . 
  API    : 
 
 - (RACSignal *)searchPlayers:(NSString *)query limit:(NSUInteger)limit; - (RACSignal *)fetchPlayers:(NSArray *)playerIDs; 
 RACSignal —  ,       ,     .    —       ,    .          / : 
 
  RACSignal *fetchPlayersAndRatings = [[[[API searchPlayers:@"" limit:0] flattenMap:^RACStream *(id players) { // skipping data processing return [API fetchPlayers:@[]]; }] flattenMap:^RACStream *(id fullPlayers) { // Skipping data processing return [API statsSliceForPlayer:nil hoursAgo:@[] fields:nil]; }] flattenMap:^RACStream *(id value) { // Compose all responses here id composedResponse; return [RACSignal return:composedResponse]; }]; [fetchPlayersAndRatings subscribeNext:^(id x) { // Fully composed response } error:^(NSError *error) { // All erros go to one place }]; 
  ReactiveCocoa   —      ,      .        ,         Promises  Futures. 
 
      
        : 
 Core ( , ); WOT (    World of Tanks–); WOWP (    World of Warplanes–) 
  ,         ,      — . ,    (    )       CocoaPods ,       .       subspecs ,        : 
 
  # Subspecs s.subspec 'Core' do |cs| cs.source_files = 'WGNAPIClient/Core/**/*.{h,m}' end s.subspec 'WOT' do |cs| cs.dependency 'WGNAPIClient/Core' cs.source_files = 'WGNAPIClient/WOT/**/*.{h,m}' end s.subspec 'WOWP' do |cs| cs.dependency 'WGNAPIClient/Core' cs.source_files = 'WGNAPIClient/WOWP/**/*.{h,m}' end 
 
    ""  ""  ,      : 
 pod 'WGNAPIClient/WOT' pod 'WGNAPIClient/WOWP' 
     
              .        . 
         (98%).           : 
 
   ;    . 
   
     : 
  context(@"API works with players data", ^{ it (@"should search players", ^{ stubResponse(@"/wot/account/list", @"players-search.json"); RACSignal *searchRequest = [client searchPlayers:@"garnett" limit:0]; NSArray *response = [searchRequest asynchronousFirstOrDefault:nil success:NULL error:&error]; [[error should] beNil]; [[response should] beKindOfClass:NSArray.class]; [[response.lastObject should] beKindOfClass:WOTSearchPlayer.class]; WOTSearchPlayer *player = response[0]; [[player.ID should] equal:@"1785514"]; }); }); 
 ,     / ,    RACSignal      ,       : 
 (id)asynchronousFirstOrDefault:(id)defaultValue success:(BOOL *)success error:(NSError **)error; 
   
 describe(@"WOTRatingType", ^{ it(@"should parse json to model object", ^{ NSDictionary *json = @{ @"threshold": @5, @"type": @"1", @"rank_fields": @[ @"xp_amount_rank", @"xp_max_rank", @"battles_count_rank", @"spotted_count_rank", @"damage_dealt_rank", ] }; NSError *error; WOTRatingType *ratingType = [MTLJSONAdapter modelOfClass:WOTRatingType.class fromJSONDictionary:json error:&error]; [[error should] beNil]; [[ratingType should] beKindOfClass:WOTRatingType.class]; [[json[@"threshold"] should] equal:theValue(ratingType.threshold)]; * [[json[@"type"] should] equal:ratingType.type]; [[ratingType.rankFields shouldNot] beNil]; [[ratingType.rankFields should] beKindOfClass:NSArray.class]; [[ratingType.rankFields should] haveCountOf:5]; [[@"maximumXP" should] equal:ratingType.rankFields[1]]; }); }); 
 
 * —    - -    Kiwi,      ,     nil . 
 
       API    : 
    ;   . 
  ,  ,       :  query  http-.   
 
  
    ,      ReactiveCocoa ,         (    ).    ( )   2k LOC (  ~1k -  ,   ~700 -     ),     :        —         ..
Kiwi
BDD- RSpec - , . «--».
OHTTPStubs
, web- . "", - .
.
There are only two hard things in Computer Science: cache invalidation and naming things.
-- Phil Karlton
« » :
..
, .
- inMemory CoreData , NSCache . , , — . NSURLConnection .
NSURLCache
NSURLCache NSURLRequest . , .
:
NSURLCache *urlCache = [[NSURLCache alloc] initWithMemoryCapacity:kInMemoryCacheSize diskCapacity:kOnDiskCacheSize diskPath:path]; [NSURLCache setSharedURLCache:urlCache];
, .
NSURLConnectionDelegate , "" :
- (NSCachedURLResponse *)connection:(NSURLConnection *)connection willCacheResponse:(NSCachedURLResponse *)cachedResponse;
?
NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse*)[cachedResponse response]; NSDictionary *headers = [httpResponse allHeaderFields]; NSString *cacheControl = [headers valueForKey:@"Cache-Control"]; NSString *expires = [headers valueForKey:@"Expires"]; if((cacheControl == nil) && (expires == nil)) { NSMutableDictionary *modifiedHeaders = [httpResponse.allHeaderFields mutableCopy]; NSString *expireDate = [NSDate RFC822StringFromDate:[[NSDate date] dateByAddingTimeInterval:cacheTime]]; NSDictionary *values = @{@"Expires": expireDate, @"Cache-Control": @"public"}; [modifiedHeaders addEntriesFromDictionary:values]; NSHTTPURLResponse *response = [[NSHTTPURLResponse alloc] initWithURL:httpResponse.URL statusCode:httpResponse.statusCode HTTPVersion:@"HTTP/1.1" headerFields:modifiedHeaders]; NSCachedURLResponse *modifiedCachedResponse = [[NSCachedURLResponse alloc] initWithResponse:response data:cachedResponse.data userInfo:cachedResponse.userInfo storagePolicy:NSURLCacheStorageAllowed]; return modifiedCachedResponse; } return cachedResponse;
HTTP- .
, ( , , ). , ( cacheTime ) .
— ( ). , , .
:
(, , ..); - NSURLCache , .
:
30 ; ; : 200- , NSURLConnection .
""
API (, ), fields :
. . . , .
, Player , JSON-, . , fields , NSDictionary . , - . , .
JSON -> NSObject Mantle , API ( RACSignal API ):
- (RACSignal *)searchPlayers:(NSString *)query limit:(NSUInteger)limit { NSMutableDictionary *parameters = [NSMutableDictionary dictionary]; parameters[@"search"] = query; if (limit) parameters[@"limit"] = @(limit); return [self getPath:@"account/list" parameters:parameters resultClass:WOTSearchPlayer.class]; }
, resultClass , ? :
- (RACSignal *)searchPlayers:(NSString *)query limit:(NSUInteger)limit resultClass:(Class)resultClass
, API, .
API , : , , "" . ( ). , ( AFJSONRequestOperation ):
op1 = [AFJSONRequestOperation JSONRequestOperationWithRequest:nil success:^(id JSON) { op2 = [AFJSONRequestOperation JSONRequestOperationWithRequest:nil success:^(id JSON) { op3 = [AFJSONRequestOperation JSONRequestOperationWithRequest:nil success:^(id JSON) { // Combine all the responses and return } failure:^(NSError *error) { // Handle error }]; } failure:^(NSError *error) { // Handle error }]; } failure:^(NSError *error) { // Handle error }];
, , . ReactiveCocoa , .
API :
- (RACSignal *)searchPlayers:(NSString *)query limit:(NSUInteger)limit; - (RACSignal *)fetchPlayers:(NSArray *)playerIDs;
RACSignal — , , . — , . / :
RACSignal *fetchPlayersAndRatings = [[[[API searchPlayers:@"" limit:0] flattenMap:^RACStream *(id players) { // skipping data processing return [API fetchPlayers:@[]]; }] flattenMap:^RACStream *(id fullPlayers) { // Skipping data processing return [API statsSliceForPlayer:nil hoursAgo:@[] fields:nil]; }] flattenMap:^RACStream *(id value) { // Compose all responses here id composedResponse; return [RACSignal return:composedResponse]; }]; [fetchPlayersAndRatings subscribeNext:^(id x) { // Fully composed response } error:^(NSError *error) { // All erros go to one place }];
ReactiveCocoa — , . , Promises Futures.
:
Core ( , ); WOT ( World of Tanks–); WOWP ( World of Warplanes–)
, , — . , ( ) CocoaPods , . subspecs , :
# Subspecs s.subspec 'Core' do |cs| cs.source_files = 'WGNAPIClient/Core/**/*.{h,m}' end s.subspec 'WOT' do |cs| cs.dependency 'WGNAPIClient/Core' cs.source_files = 'WGNAPIClient/WOT/**/*.{h,m}' end s.subspec 'WOWP' do |cs| cs.dependency 'WGNAPIClient/Core' cs.source_files = 'WGNAPIClient/WOWP/**/*.{h,m}' end
"" "" , :
pod 'WGNAPIClient/WOT' pod 'WGNAPIClient/WOWP'
. .
(98%). :
; .
:
context(@"API works with players data", ^{ it (@"should search players", ^{ stubResponse(@"/wot/account/list", @"players-search.json"); RACSignal *searchRequest = [client searchPlayers:@"garnett" limit:0]; NSArray *response = [searchRequest asynchronousFirstOrDefault:nil success:NULL error:&error]; [[error should] beNil]; [[response should] beKindOfClass:NSArray.class]; [[response.lastObject should] beKindOfClass:WOTSearchPlayer.class]; WOTSearchPlayer *player = response[0]; [[player.ID should] equal:@"1785514"]; }); });
, / , RACSignal , :
(id)asynchronousFirstOrDefault:(id)defaultValue success:(BOOL *)success error:(NSError **)error;
describe(@"WOTRatingType", ^{ it(@"should parse json to model object", ^{ NSDictionary *json = @{ @"threshold": @5, @"type": @"1", @"rank_fields": @[ @"xp_amount_rank", @"xp_max_rank", @"battles_count_rank", @"spotted_count_rank", @"damage_dealt_rank", ] }; NSError *error; WOTRatingType *ratingType = [MTLJSONAdapter modelOfClass:WOTRatingType.class fromJSONDictionary:json error:&error]; [[error should] beNil]; [[ratingType should] beKindOfClass:WOTRatingType.class]; [[json[@"threshold"] should] equal:theValue(ratingType.threshold)]; * [[json[@"type"] should] equal:ratingType.type]; [[ratingType.rankFields shouldNot] beNil]; [[ratingType.rankFields should] beKindOfClass:NSArray.class]; [[ratingType.rankFields should] haveCountOf:5]; [[@"maximumXP" should] equal:ratingType.rankFields[1]]; }); });
* — - - Kiwi, , nil .
API :
; .
, , : query http-. 
, ReactiveCocoa , ( ). ( ) 2k LOC ( ~1k - , ~700 - ), : — ..
Kiwi
BDD- RSpec - , . «--».
OHTTPStubs
, web- . "", - .
.
There are only two hard things in Computer Science: cache invalidation and naming things.
-- Phil Karlton
« » :
..
, .
- inMemory CoreData , NSCache . , , — . NSURLConnection .
NSURLCache
NSURLCache NSURLRequest . , .
:
NSURLCache *urlCache = [[NSURLCache alloc] initWithMemoryCapacity:kInMemoryCacheSize diskCapacity:kOnDiskCacheSize diskPath:path]; [NSURLCache setSharedURLCache:urlCache];
, .
NSURLConnectionDelegate , "" :
- (NSCachedURLResponse *)connection:(NSURLConnection *)connection willCacheResponse:(NSCachedURLResponse *)cachedResponse;
?
NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse*)[cachedResponse response]; NSDictionary *headers = [httpResponse allHeaderFields]; NSString *cacheControl = [headers valueForKey:@"Cache-Control"]; NSString *expires = [headers valueForKey:@"Expires"]; if((cacheControl == nil) && (expires == nil)) { NSMutableDictionary *modifiedHeaders = [httpResponse.allHeaderFields mutableCopy]; NSString *expireDate = [NSDate RFC822StringFromDate:[[NSDate date] dateByAddingTimeInterval:cacheTime]]; NSDictionary *values = @{@"Expires": expireDate, @"Cache-Control": @"public"}; [modifiedHeaders addEntriesFromDictionary:values]; NSHTTPURLResponse *response = [[NSHTTPURLResponse alloc] initWithURL:httpResponse.URL statusCode:httpResponse.statusCode HTTPVersion:@"HTTP/1.1" headerFields:modifiedHeaders]; NSCachedURLResponse *modifiedCachedResponse = [[NSCachedURLResponse alloc] initWithResponse:response data:cachedResponse.data userInfo:cachedResponse.userInfo storagePolicy:NSURLCacheStorageAllowed]; return modifiedCachedResponse; } return cachedResponse;
HTTP- .
, ( , , ). , ( cacheTime ) .
— ( ). , , .
:
(, , ..); - NSURLCache , .
:
30 ; ; : 200- , NSURLConnection .
""
API (, ), fields :
. . . , .
, Player , JSON-, . , fields , NSDictionary . , - . , .
JSON -> NSObject Mantle , API ( RACSignal API ):
- (RACSignal *)searchPlayers:(NSString *)query limit:(NSUInteger)limit { NSMutableDictionary *parameters = [NSMutableDictionary dictionary]; parameters[@"search"] = query; if (limit) parameters[@"limit"] = @(limit); return [self getPath:@"account/list" parameters:parameters resultClass:WOTSearchPlayer.class]; }
, resultClass , ? :
- (RACSignal *)searchPlayers:(NSString *)query limit:(NSUInteger)limit resultClass:(Class)resultClass
, API, .
API , : , , "" . ( ). , ( AFJSONRequestOperation ):
op1 = [AFJSONRequestOperation JSONRequestOperationWithRequest:nil success:^(id JSON) { op2 = [AFJSONRequestOperation JSONRequestOperationWithRequest:nil success:^(id JSON) { op3 = [AFJSONRequestOperation JSONRequestOperationWithRequest:nil success:^(id JSON) { // Combine all the responses and return } failure:^(NSError *error) { // Handle error }]; } failure:^(NSError *error) { // Handle error }]; } failure:^(NSError *error) { // Handle error }];
, , . ReactiveCocoa , .
API :
- (RACSignal *)searchPlayers:(NSString *)query limit:(NSUInteger)limit; - (RACSignal *)fetchPlayers:(NSArray *)playerIDs;
RACSignal — , , . — , . / :
RACSignal *fetchPlayersAndRatings = [[[[API searchPlayers:@"" limit:0] flattenMap:^RACStream *(id players) { // skipping data processing return [API fetchPlayers:@[]]; }] flattenMap:^RACStream *(id fullPlayers) { // Skipping data processing return [API statsSliceForPlayer:nil hoursAgo:@[] fields:nil]; }] flattenMap:^RACStream *(id value) { // Compose all responses here id composedResponse; return [RACSignal return:composedResponse]; }]; [fetchPlayersAndRatings subscribeNext:^(id x) { // Fully composed response } error:^(NSError *error) { // All erros go to one place }];
ReactiveCocoa — , . , Promises Futures.
:
Core ( , ); WOT ( World of Tanks–); WOWP ( World of Warplanes–)
, , — . , ( ) CocoaPods , . subspecs , :
# Subspecs s.subspec 'Core' do |cs| cs.source_files = 'WGNAPIClient/Core/**/*.{h,m}' end s.subspec 'WOT' do |cs| cs.dependency 'WGNAPIClient/Core' cs.source_files = 'WGNAPIClient/WOT/**/*.{h,m}' end s.subspec 'WOWP' do |cs| cs.dependency 'WGNAPIClient/Core' cs.source_files = 'WGNAPIClient/WOWP/**/*.{h,m}' end
"" "" , :
pod 'WGNAPIClient/WOT' pod 'WGNAPIClient/WOWP'
. .
(98%). :
; .
:
context(@"API works with players data", ^{ it (@"should search players", ^{ stubResponse(@"/wot/account/list", @"players-search.json"); RACSignal *searchRequest = [client searchPlayers:@"garnett" limit:0]; NSArray *response = [searchRequest asynchronousFirstOrDefault:nil success:NULL error:&error]; [[error should] beNil]; [[response should] beKindOfClass:NSArray.class]; [[response.lastObject should] beKindOfClass:WOTSearchPlayer.class]; WOTSearchPlayer *player = response[0]; [[player.ID should] equal:@"1785514"]; }); });
, / , RACSignal , :
(id)asynchronousFirstOrDefault:(id)defaultValue success:(BOOL *)success error:(NSError **)error;
describe(@"WOTRatingType", ^{ it(@"should parse json to model object", ^{ NSDictionary *json = @{ @"threshold": @5, @"type": @"1", @"rank_fields": @[ @"xp_amount_rank", @"xp_max_rank", @"battles_count_rank", @"spotted_count_rank", @"damage_dealt_rank", ] }; NSError *error; WOTRatingType *ratingType = [MTLJSONAdapter modelOfClass:WOTRatingType.class fromJSONDictionary:json error:&error]; [[error should] beNil]; [[ratingType should] beKindOfClass:WOTRatingType.class]; [[json[@"threshold"] should] equal:theValue(ratingType.threshold)]; * [[json[@"type"] should] equal:ratingType.type]; [[ratingType.rankFields shouldNot] beNil]; [[ratingType.rankFields should] beKindOfClass:NSArray.class]; [[ratingType.rankFields should] haveCountOf:5]; [[@"maximumXP" should] equal:ratingType.rankFields[1]]; }); });
* — - - Kiwi, , nil .
API :
; .
, , : query http-. 
, ReactiveCocoa , ( ). ( ) 2k LOC ( ~1k - , ~700 - ), : — ..
Kiwi
BDD- RSpec - , . «--».
OHTTPStubs
, web- . "", - .
.
There are only two hard things in Computer Science: cache invalidation and naming things.
-- Phil Karlton
« » :
..
, .
- inMemory CoreData , NSCache . , , — . NSURLConnection .
NSURLCache
NSURLCache NSURLRequest . , .
:
NSURLCache *urlCache = [[NSURLCache alloc] initWithMemoryCapacity:kInMemoryCacheSize diskCapacity:kOnDiskCacheSize diskPath:path]; [NSURLCache setSharedURLCache:urlCache];
, .
NSURLConnectionDelegate , "" :
- (NSCachedURLResponse *)connection:(NSURLConnection *)connection willCacheResponse:(NSCachedURLResponse *)cachedResponse;
?
NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse*)[cachedResponse response]; NSDictionary *headers = [httpResponse allHeaderFields]; NSString *cacheControl = [headers valueForKey:@"Cache-Control"]; NSString *expires = [headers valueForKey:@"Expires"]; if((cacheControl == nil) && (expires == nil)) { NSMutableDictionary *modifiedHeaders = [httpResponse.allHeaderFields mutableCopy]; NSString *expireDate = [NSDate RFC822StringFromDate:[[NSDate date] dateByAddingTimeInterval:cacheTime]]; NSDictionary *values = @{@"Expires": expireDate, @"Cache-Control": @"public"}; [modifiedHeaders addEntriesFromDictionary:values]; NSHTTPURLResponse *response = [[NSHTTPURLResponse alloc] initWithURL:httpResponse.URL statusCode:httpResponse.statusCode HTTPVersion:@"HTTP/1.1" headerFields:modifiedHeaders]; NSCachedURLResponse *modifiedCachedResponse = [[NSCachedURLResponse alloc] initWithResponse:response data:cachedResponse.data userInfo:cachedResponse.userInfo storagePolicy:NSURLCacheStorageAllowed]; return modifiedCachedResponse; } return cachedResponse;
HTTP- .
, ( , , ). , ( cacheTime ) .
— ( ). , , .
:
(, , ..); - NSURLCache , .
:
30 ; ; : 200- , NSURLConnection .
""
API (, ), fields :
. . . , .
, Player , JSON-, . , fields , NSDictionary . , - . , .
JSON -> NSObject Mantle , API ( RACSignal API ):
- (RACSignal *)searchPlayers:(NSString *)query limit:(NSUInteger)limit { NSMutableDictionary *parameters = [NSMutableDictionary dictionary]; parameters[@"search"] = query; if (limit) parameters[@"limit"] = @(limit); return [self getPath:@"account/list" parameters:parameters resultClass:WOTSearchPlayer.class]; }
, resultClass , ? :
- (RACSignal *)searchPlayers:(NSString *)query limit:(NSUInteger)limit resultClass:(Class)resultClass
, API, .
API , : , , "" . ( ). , ( AFJSONRequestOperation ):
op1 = [AFJSONRequestOperation JSONRequestOperationWithRequest:nil success:^(id JSON) { op2 = [AFJSONRequestOperation JSONRequestOperationWithRequest:nil success:^(id JSON) { op3 = [AFJSONRequestOperation JSONRequestOperationWithRequest:nil success:^(id JSON) { // Combine all the responses and return } failure:^(NSError *error) { // Handle error }]; } failure:^(NSError *error) { // Handle error }]; } failure:^(NSError *error) { // Handle error }];
, , . ReactiveCocoa , .
API :
- (RACSignal *)searchPlayers:(NSString *)query limit:(NSUInteger)limit; - (RACSignal *)fetchPlayers:(NSArray *)playerIDs;
RACSignal — , , . — , . / :
RACSignal *fetchPlayersAndRatings = [[[[API searchPlayers:@"" limit:0] flattenMap:^RACStream *(id players) { // skipping data processing return [API fetchPlayers:@[]]; }] flattenMap:^RACStream *(id fullPlayers) { // Skipping data processing return [API statsSliceForPlayer:nil hoursAgo:@[] fields:nil]; }] flattenMap:^RACStream *(id value) { // Compose all responses here id composedResponse; return [RACSignal return:composedResponse]; }]; [fetchPlayersAndRatings subscribeNext:^(id x) { // Fully composed response } error:^(NSError *error) { // All erros go to one place }];
ReactiveCocoa — , . , Promises Futures.
:
Core ( , ); WOT ( World of Tanks–); WOWP ( World of Warplanes–)
, , — . , ( ) CocoaPods , . subspecs , :
# Subspecs s.subspec 'Core' do |cs| cs.source_files = 'WGNAPIClient/Core/**/*.{h,m}' end s.subspec 'WOT' do |cs| cs.dependency 'WGNAPIClient/Core' cs.source_files = 'WGNAPIClient/WOT/**/*.{h,m}' end s.subspec 'WOWP' do |cs| cs.dependency 'WGNAPIClient/Core' cs.source_files = 'WGNAPIClient/WOWP/**/*.{h,m}' end
"" "" , :
pod 'WGNAPIClient/WOT' pod 'WGNAPIClient/WOWP'
. .
(98%). :
; .
:
context(@"API works with players data", ^{ it (@"should search players", ^{ stubResponse(@"/wot/account/list", @"players-search.json"); RACSignal *searchRequest = [client searchPlayers:@"garnett" limit:0]; NSArray *response = [searchRequest asynchronousFirstOrDefault:nil success:NULL error:&error]; [[error should] beNil]; [[response should] beKindOfClass:NSArray.class]; [[response.lastObject should] beKindOfClass:WOTSearchPlayer.class]; WOTSearchPlayer *player = response[0]; [[player.ID should] equal:@"1785514"]; }); });
, / , RACSignal , :
(id)asynchronousFirstOrDefault:(id)defaultValue success:(BOOL *)success error:(NSError **)error;
describe(@"WOTRatingType", ^{ it(@"should parse json to model object", ^{ NSDictionary *json = @{ @"threshold": @5, @"type": @"1", @"rank_fields": @[ @"xp_amount_rank", @"xp_max_rank", @"battles_count_rank", @"spotted_count_rank", @"damage_dealt_rank", ] }; NSError *error; WOTRatingType *ratingType = [MTLJSONAdapter modelOfClass:WOTRatingType.class fromJSONDictionary:json error:&error]; [[error should] beNil]; [[ratingType should] beKindOfClass:WOTRatingType.class]; [[json[@"threshold"] should] equal:theValue(ratingType.threshold)]; * [[json[@"type"] should] equal:ratingType.type]; [[ratingType.rankFields shouldNot] beNil]; [[ratingType.rankFields should] beKindOfClass:NSArray.class]; [[ratingType.rankFields should] haveCountOf:5]; [[@"maximumXP" should] equal:ratingType.rankFields[1]]; }); });
* — - - Kiwi, , nil .
API :
; .
, , : query http-. 
, ReactiveCocoa , ( ). ( ) 2k LOC ( ~1k - , ~700 - ), : — .     . 
 
 Kiwi 
  BDD-    RSpec -  ,   .   «--». 
 
 OHTTPStubs 
   ,      web-   .      "",      - . 
 
     . 
 
    
 There are only two hard things in Computer Science: cache invalidation and naming things. 
 
 -- Phil Karlton 
  « »  : 
 
                .. 
 ,            . 
       -     inMemory CoreData ,       NSCache .   ,       ,         —         .         NSURLConnection . 
 
 NSURLCache 
 NSURLCache        NSURLRequest   .      ,    . 
 
   : 
 NSURLCache *urlCache = [[NSURLCache alloc] initWithMemoryCapacity:kInMemoryCacheSize diskCapacity:kOnDiskCacheSize diskPath:path]; [NSURLCache setSharedURLCache:urlCache]; 
     ,           . 
 
  NSURLConnectionDelegate   ,     ""    : 
 - (NSCachedURLResponse *)connection:(NSURLConnection *)connection willCacheResponse:(NSCachedURLResponse *)cachedResponse; 
    ? 
 
  NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse*)[cachedResponse response]; NSDictionary *headers = [httpResponse allHeaderFields]; NSString *cacheControl = [headers valueForKey:@"Cache-Control"]; NSString *expires = [headers valueForKey:@"Expires"]; if((cacheControl == nil) && (expires == nil)) { NSMutableDictionary *modifiedHeaders = [httpResponse.allHeaderFields mutableCopy]; NSString *expireDate = [NSDate RFC822StringFromDate:[[NSDate date] dateByAddingTimeInterval:cacheTime]]; NSDictionary *values = @{@"Expires": expireDate, @"Cache-Control": @"public"}; [modifiedHeaders addEntriesFromDictionary:values]; NSHTTPURLResponse *response = [[NSHTTPURLResponse alloc] initWithURL:httpResponse.URL statusCode:httpResponse.statusCode HTTPVersion:@"HTTP/1.1" headerFields:modifiedHeaders]; NSCachedURLResponse *modifiedCachedResponse = [[NSCachedURLResponse alloc] initWithResponse:response data:cachedResponse.data userInfo:cachedResponse.userInfo storagePolicy:NSURLCacheStorageAllowed]; return modifiedCachedResponse; } return cachedResponse; 
      HTTP-      . 
 
 ,     (    ,    ,            ).   ,     ( cacheTime )   . 
     —        (      ).    ,      ,    . 
 
 : 
 
        (, ,   ..);  -   NSURLCache ,   . 
 : 
     30  ;       ; :         200- , NSURLConnection   . 
  ""  
          API (,    ),     fields : 
 
   .   .    .    ,   . 
  ,   Player ,      JSON-,   .        ,       fields ,        NSDictionary .         ,  -       .          ,           . 
   JSON -> NSObject    Mantle ,     API      ( RACSignal   API      ): 
 
 - (RACSignal *)searchPlayers:(NSString *)query limit:(NSUInteger)limit { NSMutableDictionary *parameters = [NSMutableDictionary dictionary]; parameters[@"search"] = query; if (limit) parameters[@"limit"] = @(limit); return [self getPath:@"account/list" parameters:parameters resultClass:WOTSearchPlayer.class]; } 
  ,       resultClass ,         ? : 
 
  - (RACSignal *)searchPlayers:(NSString *)query limit:(NSUInteger)limit resultClass:(Class)resultClass 
 ,     API,           . 
 
       
     API     ,               : ,   ,     ""    . (    ).    ,       (   AFJSONRequestOperation ): 
 
  op1 = [AFJSONRequestOperation JSONRequestOperationWithRequest:nil success:^(id JSON) { op2 = [AFJSONRequestOperation JSONRequestOperationWithRequest:nil success:^(id JSON) { op3 = [AFJSONRequestOperation JSONRequestOperationWithRequest:nil success:^(id JSON) { // Combine all the responses and return } failure:^(NSError *error) { // Handle error }]; } failure:^(NSError *error) { // Handle error }]; } failure:^(NSError *error) { // Handle error }]; 
  ,    ,          .         ReactiveCocoa  ,       . 
  API    : 
 
 - (RACSignal *)searchPlayers:(NSString *)query limit:(NSUInteger)limit; - (RACSignal *)fetchPlayers:(NSArray *)playerIDs; 
 RACSignal —  ,       ,     .    —       ,    .          / : 
 
  RACSignal *fetchPlayersAndRatings = [[[[API searchPlayers:@"" limit:0] flattenMap:^RACStream *(id players) { // skipping data processing return [API fetchPlayers:@[]]; }] flattenMap:^RACStream *(id fullPlayers) { // Skipping data processing return [API statsSliceForPlayer:nil hoursAgo:@[] fields:nil]; }] flattenMap:^RACStream *(id value) { // Compose all responses here id composedResponse; return [RACSignal return:composedResponse]; }]; [fetchPlayersAndRatings subscribeNext:^(id x) { // Fully composed response } error:^(NSError *error) { // All erros go to one place }]; 
  ReactiveCocoa   —      ,      .        ,         Promises  Futures. 
 
      
        : 
 Core ( , ); WOT (    World of Tanks–); WOWP (    World of Warplanes–) 
  ,         ,      — . ,    (    )       CocoaPods ,       .       subspecs ,        : 
 
  # Subspecs s.subspec 'Core' do |cs| cs.source_files = 'WGNAPIClient/Core/**/*.{h,m}' end s.subspec 'WOT' do |cs| cs.dependency 'WGNAPIClient/Core' cs.source_files = 'WGNAPIClient/WOT/**/*.{h,m}' end s.subspec 'WOWP' do |cs| cs.dependency 'WGNAPIClient/Core' cs.source_files = 'WGNAPIClient/WOWP/**/*.{h,m}' end 
 
    ""  ""  ,      : 
 pod 'WGNAPIClient/WOT' pod 'WGNAPIClient/WOWP' 
     
              .        . 
         (98%).           : 
 
   ;    . 
   
     : 
  context(@"API works with players data", ^{ it (@"should search players", ^{ stubResponse(@"/wot/account/list", @"players-search.json"); RACSignal *searchRequest = [client searchPlayers:@"garnett" limit:0]; NSArray *response = [searchRequest asynchronousFirstOrDefault:nil success:NULL error:&error]; [[error should] beNil]; [[response should] beKindOfClass:NSArray.class]; [[response.lastObject should] beKindOfClass:WOTSearchPlayer.class]; WOTSearchPlayer *player = response[0]; [[player.ID should] equal:@"1785514"]; }); }); 
 ,     / ,    RACSignal      ,       : 
 (id)asynchronousFirstOrDefault:(id)defaultValue success:(BOOL *)success error:(NSError **)error; 
   
 describe(@"WOTRatingType", ^{ it(@"should parse json to model object", ^{ NSDictionary *json = @{ @"threshold": @5, @"type": @"1", @"rank_fields": @[ @"xp_amount_rank", @"xp_max_rank", @"battles_count_rank", @"spotted_count_rank", @"damage_dealt_rank", ] }; NSError *error; WOTRatingType *ratingType = [MTLJSONAdapter modelOfClass:WOTRatingType.class fromJSONDictionary:json error:&error]; [[error should] beNil]; [[ratingType should] beKindOfClass:WOTRatingType.class]; [[json[@"threshold"] should] equal:theValue(ratingType.threshold)]; * [[json[@"type"] should] equal:ratingType.type]; [[ratingType.rankFields shouldNot] beNil]; [[ratingType.rankFields should] beKindOfClass:NSArray.class]; [[ratingType.rankFields should] haveCountOf:5]; [[@"maximumXP" should] equal:ratingType.rankFields[1]]; }); }); 
 
 * —    - -    Kiwi,      ,     nil . 
 
       API    : 
    ;   . 
  ,  ,       :  query  http-.   
 
  
    ,      ReactiveCocoa ,         (    ).    ( )   2k LOC (  ~1k -  ,   ~700 -     ),     :        —         . .
Kiwi
BDD- RSpec - , . «--».
OHTTPStubs
, web- . "", - .
.
There are only two hard things in Computer Science: cache invalidation and naming things.
-- Phil Karlton
« » :
..
, .
- inMemory CoreData , NSCache . , , — . NSURLConnection .
NSURLCache
NSURLCache NSURLRequest . , .
:
NSURLCache *urlCache = [[NSURLCache alloc] initWithMemoryCapacity:kInMemoryCacheSize diskCapacity:kOnDiskCacheSize diskPath:path]; [NSURLCache setSharedURLCache:urlCache];
, .
NSURLConnectionDelegate , "" :
- (NSCachedURLResponse *)connection:(NSURLConnection *)connection willCacheResponse:(NSCachedURLResponse *)cachedResponse;
?
NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse*)[cachedResponse response]; NSDictionary *headers = [httpResponse allHeaderFields]; NSString *cacheControl = [headers valueForKey:@"Cache-Control"]; NSString *expires = [headers valueForKey:@"Expires"]; if((cacheControl == nil) && (expires == nil)) { NSMutableDictionary *modifiedHeaders = [httpResponse.allHeaderFields mutableCopy]; NSString *expireDate = [NSDate RFC822StringFromDate:[[NSDate date] dateByAddingTimeInterval:cacheTime]]; NSDictionary *values = @{@"Expires": expireDate, @"Cache-Control": @"public"}; [modifiedHeaders addEntriesFromDictionary:values]; NSHTTPURLResponse *response = [[NSHTTPURLResponse alloc] initWithURL:httpResponse.URL statusCode:httpResponse.statusCode HTTPVersion:@"HTTP/1.1" headerFields:modifiedHeaders]; NSCachedURLResponse *modifiedCachedResponse = [[NSCachedURLResponse alloc] initWithResponse:response data:cachedResponse.data userInfo:cachedResponse.userInfo storagePolicy:NSURLCacheStorageAllowed]; return modifiedCachedResponse; } return cachedResponse;
HTTP- .
, ( , , ). , ( cacheTime ) .
— ( ). , , .
:
(, , ..); - NSURLCache , .
:
30 ; ; : 200- , NSURLConnection .
""
API (, ), fields :
. . . , .
, Player , JSON-, . , fields , NSDictionary . , - . , .
JSON -> NSObject Mantle , API ( RACSignal API ):
- (RACSignal *)searchPlayers:(NSString *)query limit:(NSUInteger)limit { NSMutableDictionary *parameters = [NSMutableDictionary dictionary]; parameters[@"search"] = query; if (limit) parameters[@"limit"] = @(limit); return [self getPath:@"account/list" parameters:parameters resultClass:WOTSearchPlayer.class]; }
, resultClass , ? :
- (RACSignal *)searchPlayers:(NSString *)query limit:(NSUInteger)limit resultClass:(Class)resultClass
, API, .
API , : , , "" . ( ). , ( AFJSONRequestOperation ):
op1 = [AFJSONRequestOperation JSONRequestOperationWithRequest:nil success:^(id JSON) { op2 = [AFJSONRequestOperation JSONRequestOperationWithRequest:nil success:^(id JSON) { op3 = [AFJSONRequestOperation JSONRequestOperationWithRequest:nil success:^(id JSON) { // Combine all the responses and return } failure:^(NSError *error) { // Handle error }]; } failure:^(NSError *error) { // Handle error }]; } failure:^(NSError *error) { // Handle error }];
, , . ReactiveCocoa , .
API :
- (RACSignal *)searchPlayers:(NSString *)query limit:(NSUInteger)limit; - (RACSignal *)fetchPlayers:(NSArray *)playerIDs;
RACSignal — , , . — , . / :
RACSignal *fetchPlayersAndRatings = [[[[API searchPlayers:@"" limit:0] flattenMap:^RACStream *(id players) { // skipping data processing return [API fetchPlayers:@[]]; }] flattenMap:^RACStream *(id fullPlayers) { // Skipping data processing return [API statsSliceForPlayer:nil hoursAgo:@[] fields:nil]; }] flattenMap:^RACStream *(id value) { // Compose all responses here id composedResponse; return [RACSignal return:composedResponse]; }]; [fetchPlayersAndRatings subscribeNext:^(id x) { // Fully composed response } error:^(NSError *error) { // All erros go to one place }];
ReactiveCocoa — , . , Promises Futures.
:
Core ( , ); WOT ( World of Tanks–); WOWP ( World of Warplanes–)
, , — . , ( ) CocoaPods , . subspecs , :
# Subspecs s.subspec 'Core' do |cs| cs.source_files = 'WGNAPIClient/Core/**/*.{h,m}' end s.subspec 'WOT' do |cs| cs.dependency 'WGNAPIClient/Core' cs.source_files = 'WGNAPIClient/WOT/**/*.{h,m}' end s.subspec 'WOWP' do |cs| cs.dependency 'WGNAPIClient/Core' cs.source_files = 'WGNAPIClient/WOWP/**/*.{h,m}' end
"" "" , :
pod 'WGNAPIClient/WOT' pod 'WGNAPIClient/WOWP'
. .
(98%). :
; .
:
context(@"API works with players data", ^{ it (@"should search players", ^{ stubResponse(@"/wot/account/list", @"players-search.json"); RACSignal *searchRequest = [client searchPlayers:@"garnett" limit:0]; NSArray *response = [searchRequest asynchronousFirstOrDefault:nil success:NULL error:&error]; [[error should] beNil]; [[response should] beKindOfClass:NSArray.class]; [[response.lastObject should] beKindOfClass:WOTSearchPlayer.class]; WOTSearchPlayer *player = response[0]; [[player.ID should] equal:@"1785514"]; }); });
, / , RACSignal , :
(id)asynchronousFirstOrDefault:(id)defaultValue success:(BOOL *)success error:(NSError **)error;
describe(@"WOTRatingType", ^{ it(@"should parse json to model object", ^{ NSDictionary *json = @{ @"threshold": @5, @"type": @"1", @"rank_fields": @[ @"xp_amount_rank", @"xp_max_rank", @"battles_count_rank", @"spotted_count_rank", @"damage_dealt_rank", ] }; NSError *error; WOTRatingType *ratingType = [MTLJSONAdapter modelOfClass:WOTRatingType.class fromJSONDictionary:json error:&error]; [[error should] beNil]; [[ratingType should] beKindOfClass:WOTRatingType.class]; [[json[@"threshold"] should] equal:theValue(ratingType.threshold)]; * [[json[@"type"] should] equal:ratingType.type]; [[ratingType.rankFields shouldNot] beNil]; [[ratingType.rankFields should] beKindOfClass:NSArray.class]; [[ratingType.rankFields should] haveCountOf:5]; [[@"maximumXP" should] equal:ratingType.rankFields[1]]; }); });
* — - - Kiwi, , nil .
API :
; .
, , : query http-. 
, ReactiveCocoa , ( ). ( ) 2k LOC ( ~1k - , ~700 - ), : — .     . 
 
 Kiwi 
  BDD-    RSpec -  ,   .   «--». 
 
 OHTTPStubs 
   ,      web-   .      "",      - . 
 
     . 
 
    
 There are only two hard things in Computer Science: cache invalidation and naming things. 
 
 -- Phil Karlton 
  « »  : 
 
                .. 
 ,            . 
       -     inMemory CoreData ,       NSCache .   ,       ,         —         .         NSURLConnection . 
 
 NSURLCache 
 NSURLCache        NSURLRequest   .      ,    . 
 
   : 
 NSURLCache *urlCache = [[NSURLCache alloc] initWithMemoryCapacity:kInMemoryCacheSize diskCapacity:kOnDiskCacheSize diskPath:path]; [NSURLCache setSharedURLCache:urlCache]; 
     ,           . 
 
  NSURLConnectionDelegate   ,     ""    : 
 - (NSCachedURLResponse *)connection:(NSURLConnection *)connection willCacheResponse:(NSCachedURLResponse *)cachedResponse; 
    ? 
 
  NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse*)[cachedResponse response]; NSDictionary *headers = [httpResponse allHeaderFields]; NSString *cacheControl = [headers valueForKey:@"Cache-Control"]; NSString *expires = [headers valueForKey:@"Expires"]; if((cacheControl == nil) && (expires == nil)) { NSMutableDictionary *modifiedHeaders = [httpResponse.allHeaderFields mutableCopy]; NSString *expireDate = [NSDate RFC822StringFromDate:[[NSDate date] dateByAddingTimeInterval:cacheTime]]; NSDictionary *values = @{@"Expires": expireDate, @"Cache-Control": @"public"}; [modifiedHeaders addEntriesFromDictionary:values]; NSHTTPURLResponse *response = [[NSHTTPURLResponse alloc] initWithURL:httpResponse.URL statusCode:httpResponse.statusCode HTTPVersion:@"HTTP/1.1" headerFields:modifiedHeaders]; NSCachedURLResponse *modifiedCachedResponse = [[NSCachedURLResponse alloc] initWithResponse:response data:cachedResponse.data userInfo:cachedResponse.userInfo storagePolicy:NSURLCacheStorageAllowed]; return modifiedCachedResponse; } return cachedResponse; 
      HTTP-      . 
 
 ,     (    ,    ,            ).   ,     ( cacheTime )   . 
     —        (      ).    ,      ,    . 
 
 : 
 
        (, ,   ..);  -   NSURLCache ,   . 
 : 
     30  ;       ; :         200- , NSURLConnection   . 
  ""  
          API (,    ),     fields : 
 
   .   .    .    ,   . 
  ,   Player ,      JSON-,   .        ,       fields ,        NSDictionary .         ,  -       .          ,           . 
   JSON -> NSObject    Mantle ,     API      ( RACSignal   API      ): 
 
 - (RACSignal *)searchPlayers:(NSString *)query limit:(NSUInteger)limit { NSMutableDictionary *parameters = [NSMutableDictionary dictionary]; parameters[@"search"] = query; if (limit) parameters[@"limit"] = @(limit); return [self getPath:@"account/list" parameters:parameters resultClass:WOTSearchPlayer.class]; } 
  ,       resultClass ,         ? : 
 
  - (RACSignal *)searchPlayers:(NSString *)query limit:(NSUInteger)limit resultClass:(Class)resultClass 
 ,     API,           . 
 
       
     API     ,               : ,   ,     ""    . (    ).    ,       (   AFJSONRequestOperation ): 
 
  op1 = [AFJSONRequestOperation JSONRequestOperationWithRequest:nil success:^(id JSON) { op2 = [AFJSONRequestOperation JSONRequestOperationWithRequest:nil success:^(id JSON) { op3 = [AFJSONRequestOperation JSONRequestOperationWithRequest:nil success:^(id JSON) { // Combine all the responses and return } failure:^(NSError *error) { // Handle error }]; } failure:^(NSError *error) { // Handle error }]; } failure:^(NSError *error) { // Handle error }]; 
  ,    ,          .         ReactiveCocoa  ,       . 
  API    : 
 
 - (RACSignal *)searchPlayers:(NSString *)query limit:(NSUInteger)limit; - (RACSignal *)fetchPlayers:(NSArray *)playerIDs; 
 RACSignal —  ,       ,     .    —       ,    .          / : 
 
  RACSignal *fetchPlayersAndRatings = [[[[API searchPlayers:@"" limit:0] flattenMap:^RACStream *(id players) { // skipping data processing return [API fetchPlayers:@[]]; }] flattenMap:^RACStream *(id fullPlayers) { // Skipping data processing return [API statsSliceForPlayer:nil hoursAgo:@[] fields:nil]; }] flattenMap:^RACStream *(id value) { // Compose all responses here id composedResponse; return [RACSignal return:composedResponse]; }]; [fetchPlayersAndRatings subscribeNext:^(id x) { // Fully composed response } error:^(NSError *error) { // All erros go to one place }]; 
  ReactiveCocoa   —      ,      .        ,         Promises  Futures. 
 
      
        : 
 Core ( , ); WOT (    World of Tanks–); WOWP (    World of Warplanes–) 
  ,         ,      — . ,    (    )       CocoaPods ,       .       subspecs ,        : 
 
  # Subspecs s.subspec 'Core' do |cs| cs.source_files = 'WGNAPIClient/Core/**/*.{h,m}' end s.subspec 'WOT' do |cs| cs.dependency 'WGNAPIClient/Core' cs.source_files = 'WGNAPIClient/WOT/**/*.{h,m}' end s.subspec 'WOWP' do |cs| cs.dependency 'WGNAPIClient/Core' cs.source_files = 'WGNAPIClient/WOWP/**/*.{h,m}' end 
 
    ""  ""  ,      : 
 pod 'WGNAPIClient/WOT' pod 'WGNAPIClient/WOWP' 
     
              .        . 
         (98%).           : 
 
   ;    . 
   
     : 
  context(@"API works with players data", ^{ it (@"should search players", ^{ stubResponse(@"/wot/account/list", @"players-search.json"); RACSignal *searchRequest = [client searchPlayers:@"garnett" limit:0]; NSArray *response = [searchRequest asynchronousFirstOrDefault:nil success:NULL error:&error]; [[error should] beNil]; [[response should] beKindOfClass:NSArray.class]; [[response.lastObject should] beKindOfClass:WOTSearchPlayer.class]; WOTSearchPlayer *player = response[0]; [[player.ID should] equal:@"1785514"]; }); }); 
 ,     / ,    RACSignal      ,       : 
 (id)asynchronousFirstOrDefault:(id)defaultValue success:(BOOL *)success error:(NSError **)error; 
   
 describe(@"WOTRatingType", ^{ it(@"should parse json to model object", ^{ NSDictionary *json = @{ @"threshold": @5, @"type": @"1", @"rank_fields": @[ @"xp_amount_rank", @"xp_max_rank", @"battles_count_rank", @"spotted_count_rank", @"damage_dealt_rank", ] }; NSError *error; WOTRatingType *ratingType = [MTLJSONAdapter modelOfClass:WOTRatingType.class fromJSONDictionary:json error:&error]; [[error should] beNil]; [[ratingType should] beKindOfClass:WOTRatingType.class]; [[json[@"threshold"] should] equal:theValue(ratingType.threshold)]; * [[json[@"type"] should] equal:ratingType.type]; [[ratingType.rankFields shouldNot] beNil]; [[ratingType.rankFields should] beKindOfClass:NSArray.class]; [[ratingType.rankFields should] haveCountOf:5]; [[@"maximumXP" should] equal:ratingType.rankFields[1]]; }); }); 
 
 * —    - -    Kiwi,      ,     nil . 
 
       API    : 
    ;   . 
  ,  ,       :  query  http-.   
 
  
    ,      ReactiveCocoa ,         (    ).    ( )   2k LOC (  ~1k -  ,   ~700 -     ),     :        —         . .
Kiwi
BDD- RSpec - , . «--».
OHTTPStubs
, web- . "", - .
.
There are only two hard things in Computer Science: cache invalidation and naming things.
-- Phil Karlton
« » :
..
, .
- inMemory CoreData , NSCache . , , — . NSURLConnection .
NSURLCache
NSURLCache NSURLRequest . , .
:
NSURLCache *urlCache = [[NSURLCache alloc] initWithMemoryCapacity:kInMemoryCacheSize diskCapacity:kOnDiskCacheSize diskPath:path]; [NSURLCache setSharedURLCache:urlCache];
, .
NSURLConnectionDelegate , "" :
- (NSCachedURLResponse *)connection:(NSURLConnection *)connection willCacheResponse:(NSCachedURLResponse *)cachedResponse;
?
NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse*)[cachedResponse response]; NSDictionary *headers = [httpResponse allHeaderFields]; NSString *cacheControl = [headers valueForKey:@"Cache-Control"]; NSString *expires = [headers valueForKey:@"Expires"]; if((cacheControl == nil) && (expires == nil)) { NSMutableDictionary *modifiedHeaders = [httpResponse.allHeaderFields mutableCopy]; NSString *expireDate = [NSDate RFC822StringFromDate:[[NSDate date] dateByAddingTimeInterval:cacheTime]]; NSDictionary *values = @{@"Expires": expireDate, @"Cache-Control": @"public"}; [modifiedHeaders addEntriesFromDictionary:values]; NSHTTPURLResponse *response = [[NSHTTPURLResponse alloc] initWithURL:httpResponse.URL statusCode:httpResponse.statusCode HTTPVersion:@"HTTP/1.1" headerFields:modifiedHeaders]; NSCachedURLResponse *modifiedCachedResponse = [[NSCachedURLResponse alloc] initWithResponse:response data:cachedResponse.data userInfo:cachedResponse.userInfo storagePolicy:NSURLCacheStorageAllowed]; return modifiedCachedResponse; } return cachedResponse;
HTTP- .
, ( , , ). , ( cacheTime ) .
— ( ). , , .
:
(, , ..); - NSURLCache , .
:
30 ; ; : 200- , NSURLConnection .
""
API (, ), fields :
. . . , .
, Player , JSON-, . , fields , NSDictionary . , - . , .
JSON -> NSObject Mantle , API ( RACSignal API ):
- (RACSignal *)searchPlayers:(NSString *)query limit:(NSUInteger)limit { NSMutableDictionary *parameters = [NSMutableDictionary dictionary]; parameters[@"search"] = query; if (limit) parameters[@"limit"] = @(limit); return [self getPath:@"account/list" parameters:parameters resultClass:WOTSearchPlayer.class]; }
, resultClass , ? :
- (RACSignal *)searchPlayers:(NSString *)query limit:(NSUInteger)limit resultClass:(Class)resultClass
, API, .
API , : , , "" . ( ). , ( AFJSONRequestOperation ):
op1 = [AFJSONRequestOperation JSONRequestOperationWithRequest:nil success:^(id JSON) { op2 = [AFJSONRequestOperation JSONRequestOperationWithRequest:nil success:^(id JSON) { op3 = [AFJSONRequestOperation JSONRequestOperationWithRequest:nil success:^(id JSON) { // Combine all the responses and return } failure:^(NSError *error) { // Handle error }]; } failure:^(NSError *error) { // Handle error }]; } failure:^(NSError *error) { // Handle error }];
, , . ReactiveCocoa , .
API :
- (RACSignal *)searchPlayers:(NSString *)query limit:(NSUInteger)limit; - (RACSignal *)fetchPlayers:(NSArray *)playerIDs;
RACSignal — , , . — , . / :
RACSignal *fetchPlayersAndRatings = [[[[API searchPlayers:@"" limit:0] flattenMap:^RACStream *(id players) { // skipping data processing return [API fetchPlayers:@[]]; }] flattenMap:^RACStream *(id fullPlayers) { // Skipping data processing return [API statsSliceForPlayer:nil hoursAgo:@[] fields:nil]; }] flattenMap:^RACStream *(id value) { // Compose all responses here id composedResponse; return [RACSignal return:composedResponse]; }]; [fetchPlayersAndRatings subscribeNext:^(id x) { // Fully composed response } error:^(NSError *error) { // All erros go to one place }];
ReactiveCocoa — , . , Promises Futures.
:
Core ( , ); WOT ( World of Tanks–); WOWP ( World of Warplanes–)
, , — . , ( ) CocoaPods , . subspecs , :
# Subspecs s.subspec 'Core' do |cs| cs.source_files = 'WGNAPIClient/Core/**/*.{h,m}' end s.subspec 'WOT' do |cs| cs.dependency 'WGNAPIClient/Core' cs.source_files = 'WGNAPIClient/WOT/**/*.{h,m}' end s.subspec 'WOWP' do |cs| cs.dependency 'WGNAPIClient/Core' cs.source_files = 'WGNAPIClient/WOWP/**/*.{h,m}' end
"" "" , :
pod 'WGNAPIClient/WOT' pod 'WGNAPIClient/WOWP'
. .
(98%). :
; .
:
context(@"API works with players data", ^{ it (@"should search players", ^{ stubResponse(@"/wot/account/list", @"players-search.json"); RACSignal *searchRequest = [client searchPlayers:@"garnett" limit:0]; NSArray *response = [searchRequest asynchronousFirstOrDefault:nil success:NULL error:&error]; [[error should] beNil]; [[response should] beKindOfClass:NSArray.class]; [[response.lastObject should] beKindOfClass:WOTSearchPlayer.class]; WOTSearchPlayer *player = response[0]; [[player.ID should] equal:@"1785514"]; }); });
, / , RACSignal , :
(id)asynchronousFirstOrDefault:(id)defaultValue success:(BOOL *)success error:(NSError **)error;
describe(@"WOTRatingType", ^{ it(@"should parse json to model object", ^{ NSDictionary *json = @{ @"threshold": @5, @"type": @"1", @"rank_fields": @[ @"xp_amount_rank", @"xp_max_rank", @"battles_count_rank", @"spotted_count_rank", @"damage_dealt_rank", ] }; NSError *error; WOTRatingType *ratingType = [MTLJSONAdapter modelOfClass:WOTRatingType.class fromJSONDictionary:json error:&error]; [[error should] beNil]; [[ratingType should] beKindOfClass:WOTRatingType.class]; [[json[@"threshold"] should] equal:theValue(ratingType.threshold)]; * [[json[@"type"] should] equal:ratingType.type]; [[ratingType.rankFields shouldNot] beNil]; [[ratingType.rankFields should] beKindOfClass:NSArray.class]; [[ratingType.rankFields should] haveCountOf:5]; [[@"maximumXP" should] equal:ratingType.rankFields[1]]; }); });
* — - - Kiwi, , nil .
API :
; .
, , : query http-. 
, ReactiveCocoa , ( ). ( ) 2k LOC ( ~1k - , ~700 - ), : — .     . 
 
 Kiwi 
  BDD-    RSpec -  ,   .   «--». 
 
 OHTTPStubs 
   ,      web-   .      "",      - . 
 
     . 
 
    
 There are only two hard things in Computer Science: cache invalidation and naming things. 
 
 -- Phil Karlton 
  « »  : 
 
                .. 
 ,            . 
       -     inMemory CoreData ,       NSCache .   ,       ,         —         .         NSURLConnection . 
 
 NSURLCache 
 NSURLCache        NSURLRequest   .      ,    . 
 
   : 
 NSURLCache *urlCache = [[NSURLCache alloc] initWithMemoryCapacity:kInMemoryCacheSize diskCapacity:kOnDiskCacheSize diskPath:path]; [NSURLCache setSharedURLCache:urlCache]; 
     ,           . 
 
  NSURLConnectionDelegate   ,     ""    : 
 - (NSCachedURLResponse *)connection:(NSURLConnection *)connection willCacheResponse:(NSCachedURLResponse *)cachedResponse; 
    ? 
 
  NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse*)[cachedResponse response]; NSDictionary *headers = [httpResponse allHeaderFields]; NSString *cacheControl = [headers valueForKey:@"Cache-Control"]; NSString *expires = [headers valueForKey:@"Expires"]; if((cacheControl == nil) && (expires == nil)) { NSMutableDictionary *modifiedHeaders = [httpResponse.allHeaderFields mutableCopy]; NSString *expireDate = [NSDate RFC822StringFromDate:[[NSDate date] dateByAddingTimeInterval:cacheTime]]; NSDictionary *values = @{@"Expires": expireDate, @"Cache-Control": @"public"}; [modifiedHeaders addEntriesFromDictionary:values]; NSHTTPURLResponse *response = [[NSHTTPURLResponse alloc] initWithURL:httpResponse.URL statusCode:httpResponse.statusCode HTTPVersion:@"HTTP/1.1" headerFields:modifiedHeaders]; NSCachedURLResponse *modifiedCachedResponse = [[NSCachedURLResponse alloc] initWithResponse:response data:cachedResponse.data userInfo:cachedResponse.userInfo storagePolicy:NSURLCacheStorageAllowed]; return modifiedCachedResponse; } return cachedResponse; 
      HTTP-      . 
 
 ,     (    ,    ,            ).   ,     ( cacheTime )   . 
     —        (      ).    ,      ,    . 
 
 : 
 
        (, ,   ..);  -   NSURLCache ,   . 
 : 
     30  ;       ; :         200- , NSURLConnection   . 
  ""  
          API (,    ),     fields : 
 
   .   .    .    ,   . 
  ,   Player ,      JSON-,   .        ,       fields ,        NSDictionary .         ,  -       .          ,           . 
   JSON -> NSObject    Mantle ,     API      ( RACSignal   API      ): 
 
 - (RACSignal *)searchPlayers:(NSString *)query limit:(NSUInteger)limit { NSMutableDictionary *parameters = [NSMutableDictionary dictionary]; parameters[@"search"] = query; if (limit) parameters[@"limit"] = @(limit); return [self getPath:@"account/list" parameters:parameters resultClass:WOTSearchPlayer.class]; } 
  ,       resultClass ,         ? : 
 
  - (RACSignal *)searchPlayers:(NSString *)query limit:(NSUInteger)limit resultClass:(Class)resultClass 
 ,     API,           . 
 
       
     API     ,               : ,   ,     ""    . (    ).    ,       (   AFJSONRequestOperation ): 
 
  op1 = [AFJSONRequestOperation JSONRequestOperationWithRequest:nil success:^(id JSON) { op2 = [AFJSONRequestOperation JSONRequestOperationWithRequest:nil success:^(id JSON) { op3 = [AFJSONRequestOperation JSONRequestOperationWithRequest:nil success:^(id JSON) { // Combine all the responses and return } failure:^(NSError *error) { // Handle error }]; } failure:^(NSError *error) { // Handle error }]; } failure:^(NSError *error) { // Handle error }]; 
  ,    ,          .         ReactiveCocoa  ,       . 
  API    : 
 
 - (RACSignal *)searchPlayers:(NSString *)query limit:(NSUInteger)limit; - (RACSignal *)fetchPlayers:(NSArray *)playerIDs; 
 RACSignal —  ,       ,     .    —       ,    .          / : 
 
  RACSignal *fetchPlayersAndRatings = [[[[API searchPlayers:@"" limit:0] flattenMap:^RACStream *(id players) { // skipping data processing return [API fetchPlayers:@[]]; }] flattenMap:^RACStream *(id fullPlayers) { // Skipping data processing return [API statsSliceForPlayer:nil hoursAgo:@[] fields:nil]; }] flattenMap:^RACStream *(id value) { // Compose all responses here id composedResponse; return [RACSignal return:composedResponse]; }]; [fetchPlayersAndRatings subscribeNext:^(id x) { // Fully composed response } error:^(NSError *error) { // All erros go to one place }]; 
  ReactiveCocoa   —      ,      .        ,         Promises  Futures. 
 
      
        : 
 Core ( , ); WOT (    World of Tanks–); WOWP (    World of Warplanes–) 
  ,         ,      — . ,    (    )       CocoaPods ,       .       subspecs ,        : 
 
  # Subspecs s.subspec 'Core' do |cs| cs.source_files = 'WGNAPIClient/Core/**/*.{h,m}' end s.subspec 'WOT' do |cs| cs.dependency 'WGNAPIClient/Core' cs.source_files = 'WGNAPIClient/WOT/**/*.{h,m}' end s.subspec 'WOWP' do |cs| cs.dependency 'WGNAPIClient/Core' cs.source_files = 'WGNAPIClient/WOWP/**/*.{h,m}' end 
 
    ""  ""  ,      : 
 pod 'WGNAPIClient/WOT' pod 'WGNAPIClient/WOWP' 
     
              .        . 
         (98%).           : 
 
   ;    . 
   
     : 
  context(@"API works with players data", ^{ it (@"should search players", ^{ stubResponse(@"/wot/account/list", @"players-search.json"); RACSignal *searchRequest = [client searchPlayers:@"garnett" limit:0]; NSArray *response = [searchRequest asynchronousFirstOrDefault:nil success:NULL error:&error]; [[error should] beNil]; [[response should] beKindOfClass:NSArray.class]; [[response.lastObject should] beKindOfClass:WOTSearchPlayer.class]; WOTSearchPlayer *player = response[0]; [[player.ID should] equal:@"1785514"]; }); }); 
 ,     / ,    RACSignal      ,       : 
 (id)asynchronousFirstOrDefault:(id)defaultValue success:(BOOL *)success error:(NSError **)error; 
   
 describe(@"WOTRatingType", ^{ it(@"should parse json to model object", ^{ NSDictionary *json = @{ @"threshold": @5, @"type": @"1", @"rank_fields": @[ @"xp_amount_rank", @"xp_max_rank", @"battles_count_rank", @"spotted_count_rank", @"damage_dealt_rank", ] }; NSError *error; WOTRatingType *ratingType = [MTLJSONAdapter modelOfClass:WOTRatingType.class fromJSONDictionary:json error:&error]; [[error should] beNil]; [[ratingType should] beKindOfClass:WOTRatingType.class]; [[json[@"threshold"] should] equal:theValue(ratingType.threshold)]; * [[json[@"type"] should] equal:ratingType.type]; [[ratingType.rankFields shouldNot] beNil]; [[ratingType.rankFields should] beKindOfClass:NSArray.class]; [[ratingType.rankFields should] haveCountOf:5]; [[@"maximumXP" should] equal:ratingType.rankFields[1]]; }); }); 
 
 * —    - -    Kiwi,      ,     nil . 
 
       API    : 
    ;   . 
  ,  ,       :  query  http-.   
 
  
    ,      ReactiveCocoa ,         (    ).    ( )   2k LOC (  ~1k -  ,   ~700 -     ),     :        —         . .
Kiwi
BDD- RSpec - , . «--».
OHTTPStubs
, web- . "", - .
.
There are only two hard things in Computer Science: cache invalidation and naming things.
-- Phil Karlton
« » :
..
, .
- inMemory CoreData , NSCache . , , — . NSURLConnection .
NSURLCache
NSURLCache NSURLRequest . , .
:
NSURLCache *urlCache = [[NSURLCache alloc] initWithMemoryCapacity:kInMemoryCacheSize diskCapacity:kOnDiskCacheSize diskPath:path]; [NSURLCache setSharedURLCache:urlCache];
, .
NSURLConnectionDelegate , "" :
- (NSCachedURLResponse *)connection:(NSURLConnection *)connection willCacheResponse:(NSCachedURLResponse *)cachedResponse;
?
NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse*)[cachedResponse response]; NSDictionary *headers = [httpResponse allHeaderFields]; NSString *cacheControl = [headers valueForKey:@"Cache-Control"]; NSString *expires = [headers valueForKey:@"Expires"]; if((cacheControl == nil) && (expires == nil)) { NSMutableDictionary *modifiedHeaders = [httpResponse.allHeaderFields mutableCopy]; NSString *expireDate = [NSDate RFC822StringFromDate:[[NSDate date] dateByAddingTimeInterval:cacheTime]]; NSDictionary *values = @{@"Expires": expireDate, @"Cache-Control": @"public"}; [modifiedHeaders addEntriesFromDictionary:values]; NSHTTPURLResponse *response = [[NSHTTPURLResponse alloc] initWithURL:httpResponse.URL statusCode:httpResponse.statusCode HTTPVersion:@"HTTP/1.1" headerFields:modifiedHeaders]; NSCachedURLResponse *modifiedCachedResponse = [[NSCachedURLResponse alloc] initWithResponse:response data:cachedResponse.data userInfo:cachedResponse.userInfo storagePolicy:NSURLCacheStorageAllowed]; return modifiedCachedResponse; } return cachedResponse;
HTTP- .
, ( , , ). , ( cacheTime ) .
— ( ). , , .
:
(, , ..); - NSURLCache , .
:
30 ; ; : 200- , NSURLConnection .
""
API (, ), fields :
. . . , .
, Player , JSON-, . , fields , NSDictionary . , - . , .
JSON -> NSObject Mantle , API ( RACSignal API ):
- (RACSignal *)searchPlayers:(NSString *)query limit:(NSUInteger)limit { NSMutableDictionary *parameters = [NSMutableDictionary dictionary]; parameters[@"search"] = query; if (limit) parameters[@"limit"] = @(limit); return [self getPath:@"account/list" parameters:parameters resultClass:WOTSearchPlayer.class]; }
, resultClass , ? :
- (RACSignal *)searchPlayers:(NSString *)query limit:(NSUInteger)limit resultClass:(Class)resultClass
, API, .
API , : , , "" . ( ). , ( AFJSONRequestOperation ):
op1 = [AFJSONRequestOperation JSONRequestOperationWithRequest:nil success:^(id JSON) { op2 = [AFJSONRequestOperation JSONRequestOperationWithRequest:nil success:^(id JSON) { op3 = [AFJSONRequestOperation JSONRequestOperationWithRequest:nil success:^(id JSON) { // Combine all the responses and return } failure:^(NSError *error) { // Handle error }]; } failure:^(NSError *error) { // Handle error }]; } failure:^(NSError *error) { // Handle error }];
, , . ReactiveCocoa , .
API :
- (RACSignal *)searchPlayers:(NSString *)query limit:(NSUInteger)limit; - (RACSignal *)fetchPlayers:(NSArray *)playerIDs;
RACSignal — , , . — , . / :
RACSignal *fetchPlayersAndRatings = [[[[API searchPlayers:@"" limit:0] flattenMap:^RACStream *(id players) { // skipping data processing return [API fetchPlayers:@[]]; }] flattenMap:^RACStream *(id fullPlayers) { // Skipping data processing return [API statsSliceForPlayer:nil hoursAgo:@[] fields:nil]; }] flattenMap:^RACStream *(id value) { // Compose all responses here id composedResponse; return [RACSignal return:composedResponse]; }]; [fetchPlayersAndRatings subscribeNext:^(id x) { // Fully composed response } error:^(NSError *error) { // All erros go to one place }];
ReactiveCocoa — , . , Promises Futures.
:
Core ( , ); WOT ( World of Tanks–); WOWP ( World of Warplanes–)
, , — . , ( ) CocoaPods , . subspecs , :
# Subspecs s.subspec 'Core' do |cs| cs.source_files = 'WGNAPIClient/Core/**/*.{h,m}' end s.subspec 'WOT' do |cs| cs.dependency 'WGNAPIClient/Core' cs.source_files = 'WGNAPIClient/WOT/**/*.{h,m}' end s.subspec 'WOWP' do |cs| cs.dependency 'WGNAPIClient/Core' cs.source_files = 'WGNAPIClient/WOWP/**/*.{h,m}' end
"" "" , :
pod 'WGNAPIClient/WOT' pod 'WGNAPIClient/WOWP'
. .
(98%). :
; .
:
context(@"API works with players data", ^{ it (@"should search players", ^{ stubResponse(@"/wot/account/list", @"players-search.json"); RACSignal *searchRequest = [client searchPlayers:@"garnett" limit:0]; NSArray *response = [searchRequest asynchronousFirstOrDefault:nil success:NULL error:&error]; [[error should] beNil]; [[response should] beKindOfClass:NSArray.class]; [[response.lastObject should] beKindOfClass:WOTSearchPlayer.class]; WOTSearchPlayer *player = response[0]; [[player.ID should] equal:@"1785514"]; }); });
, / , RACSignal , :
(id)asynchronousFirstOrDefault:(id)defaultValue success:(BOOL *)success error:(NSError **)error;
describe(@"WOTRatingType", ^{ it(@"should parse json to model object", ^{ NSDictionary *json = @{ @"threshold": @5, @"type": @"1", @"rank_fields": @[ @"xp_amount_rank", @"xp_max_rank", @"battles_count_rank", @"spotted_count_rank", @"damage_dealt_rank", ] }; NSError *error; WOTRatingType *ratingType = [MTLJSONAdapter modelOfClass:WOTRatingType.class fromJSONDictionary:json error:&error]; [[error should] beNil]; [[ratingType should] beKindOfClass:WOTRatingType.class]; [[json[@"threshold"] should] equal:theValue(ratingType.threshold)]; * [[json[@"type"] should] equal:ratingType.type]; [[ratingType.rankFields shouldNot] beNil]; [[ratingType.rankFields should] beKindOfClass:NSArray.class]; [[ratingType.rankFields should] haveCountOf:5]; [[@"maximumXP" should] equal:ratingType.rankFields[1]]; }); });
* — - - Kiwi, , nil .
API :
; .
, , : query http-. 
, ReactiveCocoa , ( ). ( ) 2k LOC ( ~1k - , ~700 - ), : — ..
Kiwi
BDD- RSpec - , . «--».
OHTTPStubs
, web- . "", - .
.
There are only two hard things in Computer Science: cache invalidation and naming things.
-- Phil Karlton
« » :
..
, .
- inMemory CoreData , NSCache . , , — . NSURLConnection .
NSURLCache
NSURLCache NSURLRequest . , .
:
NSURLCache *urlCache = [[NSURLCache alloc] initWithMemoryCapacity:kInMemoryCacheSize diskCapacity:kOnDiskCacheSize diskPath:path]; [NSURLCache setSharedURLCache:urlCache];
, .
NSURLConnectionDelegate , "" :
- (NSCachedURLResponse *)connection:(NSURLConnection *)connection willCacheResponse:(NSCachedURLResponse *)cachedResponse;
?
NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse*)[cachedResponse response]; NSDictionary *headers = [httpResponse allHeaderFields]; NSString *cacheControl = [headers valueForKey:@"Cache-Control"]; NSString *expires = [headers valueForKey:@"Expires"]; if((cacheControl == nil) && (expires == nil)) { NSMutableDictionary *modifiedHeaders = [httpResponse.allHeaderFields mutableCopy]; NSString *expireDate = [NSDate RFC822StringFromDate:[[NSDate date] dateByAddingTimeInterval:cacheTime]]; NSDictionary *values = @{@"Expires": expireDate, @"Cache-Control": @"public"}; [modifiedHeaders addEntriesFromDictionary:values]; NSHTTPURLResponse *response = [[NSHTTPURLResponse alloc] initWithURL:httpResponse.URL statusCode:httpResponse.statusCode HTTPVersion:@"HTTP/1.1" headerFields:modifiedHeaders]; NSCachedURLResponse *modifiedCachedResponse = [[NSCachedURLResponse alloc] initWithResponse:response data:cachedResponse.data userInfo:cachedResponse.userInfo storagePolicy:NSURLCacheStorageAllowed]; return modifiedCachedResponse; } return cachedResponse;
HTTP- .
, ( , , ). , ( cacheTime ) .
— ( ). , , .
:
(, , ..); - NSURLCache , .
:
30 ; ; : 200- , NSURLConnection .
""
API (, ), fields :
. . . , .
, Player , JSON-, . , fields , NSDictionary . , - . , .
JSON -> NSObject Mantle , API ( RACSignal API ):
- (RACSignal *)searchPlayers:(NSString *)query limit:(NSUInteger)limit { NSMutableDictionary *parameters = [NSMutableDictionary dictionary]; parameters[@"search"] = query; if (limit) parameters[@"limit"] = @(limit); return [self getPath:@"account/list" parameters:parameters resultClass:WOTSearchPlayer.class]; }
, resultClass , ? :
- (RACSignal *)searchPlayers:(NSString *)query limit:(NSUInteger)limit resultClass:(Class)resultClass
, API, .
API , : , , "" . ( ). , ( AFJSONRequestOperation ):
op1 = [AFJSONRequestOperation JSONRequestOperationWithRequest:nil success:^(id JSON) { op2 = [AFJSONRequestOperation JSONRequestOperationWithRequest:nil success:^(id JSON) { op3 = [AFJSONRequestOperation JSONRequestOperationWithRequest:nil success:^(id JSON) { // Combine all the responses and return } failure:^(NSError *error) { // Handle error }]; } failure:^(NSError *error) { // Handle error }]; } failure:^(NSError *error) { // Handle error }];
, , . ReactiveCocoa , .
API :
- (RACSignal *)searchPlayers:(NSString *)query limit:(NSUInteger)limit; - (RACSignal *)fetchPlayers:(NSArray *)playerIDs;
RACSignal — , , . — , . / :
RACSignal *fetchPlayersAndRatings = [[[[API searchPlayers:@"" limit:0] flattenMap:^RACStream *(id players) { // skipping data processing return [API fetchPlayers:@[]]; }] flattenMap:^RACStream *(id fullPlayers) { // Skipping data processing return [API statsSliceForPlayer:nil hoursAgo:@[] fields:nil]; }] flattenMap:^RACStream *(id value) { // Compose all responses here id composedResponse; return [RACSignal return:composedResponse]; }]; [fetchPlayersAndRatings subscribeNext:^(id x) { // Fully composed response } error:^(NSError *error) { // All erros go to one place }];
ReactiveCocoa — , . , Promises Futures.
:
Core ( , ); WOT ( World of Tanks–); WOWP ( World of Warplanes–)
, , — . , ( ) CocoaPods , . subspecs , :
# Subspecs s.subspec 'Core' do |cs| cs.source_files = 'WGNAPIClient/Core/**/*.{h,m}' end s.subspec 'WOT' do |cs| cs.dependency 'WGNAPIClient/Core' cs.source_files = 'WGNAPIClient/WOT/**/*.{h,m}' end s.subspec 'WOWP' do |cs| cs.dependency 'WGNAPIClient/Core' cs.source_files = 'WGNAPIClient/WOWP/**/*.{h,m}' end
"" "" , :
pod 'WGNAPIClient/WOT' pod 'WGNAPIClient/WOWP'
. .
(98%). :
; .
:
context(@"API works with players data", ^{ it (@"should search players", ^{ stubResponse(@"/wot/account/list", @"players-search.json"); RACSignal *searchRequest = [client searchPlayers:@"garnett" limit:0]; NSArray *response = [searchRequest asynchronousFirstOrDefault:nil success:NULL error:&error]; [[error should] beNil]; [[response should] beKindOfClass:NSArray.class]; [[response.lastObject should] beKindOfClass:WOTSearchPlayer.class]; WOTSearchPlayer *player = response[0]; [[player.ID should] equal:@"1785514"]; }); });
, / , RACSignal , :
(id)asynchronousFirstOrDefault:(id)defaultValue success:(BOOL *)success error:(NSError **)error;
describe(@"WOTRatingType", ^{ it(@"should parse json to model object", ^{ NSDictionary *json = @{ @"threshold": @5, @"type": @"1", @"rank_fields": @[ @"xp_amount_rank", @"xp_max_rank", @"battles_count_rank", @"spotted_count_rank", @"damage_dealt_rank", ] }; NSError *error; WOTRatingType *ratingType = [MTLJSONAdapter modelOfClass:WOTRatingType.class fromJSONDictionary:json error:&error]; [[error should] beNil]; [[ratingType should] beKindOfClass:WOTRatingType.class]; [[json[@"threshold"] should] equal:theValue(ratingType.threshold)]; * [[json[@"type"] should] equal:ratingType.type]; [[ratingType.rankFields shouldNot] beNil]; [[ratingType.rankFields should] beKindOfClass:NSArray.class]; [[ratingType.rankFields should] haveCountOf:5]; [[@"maximumXP" should] equal:ratingType.rankFields[1]]; }); });
* — - - Kiwi, , nil .
API :
; .
, , : query http-. 
, ReactiveCocoa , ( ). ( ) 2k LOC ( ~1k - , ~700 - ), : — ..
Kiwi
BDD- RSpec - , . «--».
OHTTPStubs
, web- . "", - .
.
There are only two hard things in Computer Science: cache invalidation and naming things.
-- Phil Karlton
« » :
..
, .
- inMemory CoreData , NSCache . , , — . NSURLConnection .
NSURLCache
NSURLCache NSURLRequest . , .
:
NSURLCache *urlCache = [[NSURLCache alloc] initWithMemoryCapacity:kInMemoryCacheSize diskCapacity:kOnDiskCacheSize diskPath:path]; [NSURLCache setSharedURLCache:urlCache];
, .
NSURLConnectionDelegate , "" :
- (NSCachedURLResponse *)connection:(NSURLConnection *)connection willCacheResponse:(NSCachedURLResponse *)cachedResponse;
?
NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse*)[cachedResponse response]; NSDictionary *headers = [httpResponse allHeaderFields]; NSString *cacheControl = [headers valueForKey:@"Cache-Control"]; NSString *expires = [headers valueForKey:@"Expires"]; if((cacheControl == nil) && (expires == nil)) { NSMutableDictionary *modifiedHeaders = [httpResponse.allHeaderFields mutableCopy]; NSString *expireDate = [NSDate RFC822StringFromDate:[[NSDate date] dateByAddingTimeInterval:cacheTime]]; NSDictionary *values = @{@"Expires": expireDate, @"Cache-Control": @"public"}; [modifiedHeaders addEntriesFromDictionary:values]; NSHTTPURLResponse *response = [[NSHTTPURLResponse alloc] initWithURL:httpResponse.URL statusCode:httpResponse.statusCode HTTPVersion:@"HTTP/1.1" headerFields:modifiedHeaders]; NSCachedURLResponse *modifiedCachedResponse = [[NSCachedURLResponse alloc] initWithResponse:response data:cachedResponse.data userInfo:cachedResponse.userInfo storagePolicy:NSURLCacheStorageAllowed]; return modifiedCachedResponse; } return cachedResponse;
HTTP- .
, ( , , ). , ( cacheTime ) .
— ( ). , , .
:
(, , ..); - NSURLCache , .
:
30 ; ; : 200- , NSURLConnection .
""
API (, ), fields :
. . . , .
, Player , JSON-, . , fields , NSDictionary . , - . , .
JSON -> NSObject Mantle , API ( RACSignal API ):
- (RACSignal *)searchPlayers:(NSString *)query limit:(NSUInteger)limit { NSMutableDictionary *parameters = [NSMutableDictionary dictionary]; parameters[@"search"] = query; if (limit) parameters[@"limit"] = @(limit); return [self getPath:@"account/list" parameters:parameters resultClass:WOTSearchPlayer.class]; }
, resultClass , ? :
- (RACSignal *)searchPlayers:(NSString *)query limit:(NSUInteger)limit resultClass:(Class)resultClass
, API, .
API , : , , "" . ( ). , ( AFJSONRequestOperation ):
op1 = [AFJSONRequestOperation JSONRequestOperationWithRequest:nil success:^(id JSON) { op2 = [AFJSONRequestOperation JSONRequestOperationWithRequest:nil success:^(id JSON) { op3 = [AFJSONRequestOperation JSONRequestOperationWithRequest:nil success:^(id JSON) { // Combine all the responses and return } failure:^(NSError *error) { // Handle error }]; } failure:^(NSError *error) { // Handle error }]; } failure:^(NSError *error) { // Handle error }];
, , . ReactiveCocoa , .
API :
- (RACSignal *)searchPlayers:(NSString *)query limit:(NSUInteger)limit; - (RACSignal *)fetchPlayers:(NSArray *)playerIDs;
RACSignal — , , . — , . / :
RACSignal *fetchPlayersAndRatings = [[[[API searchPlayers:@"" limit:0] flattenMap:^RACStream *(id players) { // skipping data processing return [API fetchPlayers:@[]]; }] flattenMap:^RACStream *(id fullPlayers) { // Skipping data processing return [API statsSliceForPlayer:nil hoursAgo:@[] fields:nil]; }] flattenMap:^RACStream *(id value) { // Compose all responses here id composedResponse; return [RACSignal return:composedResponse]; }]; [fetchPlayersAndRatings subscribeNext:^(id x) { // Fully composed response } error:^(NSError *error) { // All erros go to one place }];
ReactiveCocoa — , . , Promises Futures.
:
Core ( , ); WOT ( World of Tanks–); WOWP ( World of Warplanes–)
, , — . , ( ) CocoaPods , . subspecs , :
# Subspecs s.subspec 'Core' do |cs| cs.source_files = 'WGNAPIClient/Core/**/*.{h,m}' end s.subspec 'WOT' do |cs| cs.dependency 'WGNAPIClient/Core' cs.source_files = 'WGNAPIClient/WOT/**/*.{h,m}' end s.subspec 'WOWP' do |cs| cs.dependency 'WGNAPIClient/Core' cs.source_files = 'WGNAPIClient/WOWP/**/*.{h,m}' end
"" "" , :
pod 'WGNAPIClient/WOT' pod 'WGNAPIClient/WOWP'
. .
(98%). :
; .
:
context(@"API works with players data", ^{ it (@"should search players", ^{ stubResponse(@"/wot/account/list", @"players-search.json"); RACSignal *searchRequest = [client searchPlayers:@"garnett" limit:0]; NSArray *response = [searchRequest asynchronousFirstOrDefault:nil success:NULL error:&error]; [[error should] beNil]; [[response should] beKindOfClass:NSArray.class]; [[response.lastObject should] beKindOfClass:WOTSearchPlayer.class]; WOTSearchPlayer *player = response[0]; [[player.ID should] equal:@"1785514"]; }); });
, / , RACSignal , :
(id)asynchronousFirstOrDefault:(id)defaultValue success:(BOOL *)success error:(NSError **)error;
describe(@"WOTRatingType", ^{ it(@"should parse json to model object", ^{ NSDictionary *json = @{ @"threshold": @5, @"type": @"1", @"rank_fields": @[ @"xp_amount_rank", @"xp_max_rank", @"battles_count_rank", @"spotted_count_rank", @"damage_dealt_rank", ] }; NSError *error; WOTRatingType *ratingType = [MTLJSONAdapter modelOfClass:WOTRatingType.class fromJSONDictionary:json error:&error]; [[error should] beNil]; [[ratingType should] beKindOfClass:WOTRatingType.class]; [[json[@"threshold"] should] equal:theValue(ratingType.threshold)]; * [[json[@"type"] should] equal:ratingType.type]; [[ratingType.rankFields shouldNot] beNil]; [[ratingType.rankFields should] beKindOfClass:NSArray.class]; [[ratingType.rankFields should] haveCountOf:5]; [[@"maximumXP" should] equal:ratingType.rankFields[1]]; }); });
* — - - Kiwi, , nil .
API :
; .
, , : query http-. 
, ReactiveCocoa , ( ). ( ) 2k LOC ( ~1k - , ~700 - ), : — ..
Kiwi
BDD- RSpec - , . «--».
OHTTPStubs
, web- . "", - .
.
There are only two hard things in Computer Science: cache invalidation and naming things.
-- Phil Karlton
« » :
..
, .
- inMemory CoreData , NSCache . , , — . NSURLConnection .
NSURLCache
NSURLCache NSURLRequest . , .
:
NSURLCache *urlCache = [[NSURLCache alloc] initWithMemoryCapacity:kInMemoryCacheSize diskCapacity:kOnDiskCacheSize diskPath:path]; [NSURLCache setSharedURLCache:urlCache];
, .
NSURLConnectionDelegate , "" :
- (NSCachedURLResponse *)connection:(NSURLConnection *)connection willCacheResponse:(NSCachedURLResponse *)cachedResponse;
?
NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse*)[cachedResponse response]; NSDictionary *headers = [httpResponse allHeaderFields]; NSString *cacheControl = [headers valueForKey:@"Cache-Control"]; NSString *expires = [headers valueForKey:@"Expires"]; if((cacheControl == nil) && (expires == nil)) { NSMutableDictionary *modifiedHeaders = [httpResponse.allHeaderFields mutableCopy]; NSString *expireDate = [NSDate RFC822StringFromDate:[[NSDate date] dateByAddingTimeInterval:cacheTime]]; NSDictionary *values = @{@"Expires": expireDate, @"Cache-Control": @"public"}; [modifiedHeaders addEntriesFromDictionary:values]; NSHTTPURLResponse *response = [[NSHTTPURLResponse alloc] initWithURL:httpResponse.URL statusCode:httpResponse.statusCode HTTPVersion:@"HTTP/1.1" headerFields:modifiedHeaders]; NSCachedURLResponse *modifiedCachedResponse = [[NSCachedURLResponse alloc] initWithResponse:response data:cachedResponse.data userInfo:cachedResponse.userInfo storagePolicy:NSURLCacheStorageAllowed]; return modifiedCachedResponse; } return cachedResponse;
HTTP- .
, ( , , ). , ( cacheTime ) .
— ( ). , , .
:
(, , ..); - NSURLCache , .
:
30 ; ; : 200- , NSURLConnection .
""
API (, ), fields :
. . . , .
, Player , JSON-, . , fields , NSDictionary . , - . , .
JSON -> NSObject Mantle , API ( RACSignal API ):
- (RACSignal *)searchPlayers:(NSString *)query limit:(NSUInteger)limit { NSMutableDictionary *parameters = [NSMutableDictionary dictionary]; parameters[@"search"] = query; if (limit) parameters[@"limit"] = @(limit); return [self getPath:@"account/list" parameters:parameters resultClass:WOTSearchPlayer.class]; }
, resultClass , ? :
- (RACSignal *)searchPlayers:(NSString *)query limit:(NSUInteger)limit resultClass:(Class)resultClass
, API, .
API , : , , "" . ( ). , ( AFJSONRequestOperation ):
op1 = [AFJSONRequestOperation JSONRequestOperationWithRequest:nil success:^(id JSON) { op2 = [AFJSONRequestOperation JSONRequestOperationWithRequest:nil success:^(id JSON) { op3 = [AFJSONRequestOperation JSONRequestOperationWithRequest:nil success:^(id JSON) { // Combine all the responses and return } failure:^(NSError *error) { // Handle error }]; } failure:^(NSError *error) { // Handle error }]; } failure:^(NSError *error) { // Handle error }];
, , . ReactiveCocoa , .
API :
- (RACSignal *)searchPlayers:(NSString *)query limit:(NSUInteger)limit; - (RACSignal *)fetchPlayers:(NSArray *)playerIDs;
RACSignal — , , . — , . / :
RACSignal *fetchPlayersAndRatings = [[[[API searchPlayers:@"" limit:0] flattenMap:^RACStream *(id players) { // skipping data processing return [API fetchPlayers:@[]]; }] flattenMap:^RACStream *(id fullPlayers) { // Skipping data processing return [API statsSliceForPlayer:nil hoursAgo:@[] fields:nil]; }] flattenMap:^RACStream *(id value) { // Compose all responses here id composedResponse; return [RACSignal return:composedResponse]; }]; [fetchPlayersAndRatings subscribeNext:^(id x) { // Fully composed response } error:^(NSError *error) { // All erros go to one place }];
ReactiveCocoa — , . , Promises Futures.
:
Core ( , ); WOT ( World of Tanks–); WOWP ( World of Warplanes–)
, , — . , ( ) CocoaPods , . subspecs , :
# Subspecs s.subspec 'Core' do |cs| cs.source_files = 'WGNAPIClient/Core/**/*.{h,m}' end s.subspec 'WOT' do |cs| cs.dependency 'WGNAPIClient/Core' cs.source_files = 'WGNAPIClient/WOT/**/*.{h,m}' end s.subspec 'WOWP' do |cs| cs.dependency 'WGNAPIClient/Core' cs.source_files = 'WGNAPIClient/WOWP/**/*.{h,m}' end
"" "" , :
pod 'WGNAPIClient/WOT' pod 'WGNAPIClient/WOWP'
. .
(98%). :
; .
:
context(@"API works with players data", ^{ it (@"should search players", ^{ stubResponse(@"/wot/account/list", @"players-search.json"); RACSignal *searchRequest = [client searchPlayers:@"garnett" limit:0]; NSArray *response = [searchRequest asynchronousFirstOrDefault:nil success:NULL error:&error]; [[error should] beNil]; [[response should] beKindOfClass:NSArray.class]; [[response.lastObject should] beKindOfClass:WOTSearchPlayer.class]; WOTSearchPlayer *player = response[0]; [[player.ID should] equal:@"1785514"]; }); });
, / , RACSignal , :
(id)asynchronousFirstOrDefault:(id)defaultValue success:(BOOL *)success error:(NSError **)error;
describe(@"WOTRatingType", ^{ it(@"should parse json to model object", ^{ NSDictionary *json = @{ @"threshold": @5, @"type": @"1", @"rank_fields": @[ @"xp_amount_rank", @"xp_max_rank", @"battles_count_rank", @"spotted_count_rank", @"damage_dealt_rank", ] }; NSError *error; WOTRatingType *ratingType = [MTLJSONAdapter modelOfClass:WOTRatingType.class fromJSONDictionary:json error:&error]; [[error should] beNil]; [[ratingType should] beKindOfClass:WOTRatingType.class]; [[json[@"threshold"] should] equal:theValue(ratingType.threshold)]; * [[json[@"type"] should] equal:ratingType.type]; [[ratingType.rankFields shouldNot] beNil]; [[ratingType.rankFields should] beKindOfClass:NSArray.class]; [[ratingType.rankFields should] haveCountOf:5]; [[@"maximumXP" should] equal:ratingType.rankFields[1]]; }); });
* — - - Kiwi, , nil .
API :
; .
, , : query http-. 
, ReactiveCocoa , ( ). ( ) 2k LOC ( ~1k - , ~700 - ), : — ..
Kiwi
BDD- RSpec - , . «--».
OHTTPStubs
, web- . "", - .
.
There are only two hard things in Computer Science: cache invalidation and naming things.
-- Phil Karlton
« » :
..
, .
- inMemory CoreData , NSCache . , , — . NSURLConnection .
NSURLCache
NSURLCache NSURLRequest . , .
:
NSURLCache *urlCache = [[NSURLCache alloc] initWithMemoryCapacity:kInMemoryCacheSize diskCapacity:kOnDiskCacheSize diskPath:path]; [NSURLCache setSharedURLCache:urlCache];
, .
NSURLConnectionDelegate , "" :
- (NSCachedURLResponse *)connection:(NSURLConnection *)connection willCacheResponse:(NSCachedURLResponse *)cachedResponse;
?
NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse*)[cachedResponse response]; NSDictionary *headers = [httpResponse allHeaderFields]; NSString *cacheControl = [headers valueForKey:@"Cache-Control"]; NSString *expires = [headers valueForKey:@"Expires"]; if((cacheControl == nil) && (expires == nil)) { NSMutableDictionary *modifiedHeaders = [httpResponse.allHeaderFields mutableCopy]; NSString *expireDate = [NSDate RFC822StringFromDate:[[NSDate date] dateByAddingTimeInterval:cacheTime]]; NSDictionary *values = @{@"Expires": expireDate, @"Cache-Control": @"public"}; [modifiedHeaders addEntriesFromDictionary:values]; NSHTTPURLResponse *response = [[NSHTTPURLResponse alloc] initWithURL:httpResponse.URL statusCode:httpResponse.statusCode HTTPVersion:@"HTTP/1.1" headerFields:modifiedHeaders]; NSCachedURLResponse *modifiedCachedResponse = [[NSCachedURLResponse alloc] initWithResponse:response data:cachedResponse.data userInfo:cachedResponse.userInfo storagePolicy:NSURLCacheStorageAllowed]; return modifiedCachedResponse; } return cachedResponse;
HTTP- .
, ( , , ). , ( cacheTime ) .
— ( ). , , .
:
(, , ..); - NSURLCache , .
:
30 ; ; : 200- , NSURLConnection .
""
API (, ), fields :
. . . , .
, Player , JSON-, . , fields , NSDictionary . , - . , .
JSON -> NSObject Mantle , API ( RACSignal API ):
- (RACSignal *)searchPlayers:(NSString *)query limit:(NSUInteger)limit { NSMutableDictionary *parameters = [NSMutableDictionary dictionary]; parameters[@"search"] = query; if (limit) parameters[@"limit"] = @(limit); return [self getPath:@"account/list" parameters:parameters resultClass:WOTSearchPlayer.class]; }
, resultClass , ? :
- (RACSignal *)searchPlayers:(NSString *)query limit:(NSUInteger)limit resultClass:(Class)resultClass
, API, .
API , : , , "" . ( ). , ( AFJSONRequestOperation ):
op1 = [AFJSONRequestOperation JSONRequestOperationWithRequest:nil success:^(id JSON) { op2 = [AFJSONRequestOperation JSONRequestOperationWithRequest:nil success:^(id JSON) { op3 = [AFJSONRequestOperation JSONRequestOperationWithRequest:nil success:^(id JSON) { // Combine all the responses and return } failure:^(NSError *error) { // Handle error }]; } failure:^(NSError *error) { // Handle error }]; } failure:^(NSError *error) { // Handle error }];
, , . ReactiveCocoa , .
API :
- (RACSignal *)searchPlayers:(NSString *)query limit:(NSUInteger)limit; - (RACSignal *)fetchPlayers:(NSArray *)playerIDs;
RACSignal — , , . — , . / :
RACSignal *fetchPlayersAndRatings = [[[[API searchPlayers:@"" limit:0] flattenMap:^RACStream *(id players) { // skipping data processing return [API fetchPlayers:@[]]; }] flattenMap:^RACStream *(id fullPlayers) { // Skipping data processing return [API statsSliceForPlayer:nil hoursAgo:@[] fields:nil]; }] flattenMap:^RACStream *(id value) { // Compose all responses here id composedResponse; return [RACSignal return:composedResponse]; }]; [fetchPlayersAndRatings subscribeNext:^(id x) { // Fully composed response } error:^(NSError *error) { // All erros go to one place }];
ReactiveCocoa — , . , Promises Futures.
:
Core ( , ); WOT ( World of Tanks–); WOWP ( World of Warplanes–)
, , — . , ( ) CocoaPods , . subspecs , :
# Subspecs s.subspec 'Core' do |cs| cs.source_files = 'WGNAPIClient/Core/**/*.{h,m}' end s.subspec 'WOT' do |cs| cs.dependency 'WGNAPIClient/Core' cs.source_files = 'WGNAPIClient/WOT/**/*.{h,m}' end s.subspec 'WOWP' do |cs| cs.dependency 'WGNAPIClient/Core' cs.source_files = 'WGNAPIClient/WOWP/**/*.{h,m}' end
"" "" , :
pod 'WGNAPIClient/WOT' pod 'WGNAPIClient/WOWP'
. .
(98%). :
; .
:
context(@"API works with players data", ^{ it (@"should search players", ^{ stubResponse(@"/wot/account/list", @"players-search.json"); RACSignal *searchRequest = [client searchPlayers:@"garnett" limit:0]; NSArray *response = [searchRequest asynchronousFirstOrDefault:nil success:NULL error:&error]; [[error should] beNil]; [[response should] beKindOfClass:NSArray.class]; [[response.lastObject should] beKindOfClass:WOTSearchPlayer.class]; WOTSearchPlayer *player = response[0]; [[player.ID should] equal:@"1785514"]; }); });
, / , RACSignal , :
(id)asynchronousFirstOrDefault:(id)defaultValue success:(BOOL *)success error:(NSError **)error;
describe(@"WOTRatingType", ^{ it(@"should parse json to model object", ^{ NSDictionary *json = @{ @"threshold": @5, @"type": @"1", @"rank_fields": @[ @"xp_amount_rank", @"xp_max_rank", @"battles_count_rank", @"spotted_count_rank", @"damage_dealt_rank", ] }; NSError *error; WOTRatingType *ratingType = [MTLJSONAdapter modelOfClass:WOTRatingType.class fromJSONDictionary:json error:&error]; [[error should] beNil]; [[ratingType should] beKindOfClass:WOTRatingType.class]; [[json[@"threshold"] should] equal:theValue(ratingType.threshold)]; * [[json[@"type"] should] equal:ratingType.type]; [[ratingType.rankFields shouldNot] beNil]; [[ratingType.rankFields should] beKindOfClass:NSArray.class]; [[ratingType.rankFields should] haveCountOf:5]; [[@"maximumXP" should] equal:ratingType.rankFields[1]]; }); });
* — - - Kiwi, , nil .
API :
; .
, , : query http-. 
, ReactiveCocoa , ( ). ( ) 2k LOC ( ~1k - , ~700 - ), : — ..
Kiwi
BDD- RSpec - , . «--».
OHTTPStubs
, web- . "", - .
.
There are only two hard things in Computer Science: cache invalidation and naming things.
-- Phil Karlton
« » :
..
, .
- inMemory CoreData , NSCache . , , — . NSURLConnection .
NSURLCache
NSURLCache NSURLRequest . , .
:
NSURLCache *urlCache = [[NSURLCache alloc] initWithMemoryCapacity:kInMemoryCacheSize diskCapacity:kOnDiskCacheSize diskPath:path]; [NSURLCache setSharedURLCache:urlCache];
, .
NSURLConnectionDelegate , "" :
- (NSCachedURLResponse *)connection:(NSURLConnection *)connection willCacheResponse:(NSCachedURLResponse *)cachedResponse;
?
NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse*)[cachedResponse response]; NSDictionary *headers = [httpResponse allHeaderFields]; NSString *cacheControl = [headers valueForKey:@"Cache-Control"]; NSString *expires = [headers valueForKey:@"Expires"]; if((cacheControl == nil) && (expires == nil)) { NSMutableDictionary *modifiedHeaders = [httpResponse.allHeaderFields mutableCopy]; NSString *expireDate = [NSDate RFC822StringFromDate:[[NSDate date] dateByAddingTimeInterval:cacheTime]]; NSDictionary *values = @{@"Expires": expireDate, @"Cache-Control": @"public"}; [modifiedHeaders addEntriesFromDictionary:values]; NSHTTPURLResponse *response = [[NSHTTPURLResponse alloc] initWithURL:httpResponse.URL statusCode:httpResponse.statusCode HTTPVersion:@"HTTP/1.1" headerFields:modifiedHeaders]; NSCachedURLResponse *modifiedCachedResponse = [[NSCachedURLResponse alloc] initWithResponse:response data:cachedResponse.data userInfo:cachedResponse.userInfo storagePolicy:NSURLCacheStorageAllowed]; return modifiedCachedResponse; } return cachedResponse;
HTTP- .
, ( , , ). , ( cacheTime ) .
— ( ). , , .
:
(, , ..); - NSURLCache , .
:
30 ; ; : 200- , NSURLConnection .
""
API (, ), fields :
. . . , .
, Player , JSON-, . , fields , NSDictionary . , - . , .
JSON -> NSObject Mantle , API ( RACSignal API ):
- (RACSignal *)searchPlayers:(NSString *)query limit:(NSUInteger)limit { NSMutableDictionary *parameters = [NSMutableDictionary dictionary]; parameters[@"search"] = query; if (limit) parameters[@"limit"] = @(limit); return [self getPath:@"account/list" parameters:parameters resultClass:WOTSearchPlayer.class]; }
, resultClass , ? :
- (RACSignal *)searchPlayers:(NSString *)query limit:(NSUInteger)limit resultClass:(Class)resultClass
, API, .
API , : , , "" . ( ). , ( AFJSONRequestOperation ):
op1 = [AFJSONRequestOperation JSONRequestOperationWithRequest:nil success:^(id JSON) { op2 = [AFJSONRequestOperation JSONRequestOperationWithRequest:nil success:^(id JSON) { op3 = [AFJSONRequestOperation JSONRequestOperationWithRequest:nil success:^(id JSON) { // Combine all the responses and return } failure:^(NSError *error) { // Handle error }]; } failure:^(NSError *error) { // Handle error }]; } failure:^(NSError *error) { // Handle error }];
, , . ReactiveCocoa , .
API :
- (RACSignal *)searchPlayers:(NSString *)query limit:(NSUInteger)limit; - (RACSignal *)fetchPlayers:(NSArray *)playerIDs;
RACSignal — , , . — , . / :
RACSignal *fetchPlayersAndRatings = [[[[API searchPlayers:@"" limit:0] flattenMap:^RACStream *(id players) { // skipping data processing return [API fetchPlayers:@[]]; }] flattenMap:^RACStream *(id fullPlayers) { // Skipping data processing return [API statsSliceForPlayer:nil hoursAgo:@[] fields:nil]; }] flattenMap:^RACStream *(id value) { // Compose all responses here id composedResponse; return [RACSignal return:composedResponse]; }]; [fetchPlayersAndRatings subscribeNext:^(id x) { // Fully composed response } error:^(NSError *error) { // All erros go to one place }];
ReactiveCocoa — , . , Promises Futures.
:
Core ( , ); WOT ( World of Tanks–); WOWP ( World of Warplanes–)
, , — . , ( ) CocoaPods , . subspecs , :
# Subspecs s.subspec 'Core' do |cs| cs.source_files = 'WGNAPIClient/Core/**/*.{h,m}' end s.subspec 'WOT' do |cs| cs.dependency 'WGNAPIClient/Core' cs.source_files = 'WGNAPIClient/WOT/**/*.{h,m}' end s.subspec 'WOWP' do |cs| cs.dependency 'WGNAPIClient/Core' cs.source_files = 'WGNAPIClient/WOWP/**/*.{h,m}' end
"" "" , :
pod 'WGNAPIClient/WOT' pod 'WGNAPIClient/WOWP'
. .
(98%). :
; .
:
context(@"API works with players data", ^{ it (@"should search players", ^{ stubResponse(@"/wot/account/list", @"players-search.json"); RACSignal *searchRequest = [client searchPlayers:@"garnett" limit:0]; NSArray *response = [searchRequest asynchronousFirstOrDefault:nil success:NULL error:&error]; [[error should] beNil]; [[response should] beKindOfClass:NSArray.class]; [[response.lastObject should] beKindOfClass:WOTSearchPlayer.class]; WOTSearchPlayer *player = response[0]; [[player.ID should] equal:@"1785514"]; }); });
, / , RACSignal , :
(id)asynchronousFirstOrDefault:(id)defaultValue success:(BOOL *)success error:(NSError **)error;
describe(@"WOTRatingType", ^{ it(@"should parse json to model object", ^{ NSDictionary *json = @{ @"threshold": @5, @"type": @"1", @"rank_fields": @[ @"xp_amount_rank", @"xp_max_rank", @"battles_count_rank", @"spotted_count_rank", @"damage_dealt_rank", ] }; NSError *error; WOTRatingType *ratingType = [MTLJSONAdapter modelOfClass:WOTRatingType.class fromJSONDictionary:json error:&error]; [[error should] beNil]; [[ratingType should] beKindOfClass:WOTRatingType.class]; [[json[@"threshold"] should] equal:theValue(ratingType.threshold)]; * [[json[@"type"] should] equal:ratingType.type]; [[ratingType.rankFields shouldNot] beNil]; [[ratingType.rankFields should] beKindOfClass:NSArray.class]; [[ratingType.rankFields should] haveCountOf:5]; [[@"maximumXP" should] equal:ratingType.rankFields[1]]; }); });
* — - - Kiwi, , nil .
API :
; .
, , : query http-. 
, ReactiveCocoa , ( ). ( ) 2k LOC ( ~1k - , ~700 - ), : — ..
Kiwi
BDD- RSpec - , . «--».
OHTTPStubs
, web- . "", - .
.
There are only two hard things in Computer Science: cache invalidation and naming things.
-- Phil Karlton
« » :
..
, .
- inMemory CoreData , NSCache . , , — . NSURLConnection .
NSURLCache
NSURLCache NSURLRequest . , .
:
NSURLCache *urlCache = [[NSURLCache alloc] initWithMemoryCapacity:kInMemoryCacheSize diskCapacity:kOnDiskCacheSize diskPath:path]; [NSURLCache setSharedURLCache:urlCache];
, .
NSURLConnectionDelegate , "" :
- (NSCachedURLResponse *)connection:(NSURLConnection *)connection willCacheResponse:(NSCachedURLResponse *)cachedResponse;
?
NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse*)[cachedResponse response]; NSDictionary *headers = [httpResponse allHeaderFields]; NSString *cacheControl = [headers valueForKey:@"Cache-Control"]; NSString *expires = [headers valueForKey:@"Expires"]; if((cacheControl == nil) && (expires == nil)) { NSMutableDictionary *modifiedHeaders = [httpResponse.allHeaderFields mutableCopy]; NSString *expireDate = [NSDate RFC822StringFromDate:[[NSDate date] dateByAddingTimeInterval:cacheTime]]; NSDictionary *values = @{@"Expires": expireDate, @"Cache-Control": @"public"}; [modifiedHeaders addEntriesFromDictionary:values]; NSHTTPURLResponse *response = [[NSHTTPURLResponse alloc] initWithURL:httpResponse.URL statusCode:httpResponse.statusCode HTTPVersion:@"HTTP/1.1" headerFields:modifiedHeaders]; NSCachedURLResponse *modifiedCachedResponse = [[NSCachedURLResponse alloc] initWithResponse:response data:cachedResponse.data userInfo:cachedResponse.userInfo storagePolicy:NSURLCacheStorageAllowed]; return modifiedCachedResponse; } return cachedResponse;
HTTP- .
, ( , , ). , ( cacheTime ) .
— ( ). , , .
:
(, , ..); - NSURLCache , .
:
30 ; ; : 200- , NSURLConnection .
""
API (, ), fields :
. . . , .
, Player , JSON-, . , fields , NSDictionary . , - . , .
JSON -> NSObject Mantle , API ( RACSignal API ):
- (RACSignal *)searchPlayers:(NSString *)query limit:(NSUInteger)limit { NSMutableDictionary *parameters = [NSMutableDictionary dictionary]; parameters[@"search"] = query; if (limit) parameters[@"limit"] = @(limit); return [self getPath:@"account/list" parameters:parameters resultClass:WOTSearchPlayer.class]; }
, resultClass , ? :
- (RACSignal *)searchPlayers:(NSString *)query limit:(NSUInteger)limit resultClass:(Class)resultClass
, API, .
API , : , , "" . ( ). , ( AFJSONRequestOperation ):
op1 = [AFJSONRequestOperation JSONRequestOperationWithRequest:nil success:^(id JSON) { op2 = [AFJSONRequestOperation JSONRequestOperationWithRequest:nil success:^(id JSON) { op3 = [AFJSONRequestOperation JSONRequestOperationWithRequest:nil success:^(id JSON) { // Combine all the responses and return } failure:^(NSError *error) { // Handle error }]; } failure:^(NSError *error) { // Handle error }]; } failure:^(NSError *error) { // Handle error }];
, , . ReactiveCocoa , .
API :
- (RACSignal *)searchPlayers:(NSString *)query limit:(NSUInteger)limit; - (RACSignal *)fetchPlayers:(NSArray *)playerIDs;
RACSignal — , , . — , . / :
RACSignal *fetchPlayersAndRatings = [[[[API searchPlayers:@"" limit:0] flattenMap:^RACStream *(id players) { // skipping data processing return [API fetchPlayers:@[]]; }] flattenMap:^RACStream *(id fullPlayers) { // Skipping data processing return [API statsSliceForPlayer:nil hoursAgo:@[] fields:nil]; }] flattenMap:^RACStream *(id value) { // Compose all responses here id composedResponse; return [RACSignal return:composedResponse]; }]; [fetchPlayersAndRatings subscribeNext:^(id x) { // Fully composed response } error:^(NSError *error) { // All erros go to one place }];
ReactiveCocoa — , . , Promises Futures.
:
Core ( , ); WOT ( World of Tanks–); WOWP ( World of Warplanes–)
, , — . , ( ) CocoaPods , . subspecs , :
# Subspecs s.subspec 'Core' do |cs| cs.source_files = 'WGNAPIClient/Core/**/*.{h,m}' end s.subspec 'WOT' do |cs| cs.dependency 'WGNAPIClient/Core' cs.source_files = 'WGNAPIClient/WOT/**/*.{h,m}' end s.subspec 'WOWP' do |cs| cs.dependency 'WGNAPIClient/Core' cs.source_files = 'WGNAPIClient/WOWP/**/*.{h,m}' end
"" "" , :
pod 'WGNAPIClient/WOT' pod 'WGNAPIClient/WOWP'
. .
(98%). :
; .
:
context(@"API works with players data", ^{ it (@"should search players", ^{ stubResponse(@"/wot/account/list", @"players-search.json"); RACSignal *searchRequest = [client searchPlayers:@"garnett" limit:0]; NSArray *response = [searchRequest asynchronousFirstOrDefault:nil success:NULL error:&error]; [[error should] beNil]; [[response should] beKindOfClass:NSArray.class]; [[response.lastObject should] beKindOfClass:WOTSearchPlayer.class]; WOTSearchPlayer *player = response[0]; [[player.ID should] equal:@"1785514"]; }); });
, / , RACSignal , :
(id)asynchronousFirstOrDefault:(id)defaultValue success:(BOOL *)success error:(NSError **)error;
describe(@"WOTRatingType", ^{ it(@"should parse json to model object", ^{ NSDictionary *json = @{ @"threshold": @5, @"type": @"1", @"rank_fields": @[ @"xp_amount_rank", @"xp_max_rank", @"battles_count_rank", @"spotted_count_rank", @"damage_dealt_rank", ] }; NSError *error; WOTRatingType *ratingType = [MTLJSONAdapter modelOfClass:WOTRatingType.class fromJSONDictionary:json error:&error]; [[error should] beNil]; [[ratingType should] beKindOfClass:WOTRatingType.class]; [[json[@"threshold"] should] equal:theValue(ratingType.threshold)]; * [[json[@"type"] should] equal:ratingType.type]; [[ratingType.rankFields shouldNot] beNil]; [[ratingType.rankFields should] beKindOfClass:NSArray.class]; [[ratingType.rankFields should] haveCountOf:5]; [[@"maximumXP" should] equal:ratingType.rankFields[1]]; }); });
* — - - Kiwi, , nil .
API :
; .
, , : query http-. 
, ReactiveCocoa , ( ). ( ) 2k LOC ( ~1k - , ~700 - ), : — .     . 
 
 Kiwi 
  BDD-    RSpec -  ,   .   «--». 
 
 OHTTPStubs 
   ,      web-   .      "",      - . 
 
     . 
 
    
 There are only two hard things in Computer Science: cache invalidation and naming things. 
 
 -- Phil Karlton 
  « »  : 
 
                .. 
 ,            . 
       -     inMemory CoreData ,       NSCache .   ,       ,         —         .         NSURLConnection . 
 
 NSURLCache 
 NSURLCache        NSURLRequest   .      ,    . 
 
   : 
 NSURLCache *urlCache = [[NSURLCache alloc] initWithMemoryCapacity:kInMemoryCacheSize diskCapacity:kOnDiskCacheSize diskPath:path]; [NSURLCache setSharedURLCache:urlCache]; 
     ,           . 
 
  NSURLConnectionDelegate   ,     ""    : 
 - (NSCachedURLResponse *)connection:(NSURLConnection *)connection willCacheResponse:(NSCachedURLResponse *)cachedResponse; 
    ? 
 
  NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse*)[cachedResponse response]; NSDictionary *headers = [httpResponse allHeaderFields]; NSString *cacheControl = [headers valueForKey:@"Cache-Control"]; NSString *expires = [headers valueForKey:@"Expires"]; if((cacheControl == nil) && (expires == nil)) { NSMutableDictionary *modifiedHeaders = [httpResponse.allHeaderFields mutableCopy]; NSString *expireDate = [NSDate RFC822StringFromDate:[[NSDate date] dateByAddingTimeInterval:cacheTime]]; NSDictionary *values = @{@"Expires": expireDate, @"Cache-Control": @"public"}; [modifiedHeaders addEntriesFromDictionary:values]; NSHTTPURLResponse *response = [[NSHTTPURLResponse alloc] initWithURL:httpResponse.URL statusCode:httpResponse.statusCode HTTPVersion:@"HTTP/1.1" headerFields:modifiedHeaders]; NSCachedURLResponse *modifiedCachedResponse = [[NSCachedURLResponse alloc] initWithResponse:response data:cachedResponse.data userInfo:cachedResponse.userInfo storagePolicy:NSURLCacheStorageAllowed]; return modifiedCachedResponse; } return cachedResponse; 
      HTTP-      . 
 
 ,     (    ,    ,            ).   ,     ( cacheTime )   . 
     —        (      ).    ,      ,    . 
 
 : 
 
        (, ,   ..);  -   NSURLCache ,   . 
 : 
     30  ;       ; :         200- , NSURLConnection   . 
  ""  
          API (,    ),     fields : 
 
   .   .    .    ,   . 
  ,   Player ,      JSON-,   .        ,       fields ,        NSDictionary .         ,  -       .          ,           . 
   JSON -> NSObject    Mantle ,     API      ( RACSignal   API      ): 
 
 - (RACSignal *)searchPlayers:(NSString *)query limit:(NSUInteger)limit { NSMutableDictionary *parameters = [NSMutableDictionary dictionary]; parameters[@"search"] = query; if (limit) parameters[@"limit"] = @(limit); return [self getPath:@"account/list" parameters:parameters resultClass:WOTSearchPlayer.class]; } 
  ,       resultClass ,         ? : 
 
  - (RACSignal *)searchPlayers:(NSString *)query limit:(NSUInteger)limit resultClass:(Class)resultClass 
 ,     API,           . 
 
       
     API     ,               : ,   ,     ""    . (    ).    ,       (   AFJSONRequestOperation ): 
 
  op1 = [AFJSONRequestOperation JSONRequestOperationWithRequest:nil success:^(id JSON) { op2 = [AFJSONRequestOperation JSONRequestOperationWithRequest:nil success:^(id JSON) { op3 = [AFJSONRequestOperation JSONRequestOperationWithRequest:nil success:^(id JSON) { // Combine all the responses and return } failure:^(NSError *error) { // Handle error }]; } failure:^(NSError *error) { // Handle error }]; } failure:^(NSError *error) { // Handle error }]; 
  ,    ,          .         ReactiveCocoa  ,       . 
  API    : 
 
 - (RACSignal *)searchPlayers:(NSString *)query limit:(NSUInteger)limit; - (RACSignal *)fetchPlayers:(NSArray *)playerIDs; 
 RACSignal —  ,       ,     .    —       ,    .          / : 
 
  RACSignal *fetchPlayersAndRatings = [[[[API searchPlayers:@"" limit:0] flattenMap:^RACStream *(id players) { // skipping data processing return [API fetchPlayers:@[]]; }] flattenMap:^RACStream *(id fullPlayers) { // Skipping data processing return [API statsSliceForPlayer:nil hoursAgo:@[] fields:nil]; }] flattenMap:^RACStream *(id value) { // Compose all responses here id composedResponse; return [RACSignal return:composedResponse]; }]; [fetchPlayersAndRatings subscribeNext:^(id x) { // Fully composed response } error:^(NSError *error) { // All erros go to one place }]; 
  ReactiveCocoa   —      ,      .        ,         Promises  Futures. 
 
      
        : 
 Core ( , ); WOT (    World of Tanks–); WOWP (    World of Warplanes–) 
  ,         ,      — . ,    (    )       CocoaPods ,       .       subspecs ,        : 
 
  # Subspecs s.subspec 'Core' do |cs| cs.source_files = 'WGNAPIClient/Core/**/*.{h,m}' end s.subspec 'WOT' do |cs| cs.dependency 'WGNAPIClient/Core' cs.source_files = 'WGNAPIClient/WOT/**/*.{h,m}' end s.subspec 'WOWP' do |cs| cs.dependency 'WGNAPIClient/Core' cs.source_files = 'WGNAPIClient/WOWP/**/*.{h,m}' end 
 
    ""  ""  ,      : 
 pod 'WGNAPIClient/WOT' pod 'WGNAPIClient/WOWP' 
     
              .        . 
         (98%).           : 
 
   ;    . 
   
     : 
  context(@"API works with players data", ^{ it (@"should search players", ^{ stubResponse(@"/wot/account/list", @"players-search.json"); RACSignal *searchRequest = [client searchPlayers:@"garnett" limit:0]; NSArray *response = [searchRequest asynchronousFirstOrDefault:nil success:NULL error:&error]; [[error should] beNil]; [[response should] beKindOfClass:NSArray.class]; [[response.lastObject should] beKindOfClass:WOTSearchPlayer.class]; WOTSearchPlayer *player = response[0]; [[player.ID should] equal:@"1785514"]; }); }); 
 ,     / ,    RACSignal      ,       : 
 (id)asynchronousFirstOrDefault:(id)defaultValue success:(BOOL *)success error:(NSError **)error; 
   
 describe(@"WOTRatingType", ^{ it(@"should parse json to model object", ^{ NSDictionary *json = @{ @"threshold": @5, @"type": @"1", @"rank_fields": @[ @"xp_amount_rank", @"xp_max_rank", @"battles_count_rank", @"spotted_count_rank", @"damage_dealt_rank", ] }; NSError *error; WOTRatingType *ratingType = [MTLJSONAdapter modelOfClass:WOTRatingType.class fromJSONDictionary:json error:&error]; [[error should] beNil]; [[ratingType should] beKindOfClass:WOTRatingType.class]; [[json[@"threshold"] should] equal:theValue(ratingType.threshold)]; * [[json[@"type"] should] equal:ratingType.type]; [[ratingType.rankFields shouldNot] beNil]; [[ratingType.rankFields should] beKindOfClass:NSArray.class]; [[ratingType.rankFields should] haveCountOf:5]; [[@"maximumXP" should] equal:ratingType.rankFields[1]]; }); }); 
 
 * —    - -    Kiwi,      ,     nil . 
 
       API    : 
    ;   . 
  ,  ,       :  query  http-.   
 
  
    ,      ReactiveCocoa ,         (    ).    ( )   2k LOC (  ~1k -  ,   ~700 -     ),     :        —         . .
Kiwi
BDD- RSpec - , . «--».
OHTTPStubs
, web- . "", - .
.
There are only two hard things in Computer Science: cache invalidation and naming things.
-- Phil Karlton
« » :
..
, .
- inMemory CoreData , NSCache . , , — . NSURLConnection .
NSURLCache
NSURLCache NSURLRequest . , .
:
NSURLCache *urlCache = [[NSURLCache alloc] initWithMemoryCapacity:kInMemoryCacheSize diskCapacity:kOnDiskCacheSize diskPath:path]; [NSURLCache setSharedURLCache:urlCache];
, .
NSURLConnectionDelegate , "" :
- (NSCachedURLResponse *)connection:(NSURLConnection *)connection willCacheResponse:(NSCachedURLResponse *)cachedResponse;
?
NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse*)[cachedResponse response]; NSDictionary *headers = [httpResponse allHeaderFields]; NSString *cacheControl = [headers valueForKey:@"Cache-Control"]; NSString *expires = [headers valueForKey:@"Expires"]; if((cacheControl == nil) && (expires == nil)) { NSMutableDictionary *modifiedHeaders = [httpResponse.allHeaderFields mutableCopy]; NSString *expireDate = [NSDate RFC822StringFromDate:[[NSDate date] dateByAddingTimeInterval:cacheTime]]; NSDictionary *values = @{@"Expires": expireDate, @"Cache-Control": @"public"}; [modifiedHeaders addEntriesFromDictionary:values]; NSHTTPURLResponse *response = [[NSHTTPURLResponse alloc] initWithURL:httpResponse.URL statusCode:httpResponse.statusCode HTTPVersion:@"HTTP/1.1" headerFields:modifiedHeaders]; NSCachedURLResponse *modifiedCachedResponse = [[NSCachedURLResponse alloc] initWithResponse:response data:cachedResponse.data userInfo:cachedResponse.userInfo storagePolicy:NSURLCacheStorageAllowed]; return modifiedCachedResponse; } return cachedResponse;
HTTP- .
, ( , , ). , ( cacheTime ) .
— ( ). , , .
:
(, , ..); - NSURLCache , .
:
30 ; ; : 200- , NSURLConnection .
""
API (, ), fields :
. . . , .
, Player , JSON-, . , fields , NSDictionary . , - . , .
JSON -> NSObject Mantle , API ( RACSignal API ):
- (RACSignal *)searchPlayers:(NSString *)query limit:(NSUInteger)limit { NSMutableDictionary *parameters = [NSMutableDictionary dictionary]; parameters[@"search"] = query; if (limit) parameters[@"limit"] = @(limit); return [self getPath:@"account/list" parameters:parameters resultClass:WOTSearchPlayer.class]; }
, resultClass , ? :
- (RACSignal *)searchPlayers:(NSString *)query limit:(NSUInteger)limit resultClass:(Class)resultClass
, API, .
API , : , , "" . ( ). , ( AFJSONRequestOperation ):
op1 = [AFJSONRequestOperation JSONRequestOperationWithRequest:nil success:^(id JSON) { op2 = [AFJSONRequestOperation JSONRequestOperationWithRequest:nil success:^(id JSON) { op3 = [AFJSONRequestOperation JSONRequestOperationWithRequest:nil success:^(id JSON) { // Combine all the responses and return } failure:^(NSError *error) { // Handle error }]; } failure:^(NSError *error) { // Handle error }]; } failure:^(NSError *error) { // Handle error }];
, , . ReactiveCocoa , .
API :
- (RACSignal *)searchPlayers:(NSString *)query limit:(NSUInteger)limit; - (RACSignal *)fetchPlayers:(NSArray *)playerIDs;
RACSignal — , , . — , . / :
RACSignal *fetchPlayersAndRatings = [[[[API searchPlayers:@"" limit:0] flattenMap:^RACStream *(id players) { // skipping data processing return [API fetchPlayers:@[]]; }] flattenMap:^RACStream *(id fullPlayers) { // Skipping data processing return [API statsSliceForPlayer:nil hoursAgo:@[] fields:nil]; }] flattenMap:^RACStream *(id value) { // Compose all responses here id composedResponse; return [RACSignal return:composedResponse]; }]; [fetchPlayersAndRatings subscribeNext:^(id x) { // Fully composed response } error:^(NSError *error) { // All erros go to one place }];
ReactiveCocoa — , . , Promises Futures.
:
Core ( , ); WOT ( World of Tanks–); WOWP ( World of Warplanes–)
, , — . , ( ) CocoaPods , . subspecs , :
# Subspecs s.subspec 'Core' do |cs| cs.source_files = 'WGNAPIClient/Core/**/*.{h,m}' end s.subspec 'WOT' do |cs| cs.dependency 'WGNAPIClient/Core' cs.source_files = 'WGNAPIClient/WOT/**/*.{h,m}' end s.subspec 'WOWP' do |cs| cs.dependency 'WGNAPIClient/Core' cs.source_files = 'WGNAPIClient/WOWP/**/*.{h,m}' end
"" "" , :
pod 'WGNAPIClient/WOT' pod 'WGNAPIClient/WOWP'
. .
(98%). :
; .
:
context(@"API works with players data", ^{ it (@"should search players", ^{ stubResponse(@"/wot/account/list", @"players-search.json"); RACSignal *searchRequest = [client searchPlayers:@"garnett" limit:0]; NSArray *response = [searchRequest asynchronousFirstOrDefault:nil success:NULL error:&error]; [[error should] beNil]; [[response should] beKindOfClass:NSArray.class]; [[response.lastObject should] beKindOfClass:WOTSearchPlayer.class]; WOTSearchPlayer *player = response[0]; [[player.ID should] equal:@"1785514"]; }); });
, / , RACSignal , :
(id)asynchronousFirstOrDefault:(id)defaultValue success:(BOOL *)success error:(NSError **)error;
describe(@"WOTRatingType", ^{ it(@"should parse json to model object", ^{ NSDictionary *json = @{ @"threshold": @5, @"type": @"1", @"rank_fields": @[ @"xp_amount_rank", @"xp_max_rank", @"battles_count_rank", @"spotted_count_rank", @"damage_dealt_rank", ] }; NSError *error; WOTRatingType *ratingType = [MTLJSONAdapter modelOfClass:WOTRatingType.class fromJSONDictionary:json error:&error]; [[error should] beNil]; [[ratingType should] beKindOfClass:WOTRatingType.class]; [[json[@"threshold"] should] equal:theValue(ratingType.threshold)]; * [[json[@"type"] should] equal:ratingType.type]; [[ratingType.rankFields shouldNot] beNil]; [[ratingType.rankFields should] beKindOfClass:NSArray.class]; [[ratingType.rankFields should] haveCountOf:5]; [[@"maximumXP" should] equal:ratingType.rankFields[1]]; }); });
* — - - Kiwi, , nil .
API :
; .
, , : query http-. 
, ReactiveCocoa , ( ). ( ) 2k LOC ( ~1k - , ~700 - ), : — .     . 
 
 Kiwi 
  BDD-    RSpec -  ,   .   «--». 
 
 OHTTPStubs 
   ,      web-   .      "",      - . 
 
     . 
 
    
 There are only two hard things in Computer Science: cache invalidation and naming things. 
 
 -- Phil Karlton 
  « »  : 
 
                .. 
 ,            . 
       -     inMemory CoreData ,       NSCache .   ,       ,         —         .         NSURLConnection . 
 
 NSURLCache 
 NSURLCache        NSURLRequest   .      ,    . 
 
   : 
 NSURLCache *urlCache = [[NSURLCache alloc] initWithMemoryCapacity:kInMemoryCacheSize diskCapacity:kOnDiskCacheSize diskPath:path]; [NSURLCache setSharedURLCache:urlCache]; 
     ,           . 
 
  NSURLConnectionDelegate   ,     ""    : 
 - (NSCachedURLResponse *)connection:(NSURLConnection *)connection willCacheResponse:(NSCachedURLResponse *)cachedResponse; 
    ? 
 
  NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse*)[cachedResponse response]; NSDictionary *headers = [httpResponse allHeaderFields]; NSString *cacheControl = [headers valueForKey:@"Cache-Control"]; NSString *expires = [headers valueForKey:@"Expires"]; if((cacheControl == nil) && (expires == nil)) { NSMutableDictionary *modifiedHeaders = [httpResponse.allHeaderFields mutableCopy]; NSString *expireDate = [NSDate RFC822StringFromDate:[[NSDate date] dateByAddingTimeInterval:cacheTime]]; NSDictionary *values = @{@"Expires": expireDate, @"Cache-Control": @"public"}; [modifiedHeaders addEntriesFromDictionary:values]; NSHTTPURLResponse *response = [[NSHTTPURLResponse alloc] initWithURL:httpResponse.URL statusCode:httpResponse.statusCode HTTPVersion:@"HTTP/1.1" headerFields:modifiedHeaders]; NSCachedURLResponse *modifiedCachedResponse = [[NSCachedURLResponse alloc] initWithResponse:response data:cachedResponse.data userInfo:cachedResponse.userInfo storagePolicy:NSURLCacheStorageAllowed]; return modifiedCachedResponse; } return cachedResponse; 
      HTTP-      . 
 
 ,     (    ,    ,            ).   ,     ( cacheTime )   . 
     —        (      ).    ,      ,    . 
 
 : 
 
        (, ,   ..);  -   NSURLCache ,   . 
 : 
     30  ;       ; :         200- , NSURLConnection   . 
  ""  
          API (,    ),     fields : 
 
   .   .    .    ,   . 
  ,   Player ,      JSON-,   .        ,       fields ,        NSDictionary .         ,  -       .          ,           . 
   JSON -> NSObject    Mantle ,     API      ( RACSignal   API      ): 
 
 - (RACSignal *)searchPlayers:(NSString *)query limit:(NSUInteger)limit { NSMutableDictionary *parameters = [NSMutableDictionary dictionary]; parameters[@"search"] = query; if (limit) parameters[@"limit"] = @(limit); return [self getPath:@"account/list" parameters:parameters resultClass:WOTSearchPlayer.class]; } 
  ,       resultClass ,         ? : 
 
  - (RACSignal *)searchPlayers:(NSString *)query limit:(NSUInteger)limit resultClass:(Class)resultClass 
 ,     API,           . 
 
       
     API     ,               : ,   ,     ""    . (    ).    ,       (   AFJSONRequestOperation ): 
 
  op1 = [AFJSONRequestOperation JSONRequestOperationWithRequest:nil success:^(id JSON) { op2 = [AFJSONRequestOperation JSONRequestOperationWithRequest:nil success:^(id JSON) { op3 = [AFJSONRequestOperation JSONRequestOperationWithRequest:nil success:^(id JSON) { // Combine all the responses and return } failure:^(NSError *error) { // Handle error }]; } failure:^(NSError *error) { // Handle error }]; } failure:^(NSError *error) { // Handle error }]; 
  ,    ,          .         ReactiveCocoa  ,       . 
  API    : 
 
 - (RACSignal *)searchPlayers:(NSString *)query limit:(NSUInteger)limit; - (RACSignal *)fetchPlayers:(NSArray *)playerIDs; 
 RACSignal —  ,       ,     .    —       ,    .          / : 
 
  RACSignal *fetchPlayersAndRatings = [[[[API searchPlayers:@"" limit:0] flattenMap:^RACStream *(id players) { // skipping data processing return [API fetchPlayers:@[]]; }] flattenMap:^RACStream *(id fullPlayers) { // Skipping data processing return [API statsSliceForPlayer:nil hoursAgo:@[] fields:nil]; }] flattenMap:^RACStream *(id value) { // Compose all responses here id composedResponse; return [RACSignal return:composedResponse]; }]; [fetchPlayersAndRatings subscribeNext:^(id x) { // Fully composed response } error:^(NSError *error) { // All erros go to one place }]; 
  ReactiveCocoa   —      ,      .        ,         Promises  Futures. 
 
      
        : 
 Core ( , ); WOT (    World of Tanks–); WOWP (    World of Warplanes–) 
  ,         ,      — . ,    (    )       CocoaPods ,       .       subspecs ,        : 
 
  # Subspecs s.subspec 'Core' do |cs| cs.source_files = 'WGNAPIClient/Core/**/*.{h,m}' end s.subspec 'WOT' do |cs| cs.dependency 'WGNAPIClient/Core' cs.source_files = 'WGNAPIClient/WOT/**/*.{h,m}' end s.subspec 'WOWP' do |cs| cs.dependency 'WGNAPIClient/Core' cs.source_files = 'WGNAPIClient/WOWP/**/*.{h,m}' end 
 
    ""  ""  ,      : 
 pod 'WGNAPIClient/WOT' pod 'WGNAPIClient/WOWP' 
     
              .        . 
         (98%).           : 
 
   ;    . 
   
     : 
  context(@"API works with players data", ^{ it (@"should search players", ^{ stubResponse(@"/wot/account/list", @"players-search.json"); RACSignal *searchRequest = [client searchPlayers:@"garnett" limit:0]; NSArray *response = [searchRequest asynchronousFirstOrDefault:nil success:NULL error:&error]; [[error should] beNil]; [[response should] beKindOfClass:NSArray.class]; [[response.lastObject should] beKindOfClass:WOTSearchPlayer.class]; WOTSearchPlayer *player = response[0]; [[player.ID should] equal:@"1785514"]; }); }); 
 ,     / ,    RACSignal      ,       : 
 (id)asynchronousFirstOrDefault:(id)defaultValue success:(BOOL *)success error:(NSError **)error; 
   
 describe(@"WOTRatingType", ^{ it(@"should parse json to model object", ^{ NSDictionary *json = @{ @"threshold": @5, @"type": @"1", @"rank_fields": @[ @"xp_amount_rank", @"xp_max_rank", @"battles_count_rank", @"spotted_count_rank", @"damage_dealt_rank", ] }; NSError *error; WOTRatingType *ratingType = [MTLJSONAdapter modelOfClass:WOTRatingType.class fromJSONDictionary:json error:&error]; [[error should] beNil]; [[ratingType should] beKindOfClass:WOTRatingType.class]; [[json[@"threshold"] should] equal:theValue(ratingType.threshold)]; * [[json[@"type"] should] equal:ratingType.type]; [[ratingType.rankFields shouldNot] beNil]; [[ratingType.rankFields should] beKindOfClass:NSArray.class]; [[ratingType.rankFields should] haveCountOf:5]; [[@"maximumXP" should] equal:ratingType.rankFields[1]]; }); }); 
 
 * —    - -    Kiwi,      ,     nil . 
 
       API    : 
    ;   . 
  ,  ,       :  query  http-.   
 
  
    ,      ReactiveCocoa ,         (    ).    ( )   2k LOC (  ~1k -  ,   ~700 -     ),     :        —         . .
Kiwi
BDD- RSpec - , . «--».
OHTTPStubs
, web- . "", - .
.
There are only two hard things in Computer Science: cache invalidation and naming things.
-- Phil Karlton
« » :
..
, .
- inMemory CoreData , NSCache . , , — . NSURLConnection .
NSURLCache
NSURLCache NSURLRequest . , .
:
NSURLCache *urlCache = [[NSURLCache alloc] initWithMemoryCapacity:kInMemoryCacheSize diskCapacity:kOnDiskCacheSize diskPath:path]; [NSURLCache setSharedURLCache:urlCache];
, .
NSURLConnectionDelegate , "" :
- (NSCachedURLResponse *)connection:(NSURLConnection *)connection willCacheResponse:(NSCachedURLResponse *)cachedResponse;
?
NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse*)[cachedResponse response]; NSDictionary *headers = [httpResponse allHeaderFields]; NSString *cacheControl = [headers valueForKey:@"Cache-Control"]; NSString *expires = [headers valueForKey:@"Expires"]; if((cacheControl == nil) && (expires == nil)) { NSMutableDictionary *modifiedHeaders = [httpResponse.allHeaderFields mutableCopy]; NSString *expireDate = [NSDate RFC822StringFromDate:[[NSDate date] dateByAddingTimeInterval:cacheTime]]; NSDictionary *values = @{@"Expires": expireDate, @"Cache-Control": @"public"}; [modifiedHeaders addEntriesFromDictionary:values]; NSHTTPURLResponse *response = [[NSHTTPURLResponse alloc] initWithURL:httpResponse.URL statusCode:httpResponse.statusCode HTTPVersion:@"HTTP/1.1" headerFields:modifiedHeaders]; NSCachedURLResponse *modifiedCachedResponse = [[NSCachedURLResponse alloc] initWithResponse:response data:cachedResponse.data userInfo:cachedResponse.userInfo storagePolicy:NSURLCacheStorageAllowed]; return modifiedCachedResponse; } return cachedResponse;
HTTP- .
, ( , , ). , ( cacheTime ) .
— ( ). , , .
:
(, , ..); - NSURLCache , .
:
30 ; ; : 200- , NSURLConnection .
""
API (, ), fields :
. . . , .
, Player , JSON-, . , fields , NSDictionary . , - . , .
JSON -> NSObject Mantle , API ( RACSignal API ):
- (RACSignal *)searchPlayers:(NSString *)query limit:(NSUInteger)limit { NSMutableDictionary *parameters = [NSMutableDictionary dictionary]; parameters[@"search"] = query; if (limit) parameters[@"limit"] = @(limit); return [self getPath:@"account/list" parameters:parameters resultClass:WOTSearchPlayer.class]; }
, resultClass , ? :
- (RACSignal *)searchPlayers:(NSString *)query limit:(NSUInteger)limit resultClass:(Class)resultClass
, API, .
API , : , , "" . ( ). , ( AFJSONRequestOperation ):
op1 = [AFJSONRequestOperation JSONRequestOperationWithRequest:nil success:^(id JSON) { op2 = [AFJSONRequestOperation JSONRequestOperationWithRequest:nil success:^(id JSON) { op3 = [AFJSONRequestOperation JSONRequestOperationWithRequest:nil success:^(id JSON) { // Combine all the responses and return } failure:^(NSError *error) { // Handle error }]; } failure:^(NSError *error) { // Handle error }]; } failure:^(NSError *error) { // Handle error }];
, , . ReactiveCocoa , .
API :
- (RACSignal *)searchPlayers:(NSString *)query limit:(NSUInteger)limit; - (RACSignal *)fetchPlayers:(NSArray *)playerIDs;
RACSignal — , , . — , . / :
RACSignal *fetchPlayersAndRatings = [[[[API searchPlayers:@"" limit:0] flattenMap:^RACStream *(id players) { // skipping data processing return [API fetchPlayers:@[]]; }] flattenMap:^RACStream *(id fullPlayers) { // Skipping data processing return [API statsSliceForPlayer:nil hoursAgo:@[] fields:nil]; }] flattenMap:^RACStream *(id value) { // Compose all responses here id composedResponse; return [RACSignal return:composedResponse]; }]; [fetchPlayersAndRatings subscribeNext:^(id x) { // Fully composed response } error:^(NSError *error) { // All erros go to one place }];
ReactiveCocoa — , . , Promises Futures.
:
Core ( , ); WOT ( World of Tanks–); WOWP ( World of Warplanes–)
, , — . , ( ) CocoaPods , . subspecs , :
# Subspecs s.subspec 'Core' do |cs| cs.source_files = 'WGNAPIClient/Core/**/*.{h,m}' end s.subspec 'WOT' do |cs| cs.dependency 'WGNAPIClient/Core' cs.source_files = 'WGNAPIClient/WOT/**/*.{h,m}' end s.subspec 'WOWP' do |cs| cs.dependency 'WGNAPIClient/Core' cs.source_files = 'WGNAPIClient/WOWP/**/*.{h,m}' end
"" "" , :
pod 'WGNAPIClient/WOT' pod 'WGNAPIClient/WOWP'
. .
(98%). :
; .
:
context(@"API works with players data", ^{ it (@"should search players", ^{ stubResponse(@"/wot/account/list", @"players-search.json"); RACSignal *searchRequest = [client searchPlayers:@"garnett" limit:0]; NSArray *response = [searchRequest asynchronousFirstOrDefault:nil success:NULL error:&error]; [[error should] beNil]; [[response should] beKindOfClass:NSArray.class]; [[response.lastObject should] beKindOfClass:WOTSearchPlayer.class]; WOTSearchPlayer *player = response[0]; [[player.ID should] equal:@"1785514"]; }); });
, / , RACSignal , :
(id)asynchronousFirstOrDefault:(id)defaultValue success:(BOOL *)success error:(NSError **)error;
describe(@"WOTRatingType", ^{ it(@"should parse json to model object", ^{ NSDictionary *json = @{ @"threshold": @5, @"type": @"1", @"rank_fields": @[ @"xp_amount_rank", @"xp_max_rank", @"battles_count_rank", @"spotted_count_rank", @"damage_dealt_rank", ] }; NSError *error; WOTRatingType *ratingType = [MTLJSONAdapter modelOfClass:WOTRatingType.class fromJSONDictionary:json error:&error]; [[error should] beNil]; [[ratingType should] beKindOfClass:WOTRatingType.class]; [[json[@"threshold"] should] equal:theValue(ratingType.threshold)]; * [[json[@"type"] should] equal:ratingType.type]; [[ratingType.rankFields shouldNot] beNil]; [[ratingType.rankFields should] beKindOfClass:NSArray.class]; [[ratingType.rankFields should] haveCountOf:5]; [[@"maximumXP" should] equal:ratingType.rankFields[1]]; }); });
* — - - Kiwi, , nil .
API :
; .
, , : query http-. 
, ReactiveCocoa , ( ). ( ) 2k LOC ( ~1k - , ~700 - ), : — ..
Kiwi
BDD- RSpec - , . «--».
OHTTPStubs
, web- . "", - .
.
There are only two hard things in Computer Science: cache invalidation and naming things.
-- Phil Karlton
« » :
..
, .
- inMemory CoreData , NSCache . , , — . NSURLConnection .
NSURLCache
NSURLCache NSURLRequest . , .
:
NSURLCache *urlCache = [[NSURLCache alloc] initWithMemoryCapacity:kInMemoryCacheSize diskCapacity:kOnDiskCacheSize diskPath:path]; [NSURLCache setSharedURLCache:urlCache];
, .
NSURLConnectionDelegate , "" :
- (NSCachedURLResponse *)connection:(NSURLConnection *)connection willCacheResponse:(NSCachedURLResponse *)cachedResponse;
?
NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse*)[cachedResponse response]; NSDictionary *headers = [httpResponse allHeaderFields]; NSString *cacheControl = [headers valueForKey:@"Cache-Control"]; NSString *expires = [headers valueForKey:@"Expires"]; if((cacheControl == nil) && (expires == nil)) { NSMutableDictionary *modifiedHeaders = [httpResponse.allHeaderFields mutableCopy]; NSString *expireDate = [NSDate RFC822StringFromDate:[[NSDate date] dateByAddingTimeInterval:cacheTime]]; NSDictionary *values = @{@"Expires": expireDate, @"Cache-Control": @"public"}; [modifiedHeaders addEntriesFromDictionary:values]; NSHTTPURLResponse *response = [[NSHTTPURLResponse alloc] initWithURL:httpResponse.URL statusCode:httpResponse.statusCode HTTPVersion:@"HTTP/1.1" headerFields:modifiedHeaders]; NSCachedURLResponse *modifiedCachedResponse = [[NSCachedURLResponse alloc] initWithResponse:response data:cachedResponse.data userInfo:cachedResponse.userInfo storagePolicy:NSURLCacheStorageAllowed]; return modifiedCachedResponse; } return cachedResponse;
HTTP- .
, ( , , ). , ( cacheTime ) .
— ( ). , , .
:
(, , ..); - NSURLCache , .
:
30 ; ; : 200- , NSURLConnection .
""
API (, ), fields :
. . . , .
, Player , JSON-, . , fields , NSDictionary . , - . , .
JSON -> NSObject Mantle , API ( RACSignal API ):
- (RACSignal *)searchPlayers:(NSString *)query limit:(NSUInteger)limit { NSMutableDictionary *parameters = [NSMutableDictionary dictionary]; parameters[@"search"] = query; if (limit) parameters[@"limit"] = @(limit); return [self getPath:@"account/list" parameters:parameters resultClass:WOTSearchPlayer.class]; }
, resultClass , ? :
- (RACSignal *)searchPlayers:(NSString *)query limit:(NSUInteger)limit resultClass:(Class)resultClass
, API, .
API , : , , "" . ( ). , ( AFJSONRequestOperation ):
op1 = [AFJSONRequestOperation JSONRequestOperationWithRequest:nil success:^(id JSON) { op2 = [AFJSONRequestOperation JSONRequestOperationWithRequest:nil success:^(id JSON) { op3 = [AFJSONRequestOperation JSONRequestOperationWithRequest:nil success:^(id JSON) { // Combine all the responses and return } failure:^(NSError *error) { // Handle error }]; } failure:^(NSError *error) { // Handle error }]; } failure:^(NSError *error) { // Handle error }];
, , . ReactiveCocoa , .
API :
- (RACSignal *)searchPlayers:(NSString *)query limit:(NSUInteger)limit; - (RACSignal *)fetchPlayers:(NSArray *)playerIDs;
RACSignal — , , . — , . / :
RACSignal *fetchPlayersAndRatings = [[[[API searchPlayers:@"" limit:0] flattenMap:^RACStream *(id players) { // skipping data processing return [API fetchPlayers:@[]]; }] flattenMap:^RACStream *(id fullPlayers) { // Skipping data processing return [API statsSliceForPlayer:nil hoursAgo:@[] fields:nil]; }] flattenMap:^RACStream *(id value) { // Compose all responses here id composedResponse; return [RACSignal return:composedResponse]; }]; [fetchPlayersAndRatings subscribeNext:^(id x) { // Fully composed response } error:^(NSError *error) { // All erros go to one place }];
ReactiveCocoa — , . , Promises Futures.
:
Core ( , ); WOT ( World of Tanks–); WOWP ( World of Warplanes–)
, , — . , ( ) CocoaPods , . subspecs , :
# Subspecs s.subspec 'Core' do |cs| cs.source_files = 'WGNAPIClient/Core/**/*.{h,m}' end s.subspec 'WOT' do |cs| cs.dependency 'WGNAPIClient/Core' cs.source_files = 'WGNAPIClient/WOT/**/*.{h,m}' end s.subspec 'WOWP' do |cs| cs.dependency 'WGNAPIClient/Core' cs.source_files = 'WGNAPIClient/WOWP/**/*.{h,m}' end
"" "" , :
pod 'WGNAPIClient/WOT' pod 'WGNAPIClient/WOWP'
. .
(98%). :
; .
:
context(@"API works with players data", ^{ it (@"should search players", ^{ stubResponse(@"/wot/account/list", @"players-search.json"); RACSignal *searchRequest = [client searchPlayers:@"garnett" limit:0]; NSArray *response = [searchRequest asynchronousFirstOrDefault:nil success:NULL error:&error]; [[error should] beNil]; [[response should] beKindOfClass:NSArray.class]; [[response.lastObject should] beKindOfClass:WOTSearchPlayer.class]; WOTSearchPlayer *player = response[0]; [[player.ID should] equal:@"1785514"]; }); });
, / , RACSignal , :
(id)asynchronousFirstOrDefault:(id)defaultValue success:(BOOL *)success error:(NSError **)error;
describe(@"WOTRatingType", ^{ it(@"should parse json to model object", ^{ NSDictionary *json = @{ @"threshold": @5, @"type": @"1", @"rank_fields": @[ @"xp_amount_rank", @"xp_max_rank", @"battles_count_rank", @"spotted_count_rank", @"damage_dealt_rank", ] }; NSError *error; WOTRatingType *ratingType = [MTLJSONAdapter modelOfClass:WOTRatingType.class fromJSONDictionary:json error:&error]; [[error should] beNil]; [[ratingType should] beKindOfClass:WOTRatingType.class]; [[json[@"threshold"] should] equal:theValue(ratingType.threshold)]; * [[json[@"type"] should] equal:ratingType.type]; [[ratingType.rankFields shouldNot] beNil]; [[ratingType.rankFields should] beKindOfClass:NSArray.class]; [[ratingType.rankFields should] haveCountOf:5]; [[@"maximumXP" should] equal:ratingType.rankFields[1]]; }); });
* — - - Kiwi, , nil .
API :
; .
, , : query http-. 
, ReactiveCocoa , ( ). ( ) 2k LOC ( ~1k - , ~700 - ), : — ..
Kiwi
BDD- RSpec - , . «--».
OHTTPStubs
, web- . "", - .
.
There are only two hard things in Computer Science: cache invalidation and naming things.
-- Phil Karlton
« » :
..
, .
- inMemory CoreData , NSCache . , , — . NSURLConnection .
NSURLCache
NSURLCache NSURLRequest . , .
:
NSURLCache *urlCache = [[NSURLCache alloc] initWithMemoryCapacity:kInMemoryCacheSize diskCapacity:kOnDiskCacheSize diskPath:path]; [NSURLCache setSharedURLCache:urlCache];
, .
NSURLConnectionDelegate , "" :
- (NSCachedURLResponse *)connection:(NSURLConnection *)connection willCacheResponse:(NSCachedURLResponse *)cachedResponse;
?
NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse*)[cachedResponse response]; NSDictionary *headers = [httpResponse allHeaderFields]; NSString *cacheControl = [headers valueForKey:@"Cache-Control"]; NSString *expires = [headers valueForKey:@"Expires"]; if((cacheControl == nil) && (expires == nil)) { NSMutableDictionary *modifiedHeaders = [httpResponse.allHeaderFields mutableCopy]; NSString *expireDate = [NSDate RFC822StringFromDate:[[NSDate date] dateByAddingTimeInterval:cacheTime]]; NSDictionary *values = @{@"Expires": expireDate, @"Cache-Control": @"public"}; [modifiedHeaders addEntriesFromDictionary:values]; NSHTTPURLResponse *response = [[NSHTTPURLResponse alloc] initWithURL:httpResponse.URL statusCode:httpResponse.statusCode HTTPVersion:@"HTTP/1.1" headerFields:modifiedHeaders]; NSCachedURLResponse *modifiedCachedResponse = [[NSCachedURLResponse alloc] initWithResponse:response data:cachedResponse.data userInfo:cachedResponse.userInfo storagePolicy:NSURLCacheStorageAllowed]; return modifiedCachedResponse; } return cachedResponse;
HTTP- .
, ( , , ). , ( cacheTime ) .
— ( ). , , .
:
(, , ..); - NSURLCache , .
:
30 ; ; : 200- , NSURLConnection .
""
API (, ), fields :
. . . , .
, Player , JSON-, . , fields , NSDictionary . , - . , .
JSON -> NSObject Mantle , API ( RACSignal API ):
- (RACSignal *)searchPlayers:(NSString *)query limit:(NSUInteger)limit { NSMutableDictionary *parameters = [NSMutableDictionary dictionary]; parameters[@"search"] = query; if (limit) parameters[@"limit"] = @(limit); return [self getPath:@"account/list" parameters:parameters resultClass:WOTSearchPlayer.class]; }
, resultClass , ? :
- (RACSignal *)searchPlayers:(NSString *)query limit:(NSUInteger)limit resultClass:(Class)resultClass
, API, .
API , : , , "" . ( ). , ( AFJSONRequestOperation ):
op1 = [AFJSONRequestOperation JSONRequestOperationWithRequest:nil success:^(id JSON) { op2 = [AFJSONRequestOperation JSONRequestOperationWithRequest:nil success:^(id JSON) { op3 = [AFJSONRequestOperation JSONRequestOperationWithRequest:nil success:^(id JSON) { // Combine all the responses and return } failure:^(NSError *error) { // Handle error }]; } failure:^(NSError *error) { // Handle error }]; } failure:^(NSError *error) { // Handle error }];
, , . ReactiveCocoa , .
API :
- (RACSignal *)searchPlayers:(NSString *)query limit:(NSUInteger)limit; - (RACSignal *)fetchPlayers:(NSArray *)playerIDs;
RACSignal — , , . — , . / :
RACSignal *fetchPlayersAndRatings = [[[[API searchPlayers:@"" limit:0] flattenMap:^RACStream *(id players) { // skipping data processing return [API fetchPlayers:@[]]; }] flattenMap:^RACStream *(id fullPlayers) { // Skipping data processing return [API statsSliceForPlayer:nil hoursAgo:@[] fields:nil]; }] flattenMap:^RACStream *(id value) { // Compose all responses here id composedResponse; return [RACSignal return:composedResponse]; }]; [fetchPlayersAndRatings subscribeNext:^(id x) { // Fully composed response } error:^(NSError *error) { // All erros go to one place }];
ReactiveCocoa — , . , Promises Futures.
:
Core ( , ); WOT ( World of Tanks–); WOWP ( World of Warplanes–)
, , — . , ( ) CocoaPods , . subspecs , :
# Subspecs s.subspec 'Core' do |cs| cs.source_files = 'WGNAPIClient/Core/**/*.{h,m}' end s.subspec 'WOT' do |cs| cs.dependency 'WGNAPIClient/Core' cs.source_files = 'WGNAPIClient/WOT/**/*.{h,m}' end s.subspec 'WOWP' do |cs| cs.dependency 'WGNAPIClient/Core' cs.source_files = 'WGNAPIClient/WOWP/**/*.{h,m}' end
"" "" , :
pod 'WGNAPIClient/WOT' pod 'WGNAPIClient/WOWP'
. .
(98%). :
; .
:
context(@"API works with players data", ^{ it (@"should search players", ^{ stubResponse(@"/wot/account/list", @"players-search.json"); RACSignal *searchRequest = [client searchPlayers:@"garnett" limit:0]; NSArray *response = [searchRequest asynchronousFirstOrDefault:nil success:NULL error:&error]; [[error should] beNil]; [[response should] beKindOfClass:NSArray.class]; [[response.lastObject should] beKindOfClass:WOTSearchPlayer.class]; WOTSearchPlayer *player = response[0]; [[player.ID should] equal:@"1785514"]; }); });
, / , RACSignal , :
(id)asynchronousFirstOrDefault:(id)defaultValue success:(BOOL *)success error:(NSError **)error;
describe(@"WOTRatingType", ^{ it(@"should parse json to model object", ^{ NSDictionary *json = @{ @"threshold": @5, @"type": @"1", @"rank_fields": @[ @"xp_amount_rank", @"xp_max_rank", @"battles_count_rank", @"spotted_count_rank", @"damage_dealt_rank", ] }; NSError *error; WOTRatingType *ratingType = [MTLJSONAdapter modelOfClass:WOTRatingType.class fromJSONDictionary:json error:&error]; [[error should] beNil]; [[ratingType should] beKindOfClass:WOTRatingType.class]; [[json[@"threshold"] should] equal:theValue(ratingType.threshold)]; * [[json[@"type"] should] equal:ratingType.type]; [[ratingType.rankFields shouldNot] beNil]; [[ratingType.rankFields should] beKindOfClass:NSArray.class]; [[ratingType.rankFields should] haveCountOf:5]; [[@"maximumXP" should] equal:ratingType.rankFields[1]]; }); });
* — - - Kiwi, , nil .
API :
; .
, , : query http-. 
, ReactiveCocoa , ( ). ( ) 2k LOC ( ~1k - , ~700 - ), : — ..
Kiwi
BDD- RSpec - , . «--».
OHTTPStubs
, web- . "", - .
.
There are only two hard things in Computer Science: cache invalidation and naming things.
-- Phil Karlton
« » :
..
, .
- inMemory CoreData , NSCache . , , — . NSURLConnection .
NSURLCache
NSURLCache NSURLRequest . , .
:
NSURLCache *urlCache = [[NSURLCache alloc] initWithMemoryCapacity:kInMemoryCacheSize diskCapacity:kOnDiskCacheSize diskPath:path]; [NSURLCache setSharedURLCache:urlCache];
, .
NSURLConnectionDelegate , "" :
- (NSCachedURLResponse *)connection:(NSURLConnection *)connection willCacheResponse:(NSCachedURLResponse *)cachedResponse;
?
NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse*)[cachedResponse response]; NSDictionary *headers = [httpResponse allHeaderFields]; NSString *cacheControl = [headers valueForKey:@"Cache-Control"]; NSString *expires = [headers valueForKey:@"Expires"]; if((cacheControl == nil) && (expires == nil)) { NSMutableDictionary *modifiedHeaders = [httpResponse.allHeaderFields mutableCopy]; NSString *expireDate = [NSDate RFC822StringFromDate:[[NSDate date] dateByAddingTimeInterval:cacheTime]]; NSDictionary *values = @{@"Expires": expireDate, @"Cache-Control": @"public"}; [modifiedHeaders addEntriesFromDictionary:values]; NSHTTPURLResponse *response = [[NSHTTPURLResponse alloc] initWithURL:httpResponse.URL statusCode:httpResponse.statusCode HTTPVersion:@"HTTP/1.1" headerFields:modifiedHeaders]; NSCachedURLResponse *modifiedCachedResponse = [[NSCachedURLResponse alloc] initWithResponse:response data:cachedResponse.data userInfo:cachedResponse.userInfo storagePolicy:NSURLCacheStorageAllowed]; return modifiedCachedResponse; } return cachedResponse;
HTTP- .
, ( , , ). , ( cacheTime ) .
— ( ). , , .
:
(, , ..); - NSURLCache , .
:
30 ; ; : 200- , NSURLConnection .
""
API (, ), fields :
. . . , .
, Player , JSON-, . , fields , NSDictionary . , - . , .
JSON -> NSObject Mantle , API ( RACSignal API ):
- (RACSignal *)searchPlayers:(NSString *)query limit:(NSUInteger)limit { NSMutableDictionary *parameters = [NSMutableDictionary dictionary]; parameters[@"search"] = query; if (limit) parameters[@"limit"] = @(limit); return [self getPath:@"account/list" parameters:parameters resultClass:WOTSearchPlayer.class]; }
, resultClass , ? :
- (RACSignal *)searchPlayers:(NSString *)query limit:(NSUInteger)limit resultClass:(Class)resultClass
, API, .
API , : , , "" . ( ). , ( AFJSONRequestOperation ):
op1 = [AFJSONRequestOperation JSONRequestOperationWithRequest:nil success:^(id JSON) { op2 = [AFJSONRequestOperation JSONRequestOperationWithRequest:nil success:^(id JSON) { op3 = [AFJSONRequestOperation JSONRequestOperationWithRequest:nil success:^(id JSON) { // Combine all the responses and return } failure:^(NSError *error) { // Handle error }]; } failure:^(NSError *error) { // Handle error }]; } failure:^(NSError *error) { // Handle error }];
, , . ReactiveCocoa , .
API :
- (RACSignal *)searchPlayers:(NSString *)query limit:(NSUInteger)limit; - (RACSignal *)fetchPlayers:(NSArray *)playerIDs;
RACSignal — , , . — , . / :
RACSignal *fetchPlayersAndRatings = [[[[API searchPlayers:@"" limit:0] flattenMap:^RACStream *(id players) { // skipping data processing return [API fetchPlayers:@[]]; }] flattenMap:^RACStream *(id fullPlayers) { // Skipping data processing return [API statsSliceForPlayer:nil hoursAgo:@[] fields:nil]; }] flattenMap:^RACStream *(id value) { // Compose all responses here id composedResponse; return [RACSignal return:composedResponse]; }]; [fetchPlayersAndRatings subscribeNext:^(id x) { // Fully composed response } error:^(NSError *error) { // All erros go to one place }];
ReactiveCocoa — , . , Promises Futures.
:
Core ( , ); WOT ( World of Tanks–); WOWP ( World of Warplanes–)
, , — . , ( ) CocoaPods , . subspecs , :
# Subspecs s.subspec 'Core' do |cs| cs.source_files = 'WGNAPIClient/Core/**/*.{h,m}' end s.subspec 'WOT' do |cs| cs.dependency 'WGNAPIClient/Core' cs.source_files = 'WGNAPIClient/WOT/**/*.{h,m}' end s.subspec 'WOWP' do |cs| cs.dependency 'WGNAPIClient/Core' cs.source_files = 'WGNAPIClient/WOWP/**/*.{h,m}' end
"" "" , :
pod 'WGNAPIClient/WOT' pod 'WGNAPIClient/WOWP'
. .
(98%). :
; .
:
context(@"API works with players data", ^{ it (@"should search players", ^{ stubResponse(@"/wot/account/list", @"players-search.json"); RACSignal *searchRequest = [client searchPlayers:@"garnett" limit:0]; NSArray *response = [searchRequest asynchronousFirstOrDefault:nil success:NULL error:&error]; [[error should] beNil]; [[response should] beKindOfClass:NSArray.class]; [[response.lastObject should] beKindOfClass:WOTSearchPlayer.class]; WOTSearchPlayer *player = response[0]; [[player.ID should] equal:@"1785514"]; }); });
, / , RACSignal , :
(id)asynchronousFirstOrDefault:(id)defaultValue success:(BOOL *)success error:(NSError **)error;
describe(@"WOTRatingType", ^{ it(@"should parse json to model object", ^{ NSDictionary *json = @{ @"threshold": @5, @"type": @"1", @"rank_fields": @[ @"xp_amount_rank", @"xp_max_rank", @"battles_count_rank", @"spotted_count_rank", @"damage_dealt_rank", ] }; NSError *error; WOTRatingType *ratingType = [MTLJSONAdapter modelOfClass:WOTRatingType.class fromJSONDictionary:json error:&error]; [[error should] beNil]; [[ratingType should] beKindOfClass:WOTRatingType.class]; [[json[@"threshold"] should] equal:theValue(ratingType.threshold)]; * [[json[@"type"] should] equal:ratingType.type]; [[ratingType.rankFields shouldNot] beNil]; [[ratingType.rankFields should] beKindOfClass:NSArray.class]; [[ratingType.rankFields should] haveCountOf:5]; [[@"maximumXP" should] equal:ratingType.rankFields[1]]; }); });
* — - - Kiwi, , nil .
API :
; .
, , : query http-. 
, ReactiveCocoa , ( ). ( ) 2k LOC ( ~1k - , ~700 - ), : — ..
Kiwi
BDD- RSpec - , . «--».
OHTTPStubs
, web- . "", - .
.
There are only two hard things in Computer Science: cache invalidation and naming things.
-- Phil Karlton
« » :
..
, .
- inMemory CoreData , NSCache . , , — . NSURLConnection .
NSURLCache
NSURLCache NSURLRequest . , .
:
NSURLCache *urlCache = [[NSURLCache alloc] initWithMemoryCapacity:kInMemoryCacheSize diskCapacity:kOnDiskCacheSize diskPath:path]; [NSURLCache setSharedURLCache:urlCache];
, .
NSURLConnectionDelegate , "" :
- (NSCachedURLResponse *)connection:(NSURLConnection *)connection willCacheResponse:(NSCachedURLResponse *)cachedResponse;
?
NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse*)[cachedResponse response]; NSDictionary *headers = [httpResponse allHeaderFields]; NSString *cacheControl = [headers valueForKey:@"Cache-Control"]; NSString *expires = [headers valueForKey:@"Expires"]; if((cacheControl == nil) && (expires == nil)) { NSMutableDictionary *modifiedHeaders = [httpResponse.allHeaderFields mutableCopy]; NSString *expireDate = [NSDate RFC822StringFromDate:[[NSDate date] dateByAddingTimeInterval:cacheTime]]; NSDictionary *values = @{@"Expires": expireDate, @"Cache-Control": @"public"}; [modifiedHeaders addEntriesFromDictionary:values]; NSHTTPURLResponse *response = [[NSHTTPURLResponse alloc] initWithURL:httpResponse.URL statusCode:httpResponse.statusCode HTTPVersion:@"HTTP/1.1" headerFields:modifiedHeaders]; NSCachedURLResponse *modifiedCachedResponse = [[NSCachedURLResponse alloc] initWithResponse:response data:cachedResponse.data userInfo:cachedResponse.userInfo storagePolicy:NSURLCacheStorageAllowed]; return modifiedCachedResponse; } return cachedResponse;
HTTP- .
, ( , , ). , ( cacheTime ) .
— ( ). , , .
:
(, , ..); - NSURLCache , .
:
30 ; ; : 200- , NSURLConnection .
""
API (, ), fields :
. . . , .
, Player , JSON-, . , fields , NSDictionary . , - . , .
JSON -> NSObject Mantle , API ( RACSignal API ):
- (RACSignal *)searchPlayers:(NSString *)query limit:(NSUInteger)limit { NSMutableDictionary *parameters = [NSMutableDictionary dictionary]; parameters[@"search"] = query; if (limit) parameters[@"limit"] = @(limit); return [self getPath:@"account/list" parameters:parameters resultClass:WOTSearchPlayer.class]; }
, resultClass , ? :
- (RACSignal *)searchPlayers:(NSString *)query limit:(NSUInteger)limit resultClass:(Class)resultClass
, API, .
API , : , , "" . ( ). , ( AFJSONRequestOperation ):
op1 = [AFJSONRequestOperation JSONRequestOperationWithRequest:nil success:^(id JSON) { op2 = [AFJSONRequestOperation JSONRequestOperationWithRequest:nil success:^(id JSON) { op3 = [AFJSONRequestOperation JSONRequestOperationWithRequest:nil success:^(id JSON) { // Combine all the responses and return } failure:^(NSError *error) { // Handle error }]; } failure:^(NSError *error) { // Handle error }]; } failure:^(NSError *error) { // Handle error }];
, , . ReactiveCocoa , .
API :
- (RACSignal *)searchPlayers:(NSString *)query limit:(NSUInteger)limit; - (RACSignal *)fetchPlayers:(NSArray *)playerIDs;
RACSignal — , , . — , . / :
RACSignal *fetchPlayersAndRatings = [[[[API searchPlayers:@"" limit:0] flattenMap:^RACStream *(id players) { // skipping data processing return [API fetchPlayers:@[]]; }] flattenMap:^RACStream *(id fullPlayers) { // Skipping data processing return [API statsSliceForPlayer:nil hoursAgo:@[] fields:nil]; }] flattenMap:^RACStream *(id value) { // Compose all responses here id composedResponse; return [RACSignal return:composedResponse]; }]; [fetchPlayersAndRatings subscribeNext:^(id x) { // Fully composed response } error:^(NSError *error) { // All erros go to one place }];
ReactiveCocoa — , . , Promises Futures.
:
Core ( , ); WOT ( World of Tanks–); WOWP ( World of Warplanes–)
, , — . , ( ) CocoaPods , . subspecs , :
# Subspecs s.subspec 'Core' do |cs| cs.source_files = 'WGNAPIClient/Core/**/*.{h,m}' end s.subspec 'WOT' do |cs| cs.dependency 'WGNAPIClient/Core' cs.source_files = 'WGNAPIClient/WOT/**/*.{h,m}' end s.subspec 'WOWP' do |cs| cs.dependency 'WGNAPIClient/Core' cs.source_files = 'WGNAPIClient/WOWP/**/*.{h,m}' end
"" "" , :
pod 'WGNAPIClient/WOT' pod 'WGNAPIClient/WOWP'
. .
(98%). :
; .
:
context(@"API works with players data", ^{ it (@"should search players", ^{ stubResponse(@"/wot/account/list", @"players-search.json"); RACSignal *searchRequest = [client searchPlayers:@"garnett" limit:0]; NSArray *response = [searchRequest asynchronousFirstOrDefault:nil success:NULL error:&error]; [[error should] beNil]; [[response should] beKindOfClass:NSArray.class]; [[response.lastObject should] beKindOfClass:WOTSearchPlayer.class]; WOTSearchPlayer *player = response[0]; [[player.ID should] equal:@"1785514"]; }); });
, / , RACSignal , :
(id)asynchronousFirstOrDefault:(id)defaultValue success:(BOOL *)success error:(NSError **)error;
describe(@"WOTRatingType", ^{ it(@"should parse json to model object", ^{ NSDictionary *json = @{ @"threshold": @5, @"type": @"1", @"rank_fields": @[ @"xp_amount_rank", @"xp_max_rank", @"battles_count_rank", @"spotted_count_rank", @"damage_dealt_rank", ] }; NSError *error; WOTRatingType *ratingType = [MTLJSONAdapter modelOfClass:WOTRatingType.class fromJSONDictionary:json error:&error]; [[error should] beNil]; [[ratingType should] beKindOfClass:WOTRatingType.class]; [[json[@"threshold"] should] equal:theValue(ratingType.threshold)]; * [[json[@"type"] should] equal:ratingType.type]; [[ratingType.rankFields shouldNot] beNil]; [[ratingType.rankFields should] beKindOfClass:NSArray.class]; [[ratingType.rankFields should] haveCountOf:5]; [[@"maximumXP" should] equal:ratingType.rankFields[1]]; }); });
* — - - Kiwi, , nil .
API :
; .
, , : query http-. 
, ReactiveCocoa , ( ). ( ) 2k LOC ( ~1k - , ~700 - ), : — ..
Kiwi
BDD- RSpec - , . «--».
OHTTPStubs
, web- . "", - .
.
There are only two hard things in Computer Science: cache invalidation and naming things.
-- Phil Karlton
« » :
..
, .
- inMemory CoreData , NSCache . , , — . NSURLConnection .
NSURLCache
NSURLCache NSURLRequest . , .
:
NSURLCache *urlCache = [[NSURLCache alloc] initWithMemoryCapacity:kInMemoryCacheSize diskCapacity:kOnDiskCacheSize diskPath:path]; [NSURLCache setSharedURLCache:urlCache];
, .
NSURLConnectionDelegate , "" :
- (NSCachedURLResponse *)connection:(NSURLConnection *)connection willCacheResponse:(NSCachedURLResponse *)cachedResponse;
?
NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse*)[cachedResponse response]; NSDictionary *headers = [httpResponse allHeaderFields]; NSString *cacheControl = [headers valueForKey:@"Cache-Control"]; NSString *expires = [headers valueForKey:@"Expires"]; if((cacheControl == nil) && (expires == nil)) { NSMutableDictionary *modifiedHeaders = [httpResponse.allHeaderFields mutableCopy]; NSString *expireDate = [NSDate RFC822StringFromDate:[[NSDate date] dateByAddingTimeInterval:cacheTime]]; NSDictionary *values = @{@"Expires": expireDate, @"Cache-Control": @"public"}; [modifiedHeaders addEntriesFromDictionary:values]; NSHTTPURLResponse *response = [[NSHTTPURLResponse alloc] initWithURL:httpResponse.URL statusCode:httpResponse.statusCode HTTPVersion:@"HTTP/1.1" headerFields:modifiedHeaders]; NSCachedURLResponse *modifiedCachedResponse = [[NSCachedURLResponse alloc] initWithResponse:response data:cachedResponse.data userInfo:cachedResponse.userInfo storagePolicy:NSURLCacheStorageAllowed]; return modifiedCachedResponse; } return cachedResponse;
HTTP- .
, ( , , ). , ( cacheTime ) .
— ( ). , , .
:
(, , ..); - NSURLCache , .
:
30 ; ; : 200- , NSURLConnection .
""
API (, ), fields :
. . . , .
, Player , JSON-, . , fields , NSDictionary . , - . , .
JSON -> NSObject Mantle , API ( RACSignal API ):
- (RACSignal *)searchPlayers:(NSString *)query limit:(NSUInteger)limit { NSMutableDictionary *parameters = [NSMutableDictionary dictionary]; parameters[@"search"] = query; if (limit) parameters[@"limit"] = @(limit); return [self getPath:@"account/list" parameters:parameters resultClass:WOTSearchPlayer.class]; }
, resultClass , ? :
- (RACSignal *)searchPlayers:(NSString *)query limit:(NSUInteger)limit resultClass:(Class)resultClass
, API, .
API , : , , "" . ( ). , ( AFJSONRequestOperation ):
op1 = [AFJSONRequestOperation JSONRequestOperationWithRequest:nil success:^(id JSON) { op2 = [AFJSONRequestOperation JSONRequestOperationWithRequest:nil success:^(id JSON) { op3 = [AFJSONRequestOperation JSONRequestOperationWithRequest:nil success:^(id JSON) { // Combine all the responses and return } failure:^(NSError *error) { // Handle error }]; } failure:^(NSError *error) { // Handle error }]; } failure:^(NSError *error) { // Handle error }];
, , . ReactiveCocoa , .
API :
- (RACSignal *)searchPlayers:(NSString *)query limit:(NSUInteger)limit; - (RACSignal *)fetchPlayers:(NSArray *)playerIDs;
RACSignal — , , . — , . / :
RACSignal *fetchPlayersAndRatings = [[[[API searchPlayers:@"" limit:0] flattenMap:^RACStream *(id players) { // skipping data processing return [API fetchPlayers:@[]]; }] flattenMap:^RACStream *(id fullPlayers) { // Skipping data processing return [API statsSliceForPlayer:nil hoursAgo:@[] fields:nil]; }] flattenMap:^RACStream *(id value) { // Compose all responses here id composedResponse; return [RACSignal return:composedResponse]; }]; [fetchPlayersAndRatings subscribeNext:^(id x) { // Fully composed response } error:^(NSError *error) { // All erros go to one place }];
ReactiveCocoa — , . , Promises Futures.
:
Core ( , ); WOT ( World of Tanks–); WOWP ( World of Warplanes–)
, , — . , ( ) CocoaPods , . subspecs , :
# Subspecs s.subspec 'Core' do |cs| cs.source_files = 'WGNAPIClient/Core/**/*.{h,m}' end s.subspec 'WOT' do |cs| cs.dependency 'WGNAPIClient/Core' cs.source_files = 'WGNAPIClient/WOT/**/*.{h,m}' end s.subspec 'WOWP' do |cs| cs.dependency 'WGNAPIClient/Core' cs.source_files = 'WGNAPIClient/WOWP/**/*.{h,m}' end
"" "" , :
pod 'WGNAPIClient/WOT' pod 'WGNAPIClient/WOWP'
. .
(98%). :
; .
:
context(@"API works with players data", ^{ it (@"should search players", ^{ stubResponse(@"/wot/account/list", @"players-search.json"); RACSignal *searchRequest = [client searchPlayers:@"garnett" limit:0]; NSArray *response = [searchRequest asynchronousFirstOrDefault:nil success:NULL error:&error]; [[error should] beNil]; [[response should] beKindOfClass:NSArray.class]; [[response.lastObject should] beKindOfClass:WOTSearchPlayer.class]; WOTSearchPlayer *player = response[0]; [[player.ID should] equal:@"1785514"]; }); });
, / , RACSignal , :
(id)asynchronousFirstOrDefault:(id)defaultValue success:(BOOL *)success error:(NSError **)error;
describe(@"WOTRatingType", ^{ it(@"should parse json to model object", ^{ NSDictionary *json = @{ @"threshold": @5, @"type": @"1", @"rank_fields": @[ @"xp_amount_rank", @"xp_max_rank", @"battles_count_rank", @"spotted_count_rank", @"damage_dealt_rank", ] }; NSError *error; WOTRatingType *ratingType = [MTLJSONAdapter modelOfClass:WOTRatingType.class fromJSONDictionary:json error:&error]; [[error should] beNil]; [[ratingType should] beKindOfClass:WOTRatingType.class]; [[json[@"threshold"] should] equal:theValue(ratingType.threshold)]; * [[json[@"type"] should] equal:ratingType.type]; [[ratingType.rankFields shouldNot] beNil]; [[ratingType.rankFields should] beKindOfClass:NSArray.class]; [[ratingType.rankFields should] haveCountOf:5]; [[@"maximumXP" should] equal:ratingType.rankFields[1]]; }); });
* — - - Kiwi, , nil .
API :
; .
, , : query http-. 
, ReactiveCocoa , ( ). ( ) 2k LOC ( ~1k - , ~700 - ), : — ..
Kiwi
BDD- RSpec - , . «--».
OHTTPStubs
, web- . "", - .
.
There are only two hard things in Computer Science: cache invalidation and naming things.
-- Phil Karlton
« » :
..
, .
- inMemory CoreData , NSCache . , , — . NSURLConnection .
NSURLCache
NSURLCache NSURLRequest . , .
:
NSURLCache *urlCache = [[NSURLCache alloc] initWithMemoryCapacity:kInMemoryCacheSize diskCapacity:kOnDiskCacheSize diskPath:path]; [NSURLCache setSharedURLCache:urlCache];
, .
NSURLConnectionDelegate , "" :
- (NSCachedURLResponse *)connection:(NSURLConnection *)connection willCacheResponse:(NSCachedURLResponse *)cachedResponse;
?
NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse*)[cachedResponse response]; NSDictionary *headers = [httpResponse allHeaderFields]; NSString *cacheControl = [headers valueForKey:@"Cache-Control"]; NSString *expires = [headers valueForKey:@"Expires"]; if((cacheControl == nil) && (expires == nil)) { NSMutableDictionary *modifiedHeaders = [httpResponse.allHeaderFields mutableCopy]; NSString *expireDate = [NSDate RFC822StringFromDate:[[NSDate date] dateByAddingTimeInterval:cacheTime]]; NSDictionary *values = @{@"Expires": expireDate, @"Cache-Control": @"public"}; [modifiedHeaders addEntriesFromDictionary:values]; NSHTTPURLResponse *response = [[NSHTTPURLResponse alloc] initWithURL:httpResponse.URL statusCode:httpResponse.statusCode HTTPVersion:@"HTTP/1.1" headerFields:modifiedHeaders]; NSCachedURLResponse *modifiedCachedResponse = [[NSCachedURLResponse alloc] initWithResponse:response data:cachedResponse.data userInfo:cachedResponse.userInfo storagePolicy:NSURLCacheStorageAllowed]; return modifiedCachedResponse; } return cachedResponse;
HTTP- .
, ( , , ). , ( cacheTime ) .
— ( ). , , .
:
(, , ..); - NSURLCache , .
:
30 ; ; : 200- , NSURLConnection .
""
API (, ), fields :
. . . , .
, Player , JSON-, . , fields , NSDictionary . , - . , .
JSON -> NSObject Mantle , API ( RACSignal API ):
- (RACSignal *)searchPlayers:(NSString *)query limit:(NSUInteger)limit { NSMutableDictionary *parameters = [NSMutableDictionary dictionary]; parameters[@"search"] = query; if (limit) parameters[@"limit"] = @(limit); return [self getPath:@"account/list" parameters:parameters resultClass:WOTSearchPlayer.class]; }
, resultClass , ? :
- (RACSignal *)searchPlayers:(NSString *)query limit:(NSUInteger)limit resultClass:(Class)resultClass
, API, .
API , : , , "" . ( ). , ( AFJSONRequestOperation ):
op1 = [AFJSONRequestOperation JSONRequestOperationWithRequest:nil success:^(id JSON) { op2 = [AFJSONRequestOperation JSONRequestOperationWithRequest:nil success:^(id JSON) { op3 = [AFJSONRequestOperation JSONRequestOperationWithRequest:nil success:^(id JSON) { // Combine all the responses and return } failure:^(NSError *error) { // Handle error }]; } failure:^(NSError *error) { // Handle error }]; } failure:^(NSError *error) { // Handle error }];
, , . ReactiveCocoa , .
API :
- (RACSignal *)searchPlayers:(NSString *)query limit:(NSUInteger)limit; - (RACSignal *)fetchPlayers:(NSArray *)playerIDs;
RACSignal — , , . — , . / :
RACSignal *fetchPlayersAndRatings = [[[[API searchPlayers:@"" limit:0] flattenMap:^RACStream *(id players) { // skipping data processing return [API fetchPlayers:@[]]; }] flattenMap:^RACStream *(id fullPlayers) { // Skipping data processing return [API statsSliceForPlayer:nil hoursAgo:@[] fields:nil]; }] flattenMap:^RACStream *(id value) { // Compose all responses here id composedResponse; return [RACSignal return:composedResponse]; }]; [fetchPlayersAndRatings subscribeNext:^(id x) { // Fully composed response } error:^(NSError *error) { // All erros go to one place }];
ReactiveCocoa — , . , Promises Futures.
:
Core ( , ); WOT ( World of Tanks–); WOWP ( World of Warplanes–)
, , — . , ( ) CocoaPods , . subspecs , :
# Subspecs s.subspec 'Core' do |cs| cs.source_files = 'WGNAPIClient/Core/**/*.{h,m}' end s.subspec 'WOT' do |cs| cs.dependency 'WGNAPIClient/Core' cs.source_files = 'WGNAPIClient/WOT/**/*.{h,m}' end s.subspec 'WOWP' do |cs| cs.dependency 'WGNAPIClient/Core' cs.source_files = 'WGNAPIClient/WOWP/**/*.{h,m}' end
"" "" , :
pod 'WGNAPIClient/WOT' pod 'WGNAPIClient/WOWP'
. .
(98%). :
; .
:
context(@"API works with players data", ^{ it (@"should search players", ^{ stubResponse(@"/wot/account/list", @"players-search.json"); RACSignal *searchRequest = [client searchPlayers:@"garnett" limit:0]; NSArray *response = [searchRequest asynchronousFirstOrDefault:nil success:NULL error:&error]; [[error should] beNil]; [[response should] beKindOfClass:NSArray.class]; [[response.lastObject should] beKindOfClass:WOTSearchPlayer.class]; WOTSearchPlayer *player = response[0]; [[player.ID should] equal:@"1785514"]; }); });
, / , RACSignal , :
(id)asynchronousFirstOrDefault:(id)defaultValue success:(BOOL *)success error:(NSError **)error;
describe(@"WOTRatingType", ^{ it(@"should parse json to model object", ^{ NSDictionary *json = @{ @"threshold": @5, @"type": @"1", @"rank_fields": @[ @"xp_amount_rank", @"xp_max_rank", @"battles_count_rank", @"spotted_count_rank", @"damage_dealt_rank", ] }; NSError *error; WOTRatingType *ratingType = [MTLJSONAdapter modelOfClass:WOTRatingType.class fromJSONDictionary:json error:&error]; [[error should] beNil]; [[ratingType should] beKindOfClass:WOTRatingType.class]; [[json[@"threshold"] should] equal:theValue(ratingType.threshold)]; * [[json[@"type"] should] equal:ratingType.type]; [[ratingType.rankFields shouldNot] beNil]; [[ratingType.rankFields should] beKindOfClass:NSArray.class]; [[ratingType.rankFields should] haveCountOf:5]; [[@"maximumXP" should] equal:ratingType.rankFields[1]]; }); });
* — - - Kiwi, , nil .
API :
; .
, , : query http-. 
, ReactiveCocoa , ( ). ( ) 2k LOC ( ~1k - , ~700 - ), : — ..
Kiwi
BDD- RSpec - , . «--».
OHTTPStubs
, web- . "", - .
.
There are only two hard things in Computer Science: cache invalidation and naming things.
-- Phil Karlton
« » :
..
, .
- inMemory CoreData , NSCache . , , — . NSURLConnection .
NSURLCache
NSURLCache NSURLRequest . , .
:
NSURLCache *urlCache = [[NSURLCache alloc] initWithMemoryCapacity:kInMemoryCacheSize diskCapacity:kOnDiskCacheSize diskPath:path]; [NSURLCache setSharedURLCache:urlCache];
, .
NSURLConnectionDelegate , "" :
- (NSCachedURLResponse *)connection:(NSURLConnection *)connection willCacheResponse:(NSCachedURLResponse *)cachedResponse;
?
NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse*)[cachedResponse response]; NSDictionary *headers = [httpResponse allHeaderFields]; NSString *cacheControl = [headers valueForKey:@"Cache-Control"]; NSString *expires = [headers valueForKey:@"Expires"]; if((cacheControl == nil) && (expires == nil)) { NSMutableDictionary *modifiedHeaders = [httpResponse.allHeaderFields mutableCopy]; NSString *expireDate = [NSDate RFC822StringFromDate:[[NSDate date] dateByAddingTimeInterval:cacheTime]]; NSDictionary *values = @{@"Expires": expireDate, @"Cache-Control": @"public"}; [modifiedHeaders addEntriesFromDictionary:values]; NSHTTPURLResponse *response = [[NSHTTPURLResponse alloc] initWithURL:httpResponse.URL statusCode:httpResponse.statusCode HTTPVersion:@"HTTP/1.1" headerFields:modifiedHeaders]; NSCachedURLResponse *modifiedCachedResponse = [[NSCachedURLResponse alloc] initWithResponse:response data:cachedResponse.data userInfo:cachedResponse.userInfo storagePolicy:NSURLCacheStorageAllowed]; return modifiedCachedResponse; } return cachedResponse;
HTTP- .
, ( , , ). , ( cacheTime ) .
— ( ). , , .
:
(, , ..); - NSURLCache , .
:
30 ; ; : 200- , NSURLConnection .
""
API (, ), fields :
. . . , .
, Player , JSON-, . , fields , NSDictionary . , - . , .
JSON -> NSObject Mantle , API ( RACSignal API ):
- (RACSignal *)searchPlayers:(NSString *)query limit:(NSUInteger)limit { NSMutableDictionary *parameters = [NSMutableDictionary dictionary]; parameters[@"search"] = query; if (limit) parameters[@"limit"] = @(limit); return [self getPath:@"account/list" parameters:parameters resultClass:WOTSearchPlayer.class]; }
, resultClass , ? :
- (RACSignal *)searchPlayers:(NSString *)query limit:(NSUInteger)limit resultClass:(Class)resultClass
, API, .
API , : , , "" . ( ). , ( AFJSONRequestOperation ):
op1 = [AFJSONRequestOperation JSONRequestOperationWithRequest:nil success:^(id JSON) { op2 = [AFJSONRequestOperation JSONRequestOperationWithRequest:nil success:^(id JSON) { op3 = [AFJSONRequestOperation JSONRequestOperationWithRequest:nil success:^(id JSON) { // Combine all the responses and return } failure:^(NSError *error) { // Handle error }]; } failure:^(NSError *error) { // Handle error }]; } failure:^(NSError *error) { // Handle error }];
, , . ReactiveCocoa , .
API :
- (RACSignal *)searchPlayers:(NSString *)query limit:(NSUInteger)limit; - (RACSignal *)fetchPlayers:(NSArray *)playerIDs;
RACSignal — , , . — , . / :
RACSignal *fetchPlayersAndRatings = [[[[API searchPlayers:@"" limit:0] flattenMap:^RACStream *(id players) { // skipping data processing return [API fetchPlayers:@[]]; }] flattenMap:^RACStream *(id fullPlayers) { // Skipping data processing return [API statsSliceForPlayer:nil hoursAgo:@[] fields:nil]; }] flattenMap:^RACStream *(id value) { // Compose all responses here id composedResponse; return [RACSignal return:composedResponse]; }]; [fetchPlayersAndRatings subscribeNext:^(id x) { // Fully composed response } error:^(NSError *error) { // All erros go to one place }];
ReactiveCocoa — , . , Promises Futures.
:
Core ( , ); WOT ( World of Tanks–); WOWP ( World of Warplanes–)
, , — . , ( ) CocoaPods , . subspecs , :
# Subspecs s.subspec 'Core' do |cs| cs.source_files = 'WGNAPIClient/Core/**/*.{h,m}' end s.subspec 'WOT' do |cs| cs.dependency 'WGNAPIClient/Core' cs.source_files = 'WGNAPIClient/WOT/**/*.{h,m}' end s.subspec 'WOWP' do |cs| cs.dependency 'WGNAPIClient/Core' cs.source_files = 'WGNAPIClient/WOWP/**/*.{h,m}' end
"" "" , :
pod 'WGNAPIClient/WOT' pod 'WGNAPIClient/WOWP'
. .
(98%). :
; .
:
context(@"API works with players data", ^{ it (@"should search players", ^{ stubResponse(@"/wot/account/list", @"players-search.json"); RACSignal *searchRequest = [client searchPlayers:@"garnett" limit:0]; NSArray *response = [searchRequest asynchronousFirstOrDefault:nil success:NULL error:&error]; [[error should] beNil]; [[response should] beKindOfClass:NSArray.class]; [[response.lastObject should] beKindOfClass:WOTSearchPlayer.class]; WOTSearchPlayer *player = response[0]; [[player.ID should] equal:@"1785514"]; }); });
, / , RACSignal , :
(id)asynchronousFirstOrDefault:(id)defaultValue success:(BOOL *)success error:(NSError **)error;
describe(@"WOTRatingType", ^{ it(@"should parse json to model object", ^{ NSDictionary *json = @{ @"threshold": @5, @"type": @"1", @"rank_fields": @[ @"xp_amount_rank", @"xp_max_rank", @"battles_count_rank", @"spotted_count_rank", @"damage_dealt_rank", ] }; NSError *error; WOTRatingType *ratingType = [MTLJSONAdapter modelOfClass:WOTRatingType.class fromJSONDictionary:json error:&error]; [[error should] beNil]; [[ratingType should] beKindOfClass:WOTRatingType.class]; [[json[@"threshold"] should] equal:theValue(ratingType.threshold)]; * [[json[@"type"] should] equal:ratingType.type]; [[ratingType.rankFields shouldNot] beNil]; [[ratingType.rankFields should] beKindOfClass:NSArray.class]; [[ratingType.rankFields should] haveCountOf:5]; [[@"maximumXP" should] equal:ratingType.rankFields[1]]; }); });
* — - - Kiwi, , nil .
API :
; .
, , : query http-. 
, ReactiveCocoa , ( ). ( ) 2k LOC ( ~1k - , ~700 - ), : — .     . 
 
 Kiwi 
  BDD-    RSpec -  ,   .   «--». 
 
 OHTTPStubs 
   ,      web-   .      "",      - . 
 
     . 
 
    
 There are only two hard things in Computer Science: cache invalidation and naming things. 
 
 -- Phil Karlton 
  « »  : 
 
                .. 
 ,            . 
       -     inMemory CoreData ,       NSCache .   ,       ,         —         .         NSURLConnection . 
 
 NSURLCache 
 NSURLCache        NSURLRequest   .      ,    . 
 
   : 
 NSURLCache *urlCache = [[NSURLCache alloc] initWithMemoryCapacity:kInMemoryCacheSize diskCapacity:kOnDiskCacheSize diskPath:path]; [NSURLCache setSharedURLCache:urlCache]; 
     ,           . 
 
  NSURLConnectionDelegate   ,     ""    : 
 - (NSCachedURLResponse *)connection:(NSURLConnection *)connection willCacheResponse:(NSCachedURLResponse *)cachedResponse; 
    ? 
 
  NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse*)[cachedResponse response]; NSDictionary *headers = [httpResponse allHeaderFields]; NSString *cacheControl = [headers valueForKey:@"Cache-Control"]; NSString *expires = [headers valueForKey:@"Expires"]; if((cacheControl == nil) && (expires == nil)) { NSMutableDictionary *modifiedHeaders = [httpResponse.allHeaderFields mutableCopy]; NSString *expireDate = [NSDate RFC822StringFromDate:[[NSDate date] dateByAddingTimeInterval:cacheTime]]; NSDictionary *values = @{@"Expires": expireDate, @"Cache-Control": @"public"}; [modifiedHeaders addEntriesFromDictionary:values]; NSHTTPURLResponse *response = [[NSHTTPURLResponse alloc] initWithURL:httpResponse.URL statusCode:httpResponse.statusCode HTTPVersion:@"HTTP/1.1" headerFields:modifiedHeaders]; NSCachedURLResponse *modifiedCachedResponse = [[NSCachedURLResponse alloc] initWithResponse:response data:cachedResponse.data userInfo:cachedResponse.userInfo storagePolicy:NSURLCacheStorageAllowed]; return modifiedCachedResponse; } return cachedResponse; 
      HTTP-      . 
 
 ,     (    ,    ,            ).   ,     ( cacheTime )   . 
     —        (      ).    ,      ,    . 
 
 : 
 
        (, ,   ..);  -   NSURLCache ,   . 
 : 
     30  ;       ; :         200- , NSURLConnection   . 
  ""  
          API (,    ),     fields : 
 
   .   .    .    ,   . 
  ,   Player ,      JSON-,   .        ,       fields ,        NSDictionary .         ,  -       .          ,           . 
   JSON -> NSObject    Mantle ,     API      ( RACSignal   API      ): 
 
 - (RACSignal *)searchPlayers:(NSString *)query limit:(NSUInteger)limit { NSMutableDictionary *parameters = [NSMutableDictionary dictionary]; parameters[@"search"] = query; if (limit) parameters[@"limit"] = @(limit); return [self getPath:@"account/list" parameters:parameters resultClass:WOTSearchPlayer.class]; } 
  ,       resultClass ,         ? : 
 
  - (RACSignal *)searchPlayers:(NSString *)query limit:(NSUInteger)limit resultClass:(Class)resultClass 
 ,     API,           . 
 
       
     API     ,               : ,   ,     ""    . (    ).    ,       (   AFJSONRequestOperation ): 
 
  op1 = [AFJSONRequestOperation JSONRequestOperationWithRequest:nil success:^(id JSON) { op2 = [AFJSONRequestOperation JSONRequestOperationWithRequest:nil success:^(id JSON) { op3 = [AFJSONRequestOperation JSONRequestOperationWithRequest:nil success:^(id JSON) { // Combine all the responses and return } failure:^(NSError *error) { // Handle error }]; } failure:^(NSError *error) { // Handle error }]; } failure:^(NSError *error) { // Handle error }]; 
  ,    ,          .         ReactiveCocoa  ,       . 
  API    : 
 
 - (RACSignal *)searchPlayers:(NSString *)query limit:(NSUInteger)limit; - (RACSignal *)fetchPlayers:(NSArray *)playerIDs; 
 RACSignal —  ,       ,     .    —       ,    .          / : 
 
  RACSignal *fetchPlayersAndRatings = [[[[API searchPlayers:@"" limit:0] flattenMap:^RACStream *(id players) { // skipping data processing return [API fetchPlayers:@[]]; }] flattenMap:^RACStream *(id fullPlayers) { // Skipping data processing return [API statsSliceForPlayer:nil hoursAgo:@[] fields:nil]; }] flattenMap:^RACStream *(id value) { // Compose all responses here id composedResponse; return [RACSignal return:composedResponse]; }]; [fetchPlayersAndRatings subscribeNext:^(id x) { // Fully composed response } error:^(NSError *error) { // All erros go to one place }]; 
  ReactiveCocoa   —      ,      .        ,         Promises  Futures. 
 
      
        : 
 Core ( , ); WOT (    World of Tanks–); WOWP (    World of Warplanes–) 
  ,         ,      — . ,    (    )       CocoaPods ,       .       subspecs ,        : 
 
  # Subspecs s.subspec 'Core' do |cs| cs.source_files = 'WGNAPIClient/Core/**/*.{h,m}' end s.subspec 'WOT' do |cs| cs.dependency 'WGNAPIClient/Core' cs.source_files = 'WGNAPIClient/WOT/**/*.{h,m}' end s.subspec 'WOWP' do |cs| cs.dependency 'WGNAPIClient/Core' cs.source_files = 'WGNAPIClient/WOWP/**/*.{h,m}' end 
 
    ""  ""  ,      : 
 pod 'WGNAPIClient/WOT' pod 'WGNAPIClient/WOWP' 
     
              .        . 
         (98%).           : 
 
   ;    . 
   
     : 
  context(@"API works with players data", ^{ it (@"should search players", ^{ stubResponse(@"/wot/account/list", @"players-search.json"); RACSignal *searchRequest = [client searchPlayers:@"garnett" limit:0]; NSArray *response = [searchRequest asynchronousFirstOrDefault:nil success:NULL error:&error]; [[error should] beNil]; [[response should] beKindOfClass:NSArray.class]; [[response.lastObject should] beKindOfClass:WOTSearchPlayer.class]; WOTSearchPlayer *player = response[0]; [[player.ID should] equal:@"1785514"]; }); }); 
 ,     / ,    RACSignal      ,       : 
 (id)asynchronousFirstOrDefault:(id)defaultValue success:(BOOL *)success error:(NSError **)error; 
   
 describe(@"WOTRatingType", ^{ it(@"should parse json to model object", ^{ NSDictionary *json = @{ @"threshold": @5, @"type": @"1", @"rank_fields": @[ @"xp_amount_rank", @"xp_max_rank", @"battles_count_rank", @"spotted_count_rank", @"damage_dealt_rank", ] }; NSError *error; WOTRatingType *ratingType = [MTLJSONAdapter modelOfClass:WOTRatingType.class fromJSONDictionary:json error:&error]; [[error should] beNil]; [[ratingType should] beKindOfClass:WOTRatingType.class]; [[json[@"threshold"] should] equal:theValue(ratingType.threshold)]; * [[json[@"type"] should] equal:ratingType.type]; [[ratingType.rankFields shouldNot] beNil]; [[ratingType.rankFields should] beKindOfClass:NSArray.class]; [[ratingType.rankFields should] haveCountOf:5]; [[@"maximumXP" should] equal:ratingType.rankFields[1]]; }); }); 
 
 * —    - -    Kiwi,      ,     nil . 
 
       API    : 
    ;   . 
  ,  ,       :  query  http-.   
 
  
    ,      ReactiveCocoa ,         (    ).    ( )   2k LOC (  ~1k -  ,   ~700 -     ),     :        —         . .
Kiwi
BDD- RSpec - , . «--».
OHTTPStubs
, web- . "", - .
.
There are only two hard things in Computer Science: cache invalidation and naming things.
-- Phil Karlton
« » :
..
, .
- inMemory CoreData , NSCache . , , — . NSURLConnection .
NSURLCache
NSURLCache NSURLRequest . , .
:
NSURLCache *urlCache = [[NSURLCache alloc] initWithMemoryCapacity:kInMemoryCacheSize diskCapacity:kOnDiskCacheSize diskPath:path]; [NSURLCache setSharedURLCache:urlCache];
, .
NSURLConnectionDelegate , "" :
- (NSCachedURLResponse *)connection:(NSURLConnection *)connection willCacheResponse:(NSCachedURLResponse *)cachedResponse;
?
NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse*)[cachedResponse response]; NSDictionary *headers = [httpResponse allHeaderFields]; NSString *cacheControl = [headers valueForKey:@"Cache-Control"]; NSString *expires = [headers valueForKey:@"Expires"]; if((cacheControl == nil) && (expires == nil)) { NSMutableDictionary *modifiedHeaders = [httpResponse.allHeaderFields mutableCopy]; NSString *expireDate = [NSDate RFC822StringFromDate:[[NSDate date] dateByAddingTimeInterval:cacheTime]]; NSDictionary *values = @{@"Expires": expireDate, @"Cache-Control": @"public"}; [modifiedHeaders addEntriesFromDictionary:values]; NSHTTPURLResponse *response = [[NSHTTPURLResponse alloc] initWithURL:httpResponse.URL statusCode:httpResponse.statusCode HTTPVersion:@"HTTP/1.1" headerFields:modifiedHeaders]; NSCachedURLResponse *modifiedCachedResponse = [[NSCachedURLResponse alloc] initWithResponse:response data:cachedResponse.data userInfo:cachedResponse.userInfo storagePolicy:NSURLCacheStorageAllowed]; return modifiedCachedResponse; } return cachedResponse;
HTTP- .
, ( , , ). , ( cacheTime ) .
— ( ). , , .
:
(, , ..); - NSURLCache , .
:
30 ; ; : 200- , NSURLConnection .
""
API (, ), fields :
. . . , .
, Player , JSON-, . , fields , NSDictionary . , - . , .
JSON -> NSObject Mantle , API ( RACSignal API ):
- (RACSignal *)searchPlayers:(NSString *)query limit:(NSUInteger)limit { NSMutableDictionary *parameters = [NSMutableDictionary dictionary]; parameters[@"search"] = query; if (limit) parameters[@"limit"] = @(limit); return [self getPath:@"account/list" parameters:parameters resultClass:WOTSearchPlayer.class]; }
, resultClass , ? :
- (RACSignal *)searchPlayers:(NSString *)query limit:(NSUInteger)limit resultClass:(Class)resultClass
, API, .
API , : , , "" . ( ). , ( AFJSONRequestOperation ):
op1 = [AFJSONRequestOperation JSONRequestOperationWithRequest:nil success:^(id JSON) { op2 = [AFJSONRequestOperation JSONRequestOperationWithRequest:nil success:^(id JSON) { op3 = [AFJSONRequestOperation JSONRequestOperationWithRequest:nil success:^(id JSON) { // Combine all the responses and return } failure:^(NSError *error) { // Handle error }]; } failure:^(NSError *error) { // Handle error }]; } failure:^(NSError *error) { // Handle error }];
, , . ReactiveCocoa , .
API :
- (RACSignal *)searchPlayers:(NSString *)query limit:(NSUInteger)limit; - (RACSignal *)fetchPlayers:(NSArray *)playerIDs;
RACSignal — , , . — , . / :
RACSignal *fetchPlayersAndRatings = [[[[API searchPlayers:@"" limit:0] flattenMap:^RACStream *(id players) { // skipping data processing return [API fetchPlayers:@[]]; }] flattenMap:^RACStream *(id fullPlayers) { // Skipping data processing return [API statsSliceForPlayer:nil hoursAgo:@[] fields:nil]; }] flattenMap:^RACStream *(id value) { // Compose all responses here id composedResponse; return [RACSignal return:composedResponse]; }]; [fetchPlayersAndRatings subscribeNext:^(id x) { // Fully composed response } error:^(NSError *error) { // All erros go to one place }];
ReactiveCocoa — , . , Promises Futures.
:
Core ( , ); WOT ( World of Tanks–); WOWP ( World of Warplanes–)
, , — . , ( ) CocoaPods , . subspecs , :
# Subspecs s.subspec 'Core' do |cs| cs.source_files = 'WGNAPIClient/Core/**/*.{h,m}' end s.subspec 'WOT' do |cs| cs.dependency 'WGNAPIClient/Core' cs.source_files = 'WGNAPIClient/WOT/**/*.{h,m}' end s.subspec 'WOWP' do |cs| cs.dependency 'WGNAPIClient/Core' cs.source_files = 'WGNAPIClient/WOWP/**/*.{h,m}' end
"" "" , :
pod 'WGNAPIClient/WOT' pod 'WGNAPIClient/WOWP'
. .
(98%). :
; .
:
context(@"API works with players data", ^{ it (@"should search players", ^{ stubResponse(@"/wot/account/list", @"players-search.json"); RACSignal *searchRequest = [client searchPlayers:@"garnett" limit:0]; NSArray *response = [searchRequest asynchronousFirstOrDefault:nil success:NULL error:&error]; [[error should] beNil]; [[response should] beKindOfClass:NSArray.class]; [[response.lastObject should] beKindOfClass:WOTSearchPlayer.class]; WOTSearchPlayer *player = response[0]; [[player.ID should] equal:@"1785514"]; }); });
, / , RACSignal , :
(id)asynchronousFirstOrDefault:(id)defaultValue success:(BOOL *)success error:(NSError **)error;
describe(@"WOTRatingType", ^{ it(@"should parse json to model object", ^{ NSDictionary *json = @{ @"threshold": @5, @"type": @"1", @"rank_fields": @[ @"xp_amount_rank", @"xp_max_rank", @"battles_count_rank", @"spotted_count_rank", @"damage_dealt_rank", ] }; NSError *error; WOTRatingType *ratingType = [MTLJSONAdapter modelOfClass:WOTRatingType.class fromJSONDictionary:json error:&error]; [[error should] beNil]; [[ratingType should] beKindOfClass:WOTRatingType.class]; [[json[@"threshold"] should] equal:theValue(ratingType.threshold)]; * [[json[@"type"] should] equal:ratingType.type]; [[ratingType.rankFields shouldNot] beNil]; [[ratingType.rankFields should] beKindOfClass:NSArray.class]; [[ratingType.rankFields should] haveCountOf:5]; [[@"maximumXP" should] equal:ratingType.rankFields[1]]; }); });
* — - - Kiwi, , nil .
API :
; .
, , : query http-. 
, ReactiveCocoa , ( ). ( ) 2k LOC ( ~1k - , ~700 - ), : — .     . 
 
 Kiwi 
  BDD-    RSpec -  ,   .   «--». 
 
 OHTTPStubs 
   ,      web-   .      "",      - . 
 
     . 
 
    
 There are only two hard things in Computer Science: cache invalidation and naming things. 
 
 -- Phil Karlton 
  « »  : 
 
                .. 
 ,            . 
       -     inMemory CoreData ,       NSCache .   ,       ,         —         .         NSURLConnection . 
 
 NSURLCache 
 NSURLCache        NSURLRequest   .      ,    . 
 
   : 
 NSURLCache *urlCache = [[NSURLCache alloc] initWithMemoryCapacity:kInMemoryCacheSize diskCapacity:kOnDiskCacheSize diskPath:path]; [NSURLCache setSharedURLCache:urlCache]; 
     ,           . 
 
  NSURLConnectionDelegate   ,     ""    : 
 - (NSCachedURLResponse *)connection:(NSURLConnection *)connection willCacheResponse:(NSCachedURLResponse *)cachedResponse; 
    ? 
 
  NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse*)[cachedResponse response]; NSDictionary *headers = [httpResponse allHeaderFields]; NSString *cacheControl = [headers valueForKey:@"Cache-Control"]; NSString *expires = [headers valueForKey:@"Expires"]; if((cacheControl == nil) && (expires == nil)) { NSMutableDictionary *modifiedHeaders = [httpResponse.allHeaderFields mutableCopy]; NSString *expireDate = [NSDate RFC822StringFromDate:[[NSDate date] dateByAddingTimeInterval:cacheTime]]; NSDictionary *values = @{@"Expires": expireDate, @"Cache-Control": @"public"}; [modifiedHeaders addEntriesFromDictionary:values]; NSHTTPURLResponse *response = [[NSHTTPURLResponse alloc] initWithURL:httpResponse.URL statusCode:httpResponse.statusCode HTTPVersion:@"HTTP/1.1" headerFields:modifiedHeaders]; NSCachedURLResponse *modifiedCachedResponse = [[NSCachedURLResponse alloc] initWithResponse:response data:cachedResponse.data userInfo:cachedResponse.userInfo storagePolicy:NSURLCacheStorageAllowed]; return modifiedCachedResponse; } return cachedResponse; 
      HTTP-      . 
 
 ,     (    ,    ,            ).   ,     ( cacheTime )   . 
     —        (      ).    ,      ,    . 
 
 : 
 
        (, ,   ..);  -   NSURLCache ,   . 
 : 
     30  ;       ; :         200- , NSURLConnection   . 
  ""  
          API (,    ),     fields : 
 
   .   .    .    ,   . 
  ,   Player ,      JSON-,   .        ,       fields ,        NSDictionary .         ,  -       .          ,           . 
   JSON -> NSObject    Mantle ,     API      ( RACSignal   API      ): 
 
 - (RACSignal *)searchPlayers:(NSString *)query limit:(NSUInteger)limit { NSMutableDictionary *parameters = [NSMutableDictionary dictionary]; parameters[@"search"] = query; if (limit) parameters[@"limit"] = @(limit); return [self getPath:@"account/list" parameters:parameters resultClass:WOTSearchPlayer.class]; } 
  ,       resultClass ,         ? : 
 
  - (RACSignal *)searchPlayers:(NSString *)query limit:(NSUInteger)limit resultClass:(Class)resultClass 
 ,     API,           . 
 
       
     API     ,               : ,   ,     ""    . (    ).    ,       (   AFJSONRequestOperation ): 
 
  op1 = [AFJSONRequestOperation JSONRequestOperationWithRequest:nil success:^(id JSON) { op2 = [AFJSONRequestOperation JSONRequestOperationWithRequest:nil success:^(id JSON) { op3 = [AFJSONRequestOperation JSONRequestOperationWithRequest:nil success:^(id JSON) { // Combine all the responses and return } failure:^(NSError *error) { // Handle error }]; } failure:^(NSError *error) { // Handle error }]; } failure:^(NSError *error) { // Handle error }]; 
  ,    ,          .         ReactiveCocoa  ,       . 
  API    : 
 
 - (RACSignal *)searchPlayers:(NSString *)query limit:(NSUInteger)limit; - (RACSignal *)fetchPlayers:(NSArray *)playerIDs; 
 RACSignal —  ,       ,     .    —       ,    .          / : 
 
  RACSignal *fetchPlayersAndRatings = [[[[API searchPlayers:@"" limit:0] flattenMap:^RACStream *(id players) { // skipping data processing return [API fetchPlayers:@[]]; }] flattenMap:^RACStream *(id fullPlayers) { // Skipping data processing return [API statsSliceForPlayer:nil hoursAgo:@[] fields:nil]; }] flattenMap:^RACStream *(id value) { // Compose all responses here id composedResponse; return [RACSignal return:composedResponse]; }]; [fetchPlayersAndRatings subscribeNext:^(id x) { // Fully composed response } error:^(NSError *error) { // All erros go to one place }]; 
  ReactiveCocoa   —      ,      .        ,         Promises  Futures. 
 
      
        : 
 Core ( , ); WOT (    World of Tanks–); WOWP (    World of Warplanes–) 
  ,         ,      — . ,    (    )       CocoaPods ,       .       subspecs ,        : 
 
  # Subspecs s.subspec 'Core' do |cs| cs.source_files = 'WGNAPIClient/Core/**/*.{h,m}' end s.subspec 'WOT' do |cs| cs.dependency 'WGNAPIClient/Core' cs.source_files = 'WGNAPIClient/WOT/**/*.{h,m}' end s.subspec 'WOWP' do |cs| cs.dependency 'WGNAPIClient/Core' cs.source_files = 'WGNAPIClient/WOWP/**/*.{h,m}' end 
 
    ""  ""  ,      : 
 pod 'WGNAPIClient/WOT' pod 'WGNAPIClient/WOWP' 
     
              .        . 
         (98%).           : 
 
   ;    . 
   
     : 
  context(@"API works with players data", ^{ it (@"should search players", ^{ stubResponse(@"/wot/account/list", @"players-search.json"); RACSignal *searchRequest = [client searchPlayers:@"garnett" limit:0]; NSArray *response = [searchRequest asynchronousFirstOrDefault:nil success:NULL error:&error]; [[error should] beNil]; [[response should] beKindOfClass:NSArray.class]; [[response.lastObject should] beKindOfClass:WOTSearchPlayer.class]; WOTSearchPlayer *player = response[0]; [[player.ID should] equal:@"1785514"]; }); }); 
 ,     / ,    RACSignal      ,       : 
 (id)asynchronousFirstOrDefault:(id)defaultValue success:(BOOL *)success error:(NSError **)error; 
   
 describe(@"WOTRatingType", ^{ it(@"should parse json to model object", ^{ NSDictionary *json = @{ @"threshold": @5, @"type": @"1", @"rank_fields": @[ @"xp_amount_rank", @"xp_max_rank", @"battles_count_rank", @"spotted_count_rank", @"damage_dealt_rank", ] }; NSError *error; WOTRatingType *ratingType = [MTLJSONAdapter modelOfClass:WOTRatingType.class fromJSONDictionary:json error:&error]; [[error should] beNil]; [[ratingType should] beKindOfClass:WOTRatingType.class]; [[json[@"threshold"] should] equal:theValue(ratingType.threshold)]; * [[json[@"type"] should] equal:ratingType.type]; [[ratingType.rankFields shouldNot] beNil]; [[ratingType.rankFields should] beKindOfClass:NSArray.class]; [[ratingType.rankFields should] haveCountOf:5]; [[@"maximumXP" should] equal:ratingType.rankFields[1]]; }); }); 
 
 * —    - -    Kiwi,      ,     nil . 
 
       API    : 
    ;   . 
  ,  ,       :  query  http-.   
 
  
    ,      ReactiveCocoa ,         (    ).    ( )   2k LOC (  ~1k -  ,   ~700 -     ),     :        —         ..
Kiwi
BDD- RSpec - , . «--».
OHTTPStubs
, web- . "", - .
.
There are only two hard things in Computer Science: cache invalidation and naming things.
-- Phil Karlton
« » :
..
, .
- inMemory CoreData , NSCache . , , — . NSURLConnection .
NSURLCache
NSURLCache NSURLRequest . , .
:
NSURLCache *urlCache = [[NSURLCache alloc] initWithMemoryCapacity:kInMemoryCacheSize diskCapacity:kOnDiskCacheSize diskPath:path]; [NSURLCache setSharedURLCache:urlCache];
, .
NSURLConnectionDelegate , "" :
- (NSCachedURLResponse *)connection:(NSURLConnection *)connection willCacheResponse:(NSCachedURLResponse *)cachedResponse;
?
NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse*)[cachedResponse response]; NSDictionary *headers = [httpResponse allHeaderFields]; NSString *cacheControl = [headers valueForKey:@"Cache-Control"]; NSString *expires = [headers valueForKey:@"Expires"]; if((cacheControl == nil) && (expires == nil)) { NSMutableDictionary *modifiedHeaders = [httpResponse.allHeaderFields mutableCopy]; NSString *expireDate = [NSDate RFC822StringFromDate:[[NSDate date] dateByAddingTimeInterval:cacheTime]]; NSDictionary *values = @{@"Expires": expireDate, @"Cache-Control": @"public"}; [modifiedHeaders addEntriesFromDictionary:values]; NSHTTPURLResponse *response = [[NSHTTPURLResponse alloc] initWithURL:httpResponse.URL statusCode:httpResponse.statusCode HTTPVersion:@"HTTP/1.1" headerFields:modifiedHeaders]; NSCachedURLResponse *modifiedCachedResponse = [[NSCachedURLResponse alloc] initWithResponse:response data:cachedResponse.data userInfo:cachedResponse.userInfo storagePolicy:NSURLCacheStorageAllowed]; return modifiedCachedResponse; } return cachedResponse;
HTTP- .
, ( , , ). , ( cacheTime ) .
— ( ). , , .
:
(, , ..); - NSURLCache , .
:
30 ; ; : 200- , NSURLConnection .
""
API (, ), fields :
. . . , .
, Player , JSON-, . , fields , NSDictionary . , - . , .
JSON -> NSObject Mantle , API ( RACSignal API ):
- (RACSignal *)searchPlayers:(NSString *)query limit:(NSUInteger)limit { NSMutableDictionary *parameters = [NSMutableDictionary dictionary]; parameters[@"search"] = query; if (limit) parameters[@"limit"] = @(limit); return [self getPath:@"account/list" parameters:parameters resultClass:WOTSearchPlayer.class]; }
, resultClass , ? :
- (RACSignal *)searchPlayers:(NSString *)query limit:(NSUInteger)limit resultClass:(Class)resultClass
, API, .
API , : , , "" . ( ). , ( AFJSONRequestOperation ):
op1 = [AFJSONRequestOperation JSONRequestOperationWithRequest:nil success:^(id JSON) { op2 = [AFJSONRequestOperation JSONRequestOperationWithRequest:nil success:^(id JSON) { op3 = [AFJSONRequestOperation JSONRequestOperationWithRequest:nil success:^(id JSON) { // Combine all the responses and return } failure:^(NSError *error) { // Handle error }]; } failure:^(NSError *error) { // Handle error }]; } failure:^(NSError *error) { // Handle error }];
, , . ReactiveCocoa , .
API :
- (RACSignal *)searchPlayers:(NSString *)query limit:(NSUInteger)limit; - (RACSignal *)fetchPlayers:(NSArray *)playerIDs;
RACSignal — , , . — , . / :
RACSignal *fetchPlayersAndRatings = [[[[API searchPlayers:@"" limit:0] flattenMap:^RACStream *(id players) { // skipping data processing return [API fetchPlayers:@[]]; }] flattenMap:^RACStream *(id fullPlayers) { // Skipping data processing return [API statsSliceForPlayer:nil hoursAgo:@[] fields:nil]; }] flattenMap:^RACStream *(id value) { // Compose all responses here id composedResponse; return [RACSignal return:composedResponse]; }]; [fetchPlayersAndRatings subscribeNext:^(id x) { // Fully composed response } error:^(NSError *error) { // All erros go to one place }];
ReactiveCocoa — , . , Promises Futures.
:
Core ( , ); WOT ( World of Tanks–); WOWP ( World of Warplanes–)
, , — . , ( ) CocoaPods , . subspecs , :
# Subspecs s.subspec 'Core' do |cs| cs.source_files = 'WGNAPIClient/Core/**/*.{h,m}' end s.subspec 'WOT' do |cs| cs.dependency 'WGNAPIClient/Core' cs.source_files = 'WGNAPIClient/WOT/**/*.{h,m}' end s.subspec 'WOWP' do |cs| cs.dependency 'WGNAPIClient/Core' cs.source_files = 'WGNAPIClient/WOWP/**/*.{h,m}' end
"" "" , :
pod 'WGNAPIClient/WOT' pod 'WGNAPIClient/WOWP'
. .
(98%). :
; .
:
context(@"API works with players data", ^{ it (@"should search players", ^{ stubResponse(@"/wot/account/list", @"players-search.json"); RACSignal *searchRequest = [client searchPlayers:@"garnett" limit:0]; NSArray *response = [searchRequest asynchronousFirstOrDefault:nil success:NULL error:&error]; [[error should] beNil]; [[response should] beKindOfClass:NSArray.class]; [[response.lastObject should] beKindOfClass:WOTSearchPlayer.class]; WOTSearchPlayer *player = response[0]; [[player.ID should] equal:@"1785514"]; }); });
, / , RACSignal , :
(id)asynchronousFirstOrDefault:(id)defaultValue success:(BOOL *)success error:(NSError **)error;
describe(@"WOTRatingType", ^{ it(@"should parse json to model object", ^{ NSDictionary *json = @{ @"threshold": @5, @"type": @"1", @"rank_fields": @[ @"xp_amount_rank", @"xp_max_rank", @"battles_count_rank", @"spotted_count_rank", @"damage_dealt_rank", ] }; NSError *error; WOTRatingType *ratingType = [MTLJSONAdapter modelOfClass:WOTRatingType.class fromJSONDictionary:json error:&error]; [[error should] beNil]; [[ratingType should] beKindOfClass:WOTRatingType.class]; [[json[@"threshold"] should] equal:theValue(ratingType.threshold)]; * [[json[@"type"] should] equal:ratingType.type]; [[ratingType.rankFields shouldNot] beNil]; [[ratingType.rankFields should] beKindOfClass:NSArray.class]; [[ratingType.rankFields should] haveCountOf:5]; [[@"maximumXP" should] equal:ratingType.rankFields[1]]; }); });
* — - - Kiwi, , nil .
API :
; .
, , : query http-. 
, ReactiveCocoa , ( ). ( ) 2k LOC ( ~1k - , ~700 - ), : — ..
Kiwi
BDD- RSpec - , . «--».
OHTTPStubs
, web- . "", - .
.
There are only two hard things in Computer Science: cache invalidation and naming things.
-- Phil Karlton
« » :
..
, .
- inMemory CoreData , NSCache . , , — . NSURLConnection .
NSURLCache
NSURLCache NSURLRequest . , .
:
NSURLCache *urlCache = [[NSURLCache alloc] initWithMemoryCapacity:kInMemoryCacheSize diskCapacity:kOnDiskCacheSize diskPath:path]; [NSURLCache setSharedURLCache:urlCache];
, .
NSURLConnectionDelegate , "" :
- (NSCachedURLResponse *)connection:(NSURLConnection *)connection willCacheResponse:(NSCachedURLResponse *)cachedResponse;
?
NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse*)[cachedResponse response]; NSDictionary *headers = [httpResponse allHeaderFields]; NSString *cacheControl = [headers valueForKey:@"Cache-Control"]; NSString *expires = [headers valueForKey:@"Expires"]; if((cacheControl == nil) && (expires == nil)) { NSMutableDictionary *modifiedHeaders = [httpResponse.allHeaderFields mutableCopy]; NSString *expireDate = [NSDate RFC822StringFromDate:[[NSDate date] dateByAddingTimeInterval:cacheTime]]; NSDictionary *values = @{@"Expires": expireDate, @"Cache-Control": @"public"}; [modifiedHeaders addEntriesFromDictionary:values]; NSHTTPURLResponse *response = [[NSHTTPURLResponse alloc] initWithURL:httpResponse.URL statusCode:httpResponse.statusCode HTTPVersion:@"HTTP/1.1" headerFields:modifiedHeaders]; NSCachedURLResponse *modifiedCachedResponse = [[NSCachedURLResponse alloc] initWithResponse:response data:cachedResponse.data userInfo:cachedResponse.userInfo storagePolicy:NSURLCacheStorageAllowed]; return modifiedCachedResponse; } return cachedResponse;
HTTP- .
, ( , , ). , ( cacheTime ) .
— ( ). , , .
:
(, , ..); - NSURLCache , .
:
30 ; ; : 200- , NSURLConnection .
""
API (, ), fields :
. . . , .
, Player , JSON-, . , fields , NSDictionary . , - . , .
JSON -> NSObject Mantle , API ( RACSignal API ):
- (RACSignal *)searchPlayers:(NSString *)query limit:(NSUInteger)limit { NSMutableDictionary *parameters = [NSMutableDictionary dictionary]; parameters[@"search"] = query; if (limit) parameters[@"limit"] = @(limit); return [self getPath:@"account/list" parameters:parameters resultClass:WOTSearchPlayer.class]; }
, resultClass , ? :
- (RACSignal *)searchPlayers:(NSString *)query limit:(NSUInteger)limit resultClass:(Class)resultClass
, API, .
API , : , , "" . ( ). , ( AFJSONRequestOperation ):
op1 = [AFJSONRequestOperation JSONRequestOperationWithRequest:nil success:^(id JSON) { op2 = [AFJSONRequestOperation JSONRequestOperationWithRequest:nil success:^(id JSON) { op3 = [AFJSONRequestOperation JSONRequestOperationWithRequest:nil success:^(id JSON) { // Combine all the responses and return } failure:^(NSError *error) { // Handle error }]; } failure:^(NSError *error) { // Handle error }]; } failure:^(NSError *error) { // Handle error }];
, , . ReactiveCocoa , .
API :
- (RACSignal *)searchPlayers:(NSString *)query limit:(NSUInteger)limit; - (RACSignal *)fetchPlayers:(NSArray *)playerIDs;
RACSignal — , , . — , . / :
RACSignal *fetchPlayersAndRatings = [[[[API searchPlayers:@"" limit:0] flattenMap:^RACStream *(id players) { // skipping data processing return [API fetchPlayers:@[]]; }] flattenMap:^RACStream *(id fullPlayers) { // Skipping data processing return [API statsSliceForPlayer:nil hoursAgo:@[] fields:nil]; }] flattenMap:^RACStream *(id value) { // Compose all responses here id composedResponse; return [RACSignal return:composedResponse]; }]; [fetchPlayersAndRatings subscribeNext:^(id x) { // Fully composed response } error:^(NSError *error) { // All erros go to one place }];
ReactiveCocoa — , . , Promises Futures.
:
Core ( , ); WOT ( World of Tanks–); WOWP ( World of Warplanes–)
, , — . , ( ) CocoaPods , . subspecs , :
# Subspecs s.subspec 'Core' do |cs| cs.source_files = 'WGNAPIClient/Core/**/*.{h,m}' end s.subspec 'WOT' do |cs| cs.dependency 'WGNAPIClient/Core' cs.source_files = 'WGNAPIClient/WOT/**/*.{h,m}' end s.subspec 'WOWP' do |cs| cs.dependency 'WGNAPIClient/Core' cs.source_files = 'WGNAPIClient/WOWP/**/*.{h,m}' end
"" "" , :
pod 'WGNAPIClient/WOT' pod 'WGNAPIClient/WOWP'
. .
(98%). :
; .
:
context(@"API works with players data", ^{ it (@"should search players", ^{ stubResponse(@"/wot/account/list", @"players-search.json"); RACSignal *searchRequest = [client searchPlayers:@"garnett" limit:0]; NSArray *response = [searchRequest asynchronousFirstOrDefault:nil success:NULL error:&error]; [[error should] beNil]; [[response should] beKindOfClass:NSArray.class]; [[response.lastObject should] beKindOfClass:WOTSearchPlayer.class]; WOTSearchPlayer *player = response[0]; [[player.ID should] equal:@"1785514"]; }); });
, / , RACSignal , :
(id)asynchronousFirstOrDefault:(id)defaultValue success:(BOOL *)success error:(NSError **)error;
describe(@"WOTRatingType", ^{ it(@"should parse json to model object", ^{ NSDictionary *json = @{ @"threshold": @5, @"type": @"1", @"rank_fields": @[ @"xp_amount_rank", @"xp_max_rank", @"battles_count_rank", @"spotted_count_rank", @"damage_dealt_rank", ] }; NSError *error; WOTRatingType *ratingType = [MTLJSONAdapter modelOfClass:WOTRatingType.class fromJSONDictionary:json error:&error]; [[error should] beNil]; [[ratingType should] beKindOfClass:WOTRatingType.class]; [[json[@"threshold"] should] equal:theValue(ratingType.threshold)]; * [[json[@"type"] should] equal:ratingType.type]; [[ratingType.rankFields shouldNot] beNil]; [[ratingType.rankFields should] beKindOfClass:NSArray.class]; [[ratingType.rankFields should] haveCountOf:5]; [[@"maximumXP" should] equal:ratingType.rankFields[1]]; }); });
* — - - Kiwi, , nil .
API :
; .
, , : query http-. 
, ReactiveCocoa , ( ). ( ) 2k LOC ( ~1k - , ~700 - ), : — ..
Kiwi
BDD- RSpec - , . «--».
OHTTPStubs
, web- . "", - .
.
There are only two hard things in Computer Science: cache invalidation and naming things.
-- Phil Karlton
« » :
..
, .
- inMemory CoreData , NSCache . , , — . NSURLConnection .
NSURLCache
NSURLCache NSURLRequest . , .
:
NSURLCache *urlCache = [[NSURLCache alloc] initWithMemoryCapacity:kInMemoryCacheSize diskCapacity:kOnDiskCacheSize diskPath:path]; [NSURLCache setSharedURLCache:urlCache];
, .
NSURLConnectionDelegate , "" :
- (NSCachedURLResponse *)connection:(NSURLConnection *)connection willCacheResponse:(NSCachedURLResponse *)cachedResponse;
?
NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse*)[cachedResponse response]; NSDictionary *headers = [httpResponse allHeaderFields]; NSString *cacheControl = [headers valueForKey:@"Cache-Control"]; NSString *expires = [headers valueForKey:@"Expires"]; if((cacheControl == nil) && (expires == nil)) { NSMutableDictionary *modifiedHeaders = [httpResponse.allHeaderFields mutableCopy]; NSString *expireDate = [NSDate RFC822StringFromDate:[[NSDate date] dateByAddingTimeInterval:cacheTime]]; NSDictionary *values = @{@"Expires": expireDate, @"Cache-Control": @"public"}; [modifiedHeaders addEntriesFromDictionary:values]; NSHTTPURLResponse *response = [[NSHTTPURLResponse alloc] initWithURL:httpResponse.URL statusCode:httpResponse.statusCode HTTPVersion:@"HTTP/1.1" headerFields:modifiedHeaders]; NSCachedURLResponse *modifiedCachedResponse = [[NSCachedURLResponse alloc] initWithResponse:response data:cachedResponse.data userInfo:cachedResponse.userInfo storagePolicy:NSURLCacheStorageAllowed]; return modifiedCachedResponse; } return cachedResponse;
HTTP- .
, ( , , ). , ( cacheTime ) .
— ( ). , , .
:
(, , ..); - NSURLCache , .
:
30 ; ; : 200- , NSURLConnection .
""
API (, ), fields :
. . . , .
, Player , JSON-, . , fields , NSDictionary . , - . , .
JSON -> NSObject Mantle , API ( RACSignal API ):
- (RACSignal *)searchPlayers:(NSString *)query limit:(NSUInteger)limit { NSMutableDictionary *parameters = [NSMutableDictionary dictionary]; parameters[@"search"] = query; if (limit) parameters[@"limit"] = @(limit); return [self getPath:@"account/list" parameters:parameters resultClass:WOTSearchPlayer.class]; }
, resultClass , ? :
- (RACSignal *)searchPlayers:(NSString *)query limit:(NSUInteger)limit resultClass:(Class)resultClass
, API, .
API , : , , "" . ( ). , ( AFJSONRequestOperation ):
op1 = [AFJSONRequestOperation JSONRequestOperationWithRequest:nil success:^(id JSON) { op2 = [AFJSONRequestOperation JSONRequestOperationWithRequest:nil success:^(id JSON) { op3 = [AFJSONRequestOperation JSONRequestOperationWithRequest:nil success:^(id JSON) { // Combine all the responses and return } failure:^(NSError *error) { // Handle error }]; } failure:^(NSError *error) { // Handle error }]; } failure:^(NSError *error) { // Handle error }];
, , . ReactiveCocoa , .
API :
- (RACSignal *)searchPlayers:(NSString *)query limit:(NSUInteger)limit; - (RACSignal *)fetchPlayers:(NSArray *)playerIDs;
RACSignal — , , . — , . / :
RACSignal *fetchPlayersAndRatings = [[[[API searchPlayers:@"" limit:0] flattenMap:^RACStream *(id players) { // skipping data processing return [API fetchPlayers:@[]]; }] flattenMap:^RACStream *(id fullPlayers) { // Skipping data processing return [API statsSliceForPlayer:nil hoursAgo:@[] fields:nil]; }] flattenMap:^RACStream *(id value) { // Compose all responses here id composedResponse; return [RACSignal return:composedResponse]; }]; [fetchPlayersAndRatings subscribeNext:^(id x) { // Fully composed response } error:^(NSError *error) { // All erros go to one place }];
ReactiveCocoa — , . , Promises Futures.
:
Core ( , ); WOT ( World of Tanks–); WOWP ( World of Warplanes–)
, , — . , ( ) CocoaPods , . subspecs , :
# Subspecs s.subspec 'Core' do |cs| cs.source_files = 'WGNAPIClient/Core/**/*.{h,m}' end s.subspec 'WOT' do |cs| cs.dependency 'WGNAPIClient/Core' cs.source_files = 'WGNAPIClient/WOT/**/*.{h,m}' end s.subspec 'WOWP' do |cs| cs.dependency 'WGNAPIClient/Core' cs.source_files = 'WGNAPIClient/WOWP/**/*.{h,m}' end
"" "" , :
pod 'WGNAPIClient/WOT' pod 'WGNAPIClient/WOWP'
. .
(98%). :
; .
:
context(@"API works with players data", ^{ it (@"should search players", ^{ stubResponse(@"/wot/account/list", @"players-search.json"); RACSignal *searchRequest = [client searchPlayers:@"garnett" limit:0]; NSArray *response = [searchRequest asynchronousFirstOrDefault:nil success:NULL error:&error]; [[error should] beNil]; [[response should] beKindOfClass:NSArray.class]; [[response.lastObject should] beKindOfClass:WOTSearchPlayer.class]; WOTSearchPlayer *player = response[0]; [[player.ID should] equal:@"1785514"]; }); });
, / , RACSignal , :
(id)asynchronousFirstOrDefault:(id)defaultValue success:(BOOL *)success error:(NSError **)error;
describe(@"WOTRatingType", ^{ it(@"should parse json to model object", ^{ NSDictionary *json = @{ @"threshold": @5, @"type": @"1", @"rank_fields": @[ @"xp_amount_rank", @"xp_max_rank", @"battles_count_rank", @"spotted_count_rank", @"damage_dealt_rank", ] }; NSError *error; WOTRatingType *ratingType = [MTLJSONAdapter modelOfClass:WOTRatingType.class fromJSONDictionary:json error:&error]; [[error should] beNil]; [[ratingType should] beKindOfClass:WOTRatingType.class]; [[json[@"threshold"] should] equal:theValue(ratingType.threshold)]; * [[json[@"type"] should] equal:ratingType.type]; [[ratingType.rankFields shouldNot] beNil]; [[ratingType.rankFields should] beKindOfClass:NSArray.class]; [[ratingType.rankFields should] haveCountOf:5]; [[@"maximumXP" should] equal:ratingType.rankFields[1]]; }); });
* — - - Kiwi, , nil .
API :
; .
, , : query http-. 
, ReactiveCocoa , ( ). ( ) 2k LOC ( ~1k - , ~700 - ), : — .     . 
 
 Kiwi 
  BDD-    RSpec -  ,   .   «--». 
 
 OHTTPStubs 
   ,      web-   .      "",      - . 
 
     . 
 
    
 There are only two hard things in Computer Science: cache invalidation and naming things. 
 
 -- Phil Karlton 
  « »  : 
 
                .. 
 ,            . 
       -     inMemory CoreData ,       NSCache .   ,       ,         —         .         NSURLConnection . 
 
 NSURLCache 
 NSURLCache        NSURLRequest   .      ,    . 
 
   : 
 NSURLCache *urlCache = [[NSURLCache alloc] initWithMemoryCapacity:kInMemoryCacheSize diskCapacity:kOnDiskCacheSize diskPath:path]; [NSURLCache setSharedURLCache:urlCache]; 
     ,           . 
 
  NSURLConnectionDelegate   ,     ""    : 
 - (NSCachedURLResponse *)connection:(NSURLConnection *)connection willCacheResponse:(NSCachedURLResponse *)cachedResponse; 
    ? 
 
  NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse*)[cachedResponse response]; NSDictionary *headers = [httpResponse allHeaderFields]; NSString *cacheControl = [headers valueForKey:@"Cache-Control"]; NSString *expires = [headers valueForKey:@"Expires"]; if((cacheControl == nil) && (expires == nil)) { NSMutableDictionary *modifiedHeaders = [httpResponse.allHeaderFields mutableCopy]; NSString *expireDate = [NSDate RFC822StringFromDate:[[NSDate date] dateByAddingTimeInterval:cacheTime]]; NSDictionary *values = @{@"Expires": expireDate, @"Cache-Control": @"public"}; [modifiedHeaders addEntriesFromDictionary:values]; NSHTTPURLResponse *response = [[NSHTTPURLResponse alloc] initWithURL:httpResponse.URL statusCode:httpResponse.statusCode HTTPVersion:@"HTTP/1.1" headerFields:modifiedHeaders]; NSCachedURLResponse *modifiedCachedResponse = [[NSCachedURLResponse alloc] initWithResponse:response data:cachedResponse.data userInfo:cachedResponse.userInfo storagePolicy:NSURLCacheStorageAllowed]; return modifiedCachedResponse; } return cachedResponse; 
      HTTP-      . 
 
 ,     (    ,    ,            ).   ,     ( cacheTime )   . 
     —        (      ).    ,      ,    . 
 
 : 
 
        (, ,   ..);  -   NSURLCache ,   . 
 : 
     30  ;       ; :         200- , NSURLConnection   . 
  ""  
          API (,    ),     fields : 
 
   .   .    .    ,   . 
  ,   Player ,      JSON-,   .        ,       fields ,        NSDictionary .         ,  -       .          ,           . 
   JSON -> NSObject    Mantle ,     API      ( RACSignal   API      ): 
 
 - (RACSignal *)searchPlayers:(NSString *)query limit:(NSUInteger)limit { NSMutableDictionary *parameters = [NSMutableDictionary dictionary]; parameters[@"search"] = query; if (limit) parameters[@"limit"] = @(limit); return [self getPath:@"account/list" parameters:parameters resultClass:WOTSearchPlayer.class]; } 
  ,       resultClass ,         ? : 
 
  - (RACSignal *)searchPlayers:(NSString *)query limit:(NSUInteger)limit resultClass:(Class)resultClass 
 ,     API,           . 
 
       
     API     ,               : ,   ,     ""    . (    ).    ,       (   AFJSONRequestOperation ): 
 
  op1 = [AFJSONRequestOperation JSONRequestOperationWithRequest:nil success:^(id JSON) { op2 = [AFJSONRequestOperation JSONRequestOperationWithRequest:nil success:^(id JSON) { op3 = [AFJSONRequestOperation JSONRequestOperationWithRequest:nil success:^(id JSON) { // Combine all the responses and return } failure:^(NSError *error) { // Handle error }]; } failure:^(NSError *error) { // Handle error }]; } failure:^(NSError *error) { // Handle error }]; 
  ,    ,          .         ReactiveCocoa  ,       . 
  API    : 
 
 - (RACSignal *)searchPlayers:(NSString *)query limit:(NSUInteger)limit; - (RACSignal *)fetchPlayers:(NSArray *)playerIDs; 
 RACSignal —  ,       ,     .    —       ,    .          / : 
 
  RACSignal *fetchPlayersAndRatings = [[[[API searchPlayers:@"" limit:0] flattenMap:^RACStream *(id players) { // skipping data processing return [API fetchPlayers:@[]]; }] flattenMap:^RACStream *(id fullPlayers) { // Skipping data processing return [API statsSliceForPlayer:nil hoursAgo:@[] fields:nil]; }] flattenMap:^RACStream *(id value) { // Compose all responses here id composedResponse; return [RACSignal return:composedResponse]; }]; [fetchPlayersAndRatings subscribeNext:^(id x) { // Fully composed response } error:^(NSError *error) { // All erros go to one place }]; 
  ReactiveCocoa   —      ,      .        ,         Promises  Futures. 
 
      
        : 
 Core ( , ); WOT (    World of Tanks–); WOWP (    World of Warplanes–) 
  ,         ,      — . ,    (    )       CocoaPods ,       .       subspecs ,        : 
 
  # Subspecs s.subspec 'Core' do |cs| cs.source_files = 'WGNAPIClient/Core/**/*.{h,m}' end s.subspec 'WOT' do |cs| cs.dependency 'WGNAPIClient/Core' cs.source_files = 'WGNAPIClient/WOT/**/*.{h,m}' end s.subspec 'WOWP' do |cs| cs.dependency 'WGNAPIClient/Core' cs.source_files = 'WGNAPIClient/WOWP/**/*.{h,m}' end 
 
    ""  ""  ,      : 
 pod 'WGNAPIClient/WOT' pod 'WGNAPIClient/WOWP' 
     
              .        . 
         (98%).           : 
 
   ;    . 
   
     : 
  context(@"API works with players data", ^{ it (@"should search players", ^{ stubResponse(@"/wot/account/list", @"players-search.json"); RACSignal *searchRequest = [client searchPlayers:@"garnett" limit:0]; NSArray *response = [searchRequest asynchronousFirstOrDefault:nil success:NULL error:&error]; [[error should] beNil]; [[response should] beKindOfClass:NSArray.class]; [[response.lastObject should] beKindOfClass:WOTSearchPlayer.class]; WOTSearchPlayer *player = response[0]; [[player.ID should] equal:@"1785514"]; }); }); 
 ,     / ,    RACSignal      ,       : 
 (id)asynchronousFirstOrDefault:(id)defaultValue success:(BOOL *)success error:(NSError **)error; 
   
 describe(@"WOTRatingType", ^{ it(@"should parse json to model object", ^{ NSDictionary *json = @{ @"threshold": @5, @"type": @"1", @"rank_fields": @[ @"xp_amount_rank", @"xp_max_rank", @"battles_count_rank", @"spotted_count_rank", @"damage_dealt_rank", ] }; NSError *error; WOTRatingType *ratingType = [MTLJSONAdapter modelOfClass:WOTRatingType.class fromJSONDictionary:json error:&error]; [[error should] beNil]; [[ratingType should] beKindOfClass:WOTRatingType.class]; [[json[@"threshold"] should] equal:theValue(ratingType.threshold)]; * [[json[@"type"] should] equal:ratingType.type]; [[ratingType.rankFields shouldNot] beNil]; [[ratingType.rankFields should] beKindOfClass:NSArray.class]; [[ratingType.rankFields should] haveCountOf:5]; [[@"maximumXP" should] equal:ratingType.rankFields[1]]; }); }); 
 
 * —    - -    Kiwi,      ,     nil . 
 
       API    : 
    ;   . 
  ,  ,       :  query  http-.   
 
  
    ,      ReactiveCocoa ,         (    ).    ( )   2k LOC (  ~1k -  ,   ~700 -     ),     :        —         . .
Kiwi
BDD- RSpec - , . «--».
OHTTPStubs
, web- . "", - .
.
There are only two hard things in Computer Science: cache invalidation and naming things.
-- Phil Karlton
« » :
..
, .
- inMemory CoreData , NSCache . , , — . NSURLConnection .
NSURLCache
NSURLCache NSURLRequest . , .
:
NSURLCache *urlCache = [[NSURLCache alloc] initWithMemoryCapacity:kInMemoryCacheSize diskCapacity:kOnDiskCacheSize diskPath:path]; [NSURLCache setSharedURLCache:urlCache];
, .
NSURLConnectionDelegate , "" :
- (NSCachedURLResponse *)connection:(NSURLConnection *)connection willCacheResponse:(NSCachedURLResponse *)cachedResponse;
?
NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse*)[cachedResponse response]; NSDictionary *headers = [httpResponse allHeaderFields]; NSString *cacheControl = [headers valueForKey:@"Cache-Control"]; NSString *expires = [headers valueForKey:@"Expires"]; if((cacheControl == nil) && (expires == nil)) { NSMutableDictionary *modifiedHeaders = [httpResponse.allHeaderFields mutableCopy]; NSString *expireDate = [NSDate RFC822StringFromDate:[[NSDate date] dateByAddingTimeInterval:cacheTime]]; NSDictionary *values = @{@"Expires": expireDate, @"Cache-Control": @"public"}; [modifiedHeaders addEntriesFromDictionary:values]; NSHTTPURLResponse *response = [[NSHTTPURLResponse alloc] initWithURL:httpResponse.URL statusCode:httpResponse.statusCode HTTPVersion:@"HTTP/1.1" headerFields:modifiedHeaders]; NSCachedURLResponse *modifiedCachedResponse = [[NSCachedURLResponse alloc] initWithResponse:response data:cachedResponse.data userInfo:cachedResponse.userInfo storagePolicy:NSURLCacheStorageAllowed]; return modifiedCachedResponse; } return cachedResponse;
HTTP- .
, ( , , ). , ( cacheTime ) .
— ( ). , , .
:
(, , ..); - NSURLCache , .
:
30 ; ; : 200- , NSURLConnection .
""
API (, ), fields :
. . . , .
, Player , JSON-, . , fields , NSDictionary . , - . , .
JSON -> NSObject Mantle , API ( RACSignal API ):
- (RACSignal *)searchPlayers:(NSString *)query limit:(NSUInteger)limit { NSMutableDictionary *parameters = [NSMutableDictionary dictionary]; parameters[@"search"] = query; if (limit) parameters[@"limit"] = @(limit); return [self getPath:@"account/list" parameters:parameters resultClass:WOTSearchPlayer.class]; }
, resultClass , ? :
- (RACSignal *)searchPlayers:(NSString *)query limit:(NSUInteger)limit resultClass:(Class)resultClass
, API, .
API , : , , "" . ( ). , ( AFJSONRequestOperation ):
op1 = [AFJSONRequestOperation JSONRequestOperationWithRequest:nil success:^(id JSON) { op2 = [AFJSONRequestOperation JSONRequestOperationWithRequest:nil success:^(id JSON) { op3 = [AFJSONRequestOperation JSONRequestOperationWithRequest:nil success:^(id JSON) { // Combine all the responses and return } failure:^(NSError *error) { // Handle error }]; } failure:^(NSError *error) { // Handle error }]; } failure:^(NSError *error) { // Handle error }];
, , . ReactiveCocoa , .
API :
- (RACSignal *)searchPlayers:(NSString *)query limit:(NSUInteger)limit; - (RACSignal *)fetchPlayers:(NSArray *)playerIDs;
RACSignal — , , . — , . / :
RACSignal *fetchPlayersAndRatings = [[[[API searchPlayers:@"" limit:0] flattenMap:^RACStream *(id players) { // skipping data processing return [API fetchPlayers:@[]]; }] flattenMap:^RACStream *(id fullPlayers) { // Skipping data processing return [API statsSliceForPlayer:nil hoursAgo:@[] fields:nil]; }] flattenMap:^RACStream *(id value) { // Compose all responses here id composedResponse; return [RACSignal return:composedResponse]; }]; [fetchPlayersAndRatings subscribeNext:^(id x) { // Fully composed response } error:^(NSError *error) { // All erros go to one place }];
ReactiveCocoa — , . , Promises Futures.
:
Core ( , ); WOT ( World of Tanks–); WOWP ( World of Warplanes–)
, , — . , ( ) CocoaPods , . subspecs , :
# Subspecs s.subspec 'Core' do |cs| cs.source_files = 'WGNAPIClient/Core/**/*.{h,m}' end s.subspec 'WOT' do |cs| cs.dependency 'WGNAPIClient/Core' cs.source_files = 'WGNAPIClient/WOT/**/*.{h,m}' end s.subspec 'WOWP' do |cs| cs.dependency 'WGNAPIClient/Core' cs.source_files = 'WGNAPIClient/WOWP/**/*.{h,m}' end
"" "" , :
pod 'WGNAPIClient/WOT' pod 'WGNAPIClient/WOWP'
. .
(98%). :
; .
:
context(@"API works with players data", ^{ it (@"should search players", ^{ stubResponse(@"/wot/account/list", @"players-search.json"); RACSignal *searchRequest = [client searchPlayers:@"garnett" limit:0]; NSArray *response = [searchRequest asynchronousFirstOrDefault:nil success:NULL error:&error]; [[error should] beNil]; [[response should] beKindOfClass:NSArray.class]; [[response.lastObject should] beKindOfClass:WOTSearchPlayer.class]; WOTSearchPlayer *player = response[0]; [[player.ID should] equal:@"1785514"]; }); });
, / , RACSignal , :
(id)asynchronousFirstOrDefault:(id)defaultValue success:(BOOL *)success error:(NSError **)error;
describe(@"WOTRatingType", ^{ it(@"should parse json to model object", ^{ NSDictionary *json = @{ @"threshold": @5, @"type": @"1", @"rank_fields": @[ @"xp_amount_rank", @"xp_max_rank", @"battles_count_rank", @"spotted_count_rank", @"damage_dealt_rank", ] }; NSError *error; WOTRatingType *ratingType = [MTLJSONAdapter modelOfClass:WOTRatingType.class fromJSONDictionary:json error:&error]; [[error should] beNil]; [[ratingType should] beKindOfClass:WOTRatingType.class]; [[json[@"threshold"] should] equal:theValue(ratingType.threshold)]; * [[json[@"type"] should] equal:ratingType.type]; [[ratingType.rankFields shouldNot] beNil]; [[ratingType.rankFields should] beKindOfClass:NSArray.class]; [[ratingType.rankFields should] haveCountOf:5]; [[@"maximumXP" should] equal:ratingType.rankFields[1]]; }); });
* — - - Kiwi, , nil .
API :
; .
, , : query http-. 
, ReactiveCocoa , ( ). ( ) 2k LOC ( ~1k - , ~700 - ), : — ..
Kiwi
BDD- RSpec - , . «--».
OHTTPStubs
, web- . "", - .
.
There are only two hard things in Computer Science: cache invalidation and naming things.
-- Phil Karlton
« » :
..
, .
- inMemory CoreData , NSCache . , , — . NSURLConnection .
NSURLCache
NSURLCache NSURLRequest . , .
:
NSURLCache *urlCache = [[NSURLCache alloc] initWithMemoryCapacity:kInMemoryCacheSize diskCapacity:kOnDiskCacheSize diskPath:path]; [NSURLCache setSharedURLCache:urlCache];
, .
NSURLConnectionDelegate , "" :
- (NSCachedURLResponse *)connection:(NSURLConnection *)connection willCacheResponse:(NSCachedURLResponse *)cachedResponse;
?
NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse*)[cachedResponse response]; NSDictionary *headers = [httpResponse allHeaderFields]; NSString *cacheControl = [headers valueForKey:@"Cache-Control"]; NSString *expires = [headers valueForKey:@"Expires"]; if((cacheControl == nil) && (expires == nil)) { NSMutableDictionary *modifiedHeaders = [httpResponse.allHeaderFields mutableCopy]; NSString *expireDate = [NSDate RFC822StringFromDate:[[NSDate date] dateByAddingTimeInterval:cacheTime]]; NSDictionary *values = @{@"Expires": expireDate, @"Cache-Control": @"public"}; [modifiedHeaders addEntriesFromDictionary:values]; NSHTTPURLResponse *response = [[NSHTTPURLResponse alloc] initWithURL:httpResponse.URL statusCode:httpResponse.statusCode HTTPVersion:@"HTTP/1.1" headerFields:modifiedHeaders]; NSCachedURLResponse *modifiedCachedResponse = [[NSCachedURLResponse alloc] initWithResponse:response data:cachedResponse.data userInfo:cachedResponse.userInfo storagePolicy:NSURLCacheStorageAllowed]; return modifiedCachedResponse; } return cachedResponse;
HTTP- .
, ( , , ). , ( cacheTime ) .
— ( ). , , .
:
(, , ..); - NSURLCache , .
:
30 ; ; : 200- , NSURLConnection .
""
API (, ), fields :
. . . , .
, Player , JSON-, . , fields , NSDictionary . , - . , .
JSON -> NSObject Mantle , API ( RACSignal API ):
- (RACSignal *)searchPlayers:(NSString *)query limit:(NSUInteger)limit { NSMutableDictionary *parameters = [NSMutableDictionary dictionary]; parameters[@"search"] = query; if (limit) parameters[@"limit"] = @(limit); return [self getPath:@"account/list" parameters:parameters resultClass:WOTSearchPlayer.class]; }
, resultClass , ? :
- (RACSignal *)searchPlayers:(NSString *)query limit:(NSUInteger)limit resultClass:(Class)resultClass
, API, .
API , : , , "" . ( ). , ( AFJSONRequestOperation ):
op1 = [AFJSONRequestOperation JSONRequestOperationWithRequest:nil success:^(id JSON) { op2 = [AFJSONRequestOperation JSONRequestOperationWithRequest:nil success:^(id JSON) { op3 = [AFJSONRequestOperation JSONRequestOperationWithRequest:nil success:^(id JSON) { // Combine all the responses and return } failure:^(NSError *error) { // Handle error }]; } failure:^(NSError *error) { // Handle error }]; } failure:^(NSError *error) { // Handle error }];
, , . ReactiveCocoa , .
API :
- (RACSignal *)searchPlayers:(NSString *)query limit:(NSUInteger)limit; - (RACSignal *)fetchPlayers:(NSArray *)playerIDs;
RACSignal — , , . — , . / :
RACSignal *fetchPlayersAndRatings = [[[[API searchPlayers:@"" limit:0] flattenMap:^RACStream *(id players) { // skipping data processing return [API fetchPlayers:@[]]; }] flattenMap:^RACStream *(id fullPlayers) { // Skipping data processing return [API statsSliceForPlayer:nil hoursAgo:@[] fields:nil]; }] flattenMap:^RACStream *(id value) { // Compose all responses here id composedResponse; return [RACSignal return:composedResponse]; }]; [fetchPlayersAndRatings subscribeNext:^(id x) { // Fully composed response } error:^(NSError *error) { // All erros go to one place }];
ReactiveCocoa — , . , Promises Futures.
:
Core ( , ); WOT ( World of Tanks–); WOWP ( World of Warplanes–)
, , — . , ( ) CocoaPods , . subspecs , :
# Subspecs s.subspec 'Core' do |cs| cs.source_files = 'WGNAPIClient/Core/**/*.{h,m}' end s.subspec 'WOT' do |cs| cs.dependency 'WGNAPIClient/Core' cs.source_files = 'WGNAPIClient/WOT/**/*.{h,m}' end s.subspec 'WOWP' do |cs| cs.dependency 'WGNAPIClient/Core' cs.source_files = 'WGNAPIClient/WOWP/**/*.{h,m}' end
"" "" , :
pod 'WGNAPIClient/WOT' pod 'WGNAPIClient/WOWP'
. .
(98%). :
; .
:
context(@"API works with players data", ^{ it (@"should search players", ^{ stubResponse(@"/wot/account/list", @"players-search.json"); RACSignal *searchRequest = [client searchPlayers:@"garnett" limit:0]; NSArray *response = [searchRequest asynchronousFirstOrDefault:nil success:NULL error:&error]; [[error should] beNil]; [[response should] beKindOfClass:NSArray.class]; [[response.lastObject should] beKindOfClass:WOTSearchPlayer.class]; WOTSearchPlayer *player = response[0]; [[player.ID should] equal:@"1785514"]; }); });
, / , RACSignal , :
(id)asynchronousFirstOrDefault:(id)defaultValue success:(BOOL *)success error:(NSError **)error;
describe(@"WOTRatingType", ^{ it(@"should parse json to model object", ^{ NSDictionary *json = @{ @"threshold": @5, @"type": @"1", @"rank_fields": @[ @"xp_amount_rank", @"xp_max_rank", @"battles_count_rank", @"spotted_count_rank", @"damage_dealt_rank", ] }; NSError *error; WOTRatingType *ratingType = [MTLJSONAdapter modelOfClass:WOTRatingType.class fromJSONDictionary:json error:&error]; [[error should] beNil]; [[ratingType should] beKindOfClass:WOTRatingType.class]; [[json[@"threshold"] should] equal:theValue(ratingType.threshold)]; * [[json[@"type"] should] equal:ratingType.type]; [[ratingType.rankFields shouldNot] beNil]; [[ratingType.rankFields should] beKindOfClass:NSArray.class]; [[ratingType.rankFields should] haveCountOf:5]; [[@"maximumXP" should] equal:ratingType.rankFields[1]]; }); });
* — - - Kiwi, , nil .
API :
; .
, , : query http-. 
, ReactiveCocoa , ( ). ( ) 2k LOC ( ~1k - , ~700 - ), : — ..
Kiwi
BDD- RSpec - , . «--».
OHTTPStubs
, web- . "", - .
.
There are only two hard things in Computer Science: cache invalidation and naming things.
-- Phil Karlton
« » :
..
, .
- inMemory CoreData , NSCache . , , — . NSURLConnection .
NSURLCache
NSURLCache NSURLRequest . , .
:
NSURLCache *urlCache = [[NSURLCache alloc] initWithMemoryCapacity:kInMemoryCacheSize diskCapacity:kOnDiskCacheSize diskPath:path]; [NSURLCache setSharedURLCache:urlCache];
, .
NSURLConnectionDelegate , "" :
- (NSCachedURLResponse *)connection:(NSURLConnection *)connection willCacheResponse:(NSCachedURLResponse *)cachedResponse;
?
NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse*)[cachedResponse response]; NSDictionary *headers = [httpResponse allHeaderFields]; NSString *cacheControl = [headers valueForKey:@"Cache-Control"]; NSString *expires = [headers valueForKey:@"Expires"]; if((cacheControl == nil) && (expires == nil)) { NSMutableDictionary *modifiedHeaders = [httpResponse.allHeaderFields mutableCopy]; NSString *expireDate = [NSDate RFC822StringFromDate:[[NSDate date] dateByAddingTimeInterval:cacheTime]]; NSDictionary *values = @{@"Expires": expireDate, @"Cache-Control": @"public"}; [modifiedHeaders addEntriesFromDictionary:values]; NSHTTPURLResponse *response = [[NSHTTPURLResponse alloc] initWithURL:httpResponse.URL statusCode:httpResponse.statusCode HTTPVersion:@"HTTP/1.1" headerFields:modifiedHeaders]; NSCachedURLResponse *modifiedCachedResponse = [[NSCachedURLResponse alloc] initWithResponse:response data:cachedResponse.data userInfo:cachedResponse.userInfo storagePolicy:NSURLCacheStorageAllowed]; return modifiedCachedResponse; } return cachedResponse;
HTTP- .
, ( , , ). , ( cacheTime ) .
— ( ). , , .
:
(, , ..); - NSURLCache , .
:
30 ; ; : 200- , NSURLConnection .
""
API (, ), fields :
. . . , .
, Player , JSON-, . , fields , NSDictionary . , - . , .
JSON -> NSObject Mantle , API ( RACSignal API ):
- (RACSignal *)searchPlayers:(NSString *)query limit:(NSUInteger)limit { NSMutableDictionary *parameters = [NSMutableDictionary dictionary]; parameters[@"search"] = query; if (limit) parameters[@"limit"] = @(limit); return [self getPath:@"account/list" parameters:parameters resultClass:WOTSearchPlayer.class]; }
, resultClass , ? :
- (RACSignal *)searchPlayers:(NSString *)query limit:(NSUInteger)limit resultClass:(Class)resultClass
, API, .
API , : , , "" . ( ). , ( AFJSONRequestOperation ):
op1 = [AFJSONRequestOperation JSONRequestOperationWithRequest:nil success:^(id JSON) { op2 = [AFJSONRequestOperation JSONRequestOperationWithRequest:nil success:^(id JSON) { op3 = [AFJSONRequestOperation JSONRequestOperationWithRequest:nil success:^(id JSON) { // Combine all the responses and return } failure:^(NSError *error) { // Handle error }]; } failure:^(NSError *error) { // Handle error }]; } failure:^(NSError *error) { // Handle error }];
, , . ReactiveCocoa , .
API :
- (RACSignal *)searchPlayers:(NSString *)query limit:(NSUInteger)limit; - (RACSignal *)fetchPlayers:(NSArray *)playerIDs;
RACSignal — , , . — , . / :
RACSignal *fetchPlayersAndRatings = [[[[API searchPlayers:@"" limit:0] flattenMap:^RACStream *(id players) { // skipping data processing return [API fetchPlayers:@[]]; }] flattenMap:^RACStream *(id fullPlayers) { // Skipping data processing return [API statsSliceForPlayer:nil hoursAgo:@[] fields:nil]; }] flattenMap:^RACStream *(id value) { // Compose all responses here id composedResponse; return [RACSignal return:composedResponse]; }]; [fetchPlayersAndRatings subscribeNext:^(id x) { // Fully composed response } error:^(NSError *error) { // All erros go to one place }];
ReactiveCocoa — , . , Promises Futures.
:
Core ( , ); WOT ( World of Tanks–); WOWP ( World of Warplanes–)
, , — . , ( ) CocoaPods , . subspecs , :
# Subspecs s.subspec 'Core' do |cs| cs.source_files = 'WGNAPIClient/Core/**/*.{h,m}' end s.subspec 'WOT' do |cs| cs.dependency 'WGNAPIClient/Core' cs.source_files = 'WGNAPIClient/WOT/**/*.{h,m}' end s.subspec 'WOWP' do |cs| cs.dependency 'WGNAPIClient/Core' cs.source_files = 'WGNAPIClient/WOWP/**/*.{h,m}' end
"" "" , :
pod 'WGNAPIClient/WOT' pod 'WGNAPIClient/WOWP'
. .
(98%). :
; .
:
context(@"API works with players data", ^{ it (@"should search players", ^{ stubResponse(@"/wot/account/list", @"players-search.json"); RACSignal *searchRequest = [client searchPlayers:@"garnett" limit:0]; NSArray *response = [searchRequest asynchronousFirstOrDefault:nil success:NULL error:&error]; [[error should] beNil]; [[response should] beKindOfClass:NSArray.class]; [[response.lastObject should] beKindOfClass:WOTSearchPlayer.class]; WOTSearchPlayer *player = response[0]; [[player.ID should] equal:@"1785514"]; }); });
, / , RACSignal , :
(id)asynchronousFirstOrDefault:(id)defaultValue success:(BOOL *)success error:(NSError **)error;
describe(@"WOTRatingType", ^{ it(@"should parse json to model object", ^{ NSDictionary *json = @{ @"threshold": @5, @"type": @"1", @"rank_fields": @[ @"xp_amount_rank", @"xp_max_rank", @"battles_count_rank", @"spotted_count_rank", @"damage_dealt_rank", ] }; NSError *error; WOTRatingType *ratingType = [MTLJSONAdapter modelOfClass:WOTRatingType.class fromJSONDictionary:json error:&error]; [[error should] beNil]; [[ratingType should] beKindOfClass:WOTRatingType.class]; [[json[@"threshold"] should] equal:theValue(ratingType.threshold)]; * [[json[@"type"] should] equal:ratingType.type]; [[ratingType.rankFields shouldNot] beNil]; [[ratingType.rankFields should] beKindOfClass:NSArray.class]; [[ratingType.rankFields should] haveCountOf:5]; [[@"maximumXP" should] equal:ratingType.rankFields[1]]; }); });
* — - - Kiwi, , nil .
API :
; .
, , : query http-. 
, ReactiveCocoa , ( ). ( ) 2k LOC ( ~1k - , ~700 - ), : — ..
Kiwi
BDD- RSpec - , . «--».
OHTTPStubs
, web- . "", - .
.
There are only two hard things in Computer Science: cache invalidation and naming things.
-- Phil Karlton
« » :
..
, .
- inMemory CoreData , NSCache . , , — . NSURLConnection .
NSURLCache
NSURLCache NSURLRequest . , .
:
NSURLCache *urlCache = [[NSURLCache alloc] initWithMemoryCapacity:kInMemoryCacheSize diskCapacity:kOnDiskCacheSize diskPath:path]; [NSURLCache setSharedURLCache:urlCache];
, .
NSURLConnectionDelegate , "" :
- (NSCachedURLResponse *)connection:(NSURLConnection *)connection willCacheResponse:(NSCachedURLResponse *)cachedResponse;
?
NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse*)[cachedResponse response]; NSDictionary *headers = [httpResponse allHeaderFields]; NSString *cacheControl = [headers valueForKey:@"Cache-Control"]; NSString *expires = [headers valueForKey:@"Expires"]; if((cacheControl == nil) && (expires == nil)) { NSMutableDictionary *modifiedHeaders = [httpResponse.allHeaderFields mutableCopy]; NSString *expireDate = [NSDate RFC822StringFromDate:[[NSDate date] dateByAddingTimeInterval:cacheTime]]; NSDictionary *values = @{@"Expires": expireDate, @"Cache-Control": @"public"}; [modifiedHeaders addEntriesFromDictionary:values]; NSHTTPURLResponse *response = [[NSHTTPURLResponse alloc] initWithURL:httpResponse.URL statusCode:httpResponse.statusCode HTTPVersion:@"HTTP/1.1" headerFields:modifiedHeaders]; NSCachedURLResponse *modifiedCachedResponse = [[NSCachedURLResponse alloc] initWithResponse:response data:cachedResponse.data userInfo:cachedResponse.userInfo storagePolicy:NSURLCacheStorageAllowed]; return modifiedCachedResponse; } return cachedResponse;
HTTP- .
, ( , , ). , ( cacheTime ) .
— ( ). , , .
:
(, , ..); - NSURLCache , .
:
30 ; ; : 200- , NSURLConnection .
""
API (, ), fields :
. . . , .
, Player , JSON-, . , fields , NSDictionary . , - . , .
JSON -> NSObject Mantle , API ( RACSignal API ):
- (RACSignal *)searchPlayers:(NSString *)query limit:(NSUInteger)limit { NSMutableDictionary *parameters = [NSMutableDictionary dictionary]; parameters[@"search"] = query; if (limit) parameters[@"limit"] = @(limit); return [self getPath:@"account/list" parameters:parameters resultClass:WOTSearchPlayer.class]; }
, resultClass , ? :
- (RACSignal *)searchPlayers:(NSString *)query limit:(NSUInteger)limit resultClass:(Class)resultClass
, API, .
API , : , , "" . ( ). , ( AFJSONRequestOperation ):
op1 = [AFJSONRequestOperation JSONRequestOperationWithRequest:nil success:^(id JSON) { op2 = [AFJSONRequestOperation JSONRequestOperationWithRequest:nil success:^(id JSON) { op3 = [AFJSONRequestOperation JSONRequestOperationWithRequest:nil success:^(id JSON) { // Combine all the responses and return } failure:^(NSError *error) { // Handle error }]; } failure:^(NSError *error) { // Handle error }]; } failure:^(NSError *error) { // Handle error }];
, , . ReactiveCocoa , .
API :
- (RACSignal *)searchPlayers:(NSString *)query limit:(NSUInteger)limit; - (RACSignal *)fetchPlayers:(NSArray *)playerIDs;
RACSignal — , , . — , . / :
RACSignal *fetchPlayersAndRatings = [[[[API searchPlayers:@"" limit:0] flattenMap:^RACStream *(id players) { // skipping data processing return [API fetchPlayers:@[]]; }] flattenMap:^RACStream *(id fullPlayers) { // Skipping data processing return [API statsSliceForPlayer:nil hoursAgo:@[] fields:nil]; }] flattenMap:^RACStream *(id value) { // Compose all responses here id composedResponse; return [RACSignal return:composedResponse]; }]; [fetchPlayersAndRatings subscribeNext:^(id x) { // Fully composed response } error:^(NSError *error) { // All erros go to one place }];
ReactiveCocoa — , . , Promises Futures.
:
Core ( , ); WOT ( World of Tanks–); WOWP ( World of Warplanes–)
, , — . , ( ) CocoaPods , . subspecs , :
# Subspecs s.subspec 'Core' do |cs| cs.source_files = 'WGNAPIClient/Core/**/*.{h,m}' end s.subspec 'WOT' do |cs| cs.dependency 'WGNAPIClient/Core' cs.source_files = 'WGNAPIClient/WOT/**/*.{h,m}' end s.subspec 'WOWP' do |cs| cs.dependency 'WGNAPIClient/Core' cs.source_files = 'WGNAPIClient/WOWP/**/*.{h,m}' end
"" "" , :
pod 'WGNAPIClient/WOT' pod 'WGNAPIClient/WOWP'
. .
(98%). :
; .
:
context(@"API works with players data", ^{ it (@"should search players", ^{ stubResponse(@"/wot/account/list", @"players-search.json"); RACSignal *searchRequest = [client searchPlayers:@"garnett" limit:0]; NSArray *response = [searchRequest asynchronousFirstOrDefault:nil success:NULL error:&error]; [[error should] beNil]; [[response should] beKindOfClass:NSArray.class]; [[response.lastObject should] beKindOfClass:WOTSearchPlayer.class]; WOTSearchPlayer *player = response[0]; [[player.ID should] equal:@"1785514"]; }); });
, / , RACSignal , :
(id)asynchronousFirstOrDefault:(id)defaultValue success:(BOOL *)success error:(NSError **)error;
describe(@"WOTRatingType", ^{ it(@"should parse json to model object", ^{ NSDictionary *json = @{ @"threshold": @5, @"type": @"1", @"rank_fields": @[ @"xp_amount_rank", @"xp_max_rank", @"battles_count_rank", @"spotted_count_rank", @"damage_dealt_rank", ] }; NSError *error; WOTRatingType *ratingType = [MTLJSONAdapter modelOfClass:WOTRatingType.class fromJSONDictionary:json error:&error]; [[error should] beNil]; [[ratingType should] beKindOfClass:WOTRatingType.class]; [[json[@"threshold"] should] equal:theValue(ratingType.threshold)]; * [[json[@"type"] should] equal:ratingType.type]; [[ratingType.rankFields shouldNot] beNil]; [[ratingType.rankFields should] beKindOfClass:NSArray.class]; [[ratingType.rankFields should] haveCountOf:5]; [[@"maximumXP" should] equal:ratingType.rankFields[1]]; }); });
* — - - Kiwi, , nil .
API :
; .
, , : query http-. 
, ReactiveCocoa , ( ). ( ) 2k LOC ( ~1k - , ~700 - ), : — ..
Kiwi
BDD- RSpec - , . «--».
OHTTPStubs
, web- . "", - .
.
There are only two hard things in Computer Science: cache invalidation and naming things.
-- Phil Karlton
« » :
..
, .
- inMemory CoreData , NSCache . , , — . NSURLConnection .
NSURLCache
NSURLCache NSURLRequest . , .
:
NSURLCache *urlCache = [[NSURLCache alloc] initWithMemoryCapacity:kInMemoryCacheSize diskCapacity:kOnDiskCacheSize diskPath:path]; [NSURLCache setSharedURLCache:urlCache];
, .
NSURLConnectionDelegate , "" :
- (NSCachedURLResponse *)connection:(NSURLConnection *)connection willCacheResponse:(NSCachedURLResponse *)cachedResponse;
?
NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse*)[cachedResponse response]; NSDictionary *headers = [httpResponse allHeaderFields]; NSString *cacheControl = [headers valueForKey:@"Cache-Control"]; NSString *expires = [headers valueForKey:@"Expires"]; if((cacheControl == nil) && (expires == nil)) { NSMutableDictionary *modifiedHeaders = [httpResponse.allHeaderFields mutableCopy]; NSString *expireDate = [NSDate RFC822StringFromDate:[[NSDate date] dateByAddingTimeInterval:cacheTime]]; NSDictionary *values = @{@"Expires": expireDate, @"Cache-Control": @"public"}; [modifiedHeaders addEntriesFromDictionary:values]; NSHTTPURLResponse *response = [[NSHTTPURLResponse alloc] initWithURL:httpResponse.URL statusCode:httpResponse.statusCode HTTPVersion:@"HTTP/1.1" headerFields:modifiedHeaders]; NSCachedURLResponse *modifiedCachedResponse = [[NSCachedURLResponse alloc] initWithResponse:response data:cachedResponse.data userInfo:cachedResponse.userInfo storagePolicy:NSURLCacheStorageAllowed]; return modifiedCachedResponse; } return cachedResponse;
HTTP- .
, ( , , ). , ( cacheTime ) .
— ( ). , , .
:
(, , ..); - NSURLCache , .
:
30 ; ; : 200- , NSURLConnection .
""
API (, ), fields :
. . . , .
, Player , JSON-, . , fields , NSDictionary . , - . , .
JSON -> NSObject Mantle , API ( RACSignal API ):
- (RACSignal *)searchPlayers:(NSString *)query limit:(NSUInteger)limit { NSMutableDictionary *parameters = [NSMutableDictionary dictionary]; parameters[@"search"] = query; if (limit) parameters[@"limit"] = @(limit); return [self getPath:@"account/list" parameters:parameters resultClass:WOTSearchPlayer.class]; }
, resultClass , ? :
- (RACSignal *)searchPlayers:(NSString *)query limit:(NSUInteger)limit resultClass:(Class)resultClass
, API, .
API , : , , "" . ( ). , ( AFJSONRequestOperation ):
op1 = [AFJSONRequestOperation JSONRequestOperationWithRequest:nil success:^(id JSON) { op2 = [AFJSONRequestOperation JSONRequestOperationWithRequest:nil success:^(id JSON) { op3 = [AFJSONRequestOperation JSONRequestOperationWithRequest:nil success:^(id JSON) { // Combine all the responses and return } failure:^(NSError *error) { // Handle error }]; } failure:^(NSError *error) { // Handle error }]; } failure:^(NSError *error) { // Handle error }];
, , . ReactiveCocoa , .
API :
- (RACSignal *)searchPlayers:(NSString *)query limit:(NSUInteger)limit; - (RACSignal *)fetchPlayers:(NSArray *)playerIDs;
RACSignal — , , . — , . / :
RACSignal *fetchPlayersAndRatings = [[[[API searchPlayers:@"" limit:0] flattenMap:^RACStream *(id players) { // skipping data processing return [API fetchPlayers:@[]]; }] flattenMap:^RACStream *(id fullPlayers) { // Skipping data processing return [API statsSliceForPlayer:nil hoursAgo:@[] fields:nil]; }] flattenMap:^RACStream *(id value) { // Compose all responses here id composedResponse; return [RACSignal return:composedResponse]; }]; [fetchPlayersAndRatings subscribeNext:^(id x) { // Fully composed response } error:^(NSError *error) { // All erros go to one place }];
ReactiveCocoa — , . , Promises Futures.
:
Core ( , ); WOT ( World of Tanks–); WOWP ( World of Warplanes–)
, , — . , ( ) CocoaPods , . subspecs , :
# Subspecs s.subspec 'Core' do |cs| cs.source_files = 'WGNAPIClient/Core/**/*.{h,m}' end s.subspec 'WOT' do |cs| cs.dependency 'WGNAPIClient/Core' cs.source_files = 'WGNAPIClient/WOT/**/*.{h,m}' end s.subspec 'WOWP' do |cs| cs.dependency 'WGNAPIClient/Core' cs.source_files = 'WGNAPIClient/WOWP/**/*.{h,m}' end
"" "" , :
pod 'WGNAPIClient/WOT' pod 'WGNAPIClient/WOWP'
. .
(98%). :
; .
:
context(@"API works with players data", ^{ it (@"should search players", ^{ stubResponse(@"/wot/account/list", @"players-search.json"); RACSignal *searchRequest = [client searchPlayers:@"garnett" limit:0]; NSArray *response = [searchRequest asynchronousFirstOrDefault:nil success:NULL error:&error]; [[error should] beNil]; [[response should] beKindOfClass:NSArray.class]; [[response.lastObject should] beKindOfClass:WOTSearchPlayer.class]; WOTSearchPlayer *player = response[0]; [[player.ID should] equal:@"1785514"]; }); });
, / , RACSignal , :
(id)asynchronousFirstOrDefault:(id)defaultValue success:(BOOL *)success error:(NSError **)error;
describe(@"WOTRatingType", ^{ it(@"should parse json to model object", ^{ NSDictionary *json = @{ @"threshold": @5, @"type": @"1", @"rank_fields": @[ @"xp_amount_rank", @"xp_max_rank", @"battles_count_rank", @"spotted_count_rank", @"damage_dealt_rank", ] }; NSError *error; WOTRatingType *ratingType = [MTLJSONAdapter modelOfClass:WOTRatingType.class fromJSONDictionary:json error:&error]; [[error should] beNil]; [[ratingType should] beKindOfClass:WOTRatingType.class]; [[json[@"threshold"] should] equal:theValue(ratingType.threshold)]; * [[json[@"type"] should] equal:ratingType.type]; [[ratingType.rankFields shouldNot] beNil]; [[ratingType.rankFields should] beKindOfClass:NSArray.class]; [[ratingType.rankFields should] haveCountOf:5]; [[@"maximumXP" should] equal:ratingType.rankFields[1]]; }); });
* — - - Kiwi, , nil .
API :
; .
, , : query http-. 
, ReactiveCocoa , ( ). ( ) 2k LOC ( ~1k - , ~700 - ), : — ..
Kiwi
BDD- RSpec - , . «--».
OHTTPStubs
, web- . "", - .
.
There are only two hard things in Computer Science: cache invalidation and naming things.
-- Phil Karlton
« » :
..
, .
- inMemory CoreData , NSCache . , , — . NSURLConnection .
NSURLCache
NSURLCache NSURLRequest . , .
:
NSURLCache *urlCache = [[NSURLCache alloc] initWithMemoryCapacity:kInMemoryCacheSize diskCapacity:kOnDiskCacheSize diskPath:path]; [NSURLCache setSharedURLCache:urlCache];
, .
NSURLConnectionDelegate , "" :
- (NSCachedURLResponse *)connection:(NSURLConnection *)connection willCacheResponse:(NSCachedURLResponse *)cachedResponse;
?
NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse*)[cachedResponse response]; NSDictionary *headers = [httpResponse allHeaderFields]; NSString *cacheControl = [headers valueForKey:@"Cache-Control"]; NSString *expires = [headers valueForKey:@"Expires"]; if((cacheControl == nil) && (expires == nil)) { NSMutableDictionary *modifiedHeaders = [httpResponse.allHeaderFields mutableCopy]; NSString *expireDate = [NSDate RFC822StringFromDate:[[NSDate date] dateByAddingTimeInterval:cacheTime]]; NSDictionary *values = @{@"Expires": expireDate, @"Cache-Control": @"public"}; [modifiedHeaders addEntriesFromDictionary:values]; NSHTTPURLResponse *response = [[NSHTTPURLResponse alloc] initWithURL:httpResponse.URL statusCode:httpResponse.statusCode HTTPVersion:@"HTTP/1.1" headerFields:modifiedHeaders]; NSCachedURLResponse *modifiedCachedResponse = [[NSCachedURLResponse alloc] initWithResponse:response data:cachedResponse.data userInfo:cachedResponse.userInfo storagePolicy:NSURLCacheStorageAllowed]; return modifiedCachedResponse; } return cachedResponse;
HTTP- .
, ( , , ). , ( cacheTime ) .
— ( ). , , .
:
(, , ..); - NSURLCache , .
:
30 ; ; : 200- , NSURLConnection .
""
API (, ), fields :
. . . , .
, Player , JSON-, . , fields , NSDictionary . , - . , .
JSON -> NSObject Mantle , API ( RACSignal API ):
- (RACSignal *)searchPlayers:(NSString *)query limit:(NSUInteger)limit { NSMutableDictionary *parameters = [NSMutableDictionary dictionary]; parameters[@"search"] = query; if (limit) parameters[@"limit"] = @(limit); return [self getPath:@"account/list" parameters:parameters resultClass:WOTSearchPlayer.class]; }
, resultClass , ? :
- (RACSignal *)searchPlayers:(NSString *)query limit:(NSUInteger)limit resultClass:(Class)resultClass
, API, .
API , : , , "" . ( ). , ( AFJSONRequestOperation ):
op1 = [AFJSONRequestOperation JSONRequestOperationWithRequest:nil success:^(id JSON) { op2 = [AFJSONRequestOperation JSONRequestOperationWithRequest:nil success:^(id JSON) { op3 = [AFJSONRequestOperation JSONRequestOperationWithRequest:nil success:^(id JSON) { // Combine all the responses and return } failure:^(NSError *error) { // Handle error }]; } failure:^(NSError *error) { // Handle error }]; } failure:^(NSError *error) { // Handle error }];
, , . ReactiveCocoa , .
API :
- (RACSignal *)searchPlayers:(NSString *)query limit:(NSUInteger)limit; - (RACSignal *)fetchPlayers:(NSArray *)playerIDs;
RACSignal — , , . — , . / :
RACSignal *fetchPlayersAndRatings = [[[[API searchPlayers:@"" limit:0] flattenMap:^RACStream *(id players) { // skipping data processing return [API fetchPlayers:@[]]; }] flattenMap:^RACStream *(id fullPlayers) { // Skipping data processing return [API statsSliceForPlayer:nil hoursAgo:@[] fields:nil]; }] flattenMap:^RACStream *(id value) { // Compose all responses here id composedResponse; return [RACSignal return:composedResponse]; }]; [fetchPlayersAndRatings subscribeNext:^(id x) { // Fully composed response } error:^(NSError *error) { // All erros go to one place }];
ReactiveCocoa — , . , Promises Futures.
:
Core ( , ); WOT ( World of Tanks–); WOWP ( World of Warplanes–)
, , — . , ( ) CocoaPods , . subspecs , :
# Subspecs s.subspec 'Core' do |cs| cs.source_files = 'WGNAPIClient/Core/**/*.{h,m}' end s.subspec 'WOT' do |cs| cs.dependency 'WGNAPIClient/Core' cs.source_files = 'WGNAPIClient/WOT/**/*.{h,m}' end s.subspec 'WOWP' do |cs| cs.dependency 'WGNAPIClient/Core' cs.source_files = 'WGNAPIClient/WOWP/**/*.{h,m}' end
"" "" , :
pod 'WGNAPIClient/WOT' pod 'WGNAPIClient/WOWP'
. .
(98%). :
; .
:
context(@"API works with players data", ^{ it (@"should search players", ^{ stubResponse(@"/wot/account/list", @"players-search.json"); RACSignal *searchRequest = [client searchPlayers:@"garnett" limit:0]; NSArray *response = [searchRequest asynchronousFirstOrDefault:nil success:NULL error:&error]; [[error should] beNil]; [[response should] beKindOfClass:NSArray.class]; [[response.lastObject should] beKindOfClass:WOTSearchPlayer.class]; WOTSearchPlayer *player = response[0]; [[player.ID should] equal:@"1785514"]; }); });
, / , RACSignal , :
(id)asynchronousFirstOrDefault:(id)defaultValue success:(BOOL *)success error:(NSError **)error;
describe(@"WOTRatingType", ^{ it(@"should parse json to model object", ^{ NSDictionary *json = @{ @"threshold": @5, @"type": @"1", @"rank_fields": @[ @"xp_amount_rank", @"xp_max_rank", @"battles_count_rank", @"spotted_count_rank", @"damage_dealt_rank", ] }; NSError *error; WOTRatingType *ratingType = [MTLJSONAdapter modelOfClass:WOTRatingType.class fromJSONDictionary:json error:&error]; [[error should] beNil]; [[ratingType should] beKindOfClass:WOTRatingType.class]; [[json[@"threshold"] should] equal:theValue(ratingType.threshold)]; * [[json[@"type"] should] equal:ratingType.type]; [[ratingType.rankFields shouldNot] beNil]; [[ratingType.rankFields should] beKindOfClass:NSArray.class]; [[ratingType.rankFields should] haveCountOf:5]; [[@"maximumXP" should] equal:ratingType.rankFields[1]]; }); });
* — - - Kiwi, , nil .
API :
; .
, , : query http-. 
, ReactiveCocoa , ( ). ( ) 2k LOC ( ~1k - , ~700 - ), : — .Source: https://habr.com/ru/post/232037/
All Articles