Wargaming APIを使用するためのiOSライブラリを作成したとき



World of Tanks Assistant (WOT Assistant)World of Warplanes Assitant (WOWP Assistant)は、プレイヤーがゲーム内の統計を監視し、戦闘パフォーマンスを友人と比較し、技術的なヘルプ情報にオフラインでアクセスできるようにするコンパニオンアプリケーションです。

WOWP Assistantは比較的最近(2013年11月)登場し、World of Tanksのバージョンは2013年初めにゼロから書き直されました。これは、新しいWargaming Public APIへの移行と同時に行われました。 AssistantとAPI 相互作用のためにiOSライブラリを開発する最も技術的に興味深い側面が、開発者にとって有用であり、Wargaming Developers Contestコンテストの参加者のインスピレーションの源になることを願っています。

必要条件


ライブラリの主な高レベルの要件は、クライアントアプリケーションコードを簡素化するための使いやすさと、支援を提供したり、日常的なタスク(データキャッシングなど)を完全に解決したりする能力です。 以下では、プロジェクトの機能要件と非機能要件のリストを形式化しようとしました。


使用済みのサードパーティソリューション


これらの要件の実装の詳細に戻る前に、使用したライブラリを簡単に説明する価値があります。

AFNetworking

AFNetworkingは、ネットワークデータを操作するための事実上のライブラリ標準です。 その「汎用性」には多くの不要な機能もありますが、独自の機能を作成しないことにしました。

ReactiveCocoa

このライブラリは、iOSの世界に機能的な反応色をもたらします( Habréの記事 )。 現在、アシスタントアプリケーションで積極的に使用されています。 最初の段階では、API操作の個別のユニットとして要求を記述する便利な方法のように思えました(これが必要な理由は、以下の要求チェーンのセクションで説明します)。

マントル

iOS Githubチームの別のライブラリ。これにより、データモデルのレイヤーを大幅に簡素化できます。つまり、Webサービスの応答を解析します(READMEの例は非常に参考になります)。 ボーナスとして、すべてのオブジェクトが自動的にサポートを受けます。 .

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/J232037/


All Articles