📜 ⬆️ ⬇️

As we wrote the iOS library to work with the Wargaming API



World of Tanks Assistant (WOT Assistant) and World of Warplanes Assitant (WOWP Assistant) are companion apps for players that allow you to monitor in-game statistics, compare your combat performance with your friends, and also provide offline access to technical information .

WOWP Assistant appeared relatively recently (November 2013), and the version for World of Tanks was rewritten from scratch in early 2013, which coincided with the transition to the new Wargaming Public API . Hopefully, the most technically interesting moments of developing an iOS library for Assistant's interaction with the API will be useful for developers and provide inspiration for participants in the Wargaming Developers Contest.

Requirements


The main high-level requirement for the library is ease of use and the ability to provide assistance or even completely solve some everyday tasks (for example, data caching) in order to simplify the client application code. Below, I tried to formalize the list of functional and non-functional project requirements:
')

Used third-party solutions


Before we go back to the details of the implementation of these requirements, it is worth mentioning briefly which libraries we used.

AFNetworking

AFNetworking is the de facto standard library for working with network data. Although its “universality” pulls a bunch of unnecessary functionality behind itself, we decided not to write our own.

ReactiveCocoa

The library brings functional reactive paints to the world of iOS ( article on Habré ). Currently actively used in assistant applications. At the initial stage, it seemed to me a convenient way to describe requests as separate units of the API (for what it was needed, it will be discussed in the section on the query chain below).

Mantle

Another library from the iOS team Github, which allows you to significantly simplify the data model layer, namely the parsing of web services responses (the example in README is quite indicative). As a bonus, all objects automatically receive support. .

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 - ), : — .
  1. .

    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 - ), : — .
  2. .

    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 - ), : — .
  1. .

    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 - ), : — .
  2. .

    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