web-dev-qa-db-ja.com

iOSネットワークアプリケーション(RESTクライアント)を構築するための最適なアーキテクチャアプローチ

私は経験のあるiOS開発者であり、この質問は本当に興味深いものです。このトピックに関する多くの異なるリソースと資料を見ましたが、それでもなお混乱しています。 iOSネットワークアプリケーションに最適なアーキテクチャは何ですか?基本的な抽象フレームワーク、パターンを意味します。これは、少数のサーバーリクエストしか持たない小さなアプリでも、複雑なRESTクライアントでも、すべてのネットワークアプリケーションに適合します。 Appleは、すべてのiOSアプリケーションの基本的なアーキテクチャアプローチとしてMVCを使用することを推奨していますが、MVCも、より現代のMVVMパターンも、ネットワークロジックコードの配置場所と一般的な整理方法を説明していません。

MVCSSの場合はService)のようなものを開発する必要がありますか?このServiceレイヤーには、すべてのAPIリクエストと他のネットワークロジックを配置しますが、これは非常に複雑です。いくつかの研究を行った後、私はこれに対する2つの基本的なアプローチを見つけました。 ここ WebサービスAPILoginRequestクラスやPostCommentRequestクラスなど)へのすべてのネットワークリクエストに対して、すべてがベースリクエスト抽象クラスAbstractBaseRequestから継承するクラスを作成することをお勧めします一般的なネットワークコードとその他の設定をカプセル化するグローバルネットワークマネージャーを作成します(複雑なオブジェクトマッピングと永続性、または標準APIを使用した独自のネットワーク通信実装がある場合は、AFNetworkingカスタマイズまたはRestKitチューニングが可能です)。しかし、このアプローチは私にとってオーバーヘッドのようです。別のアプローチは、最初のアプローチのようにシングルトンAPIディスパッチャーまたはマネージャークラスを持つことです。 だがしかし すべてのリクエストに対してクラスを作成し、代わりに、このマネージャークラスのインスタンスpublicメソッドとしてfetchContactsloginUserメソッドなどのようにすべてのリクエストをカプセル化します。私がまだ知らない他の興味深いアプローチはありますか?

そして、ServiceNetworkProviderレイヤー、またはMVCアーキテクチャーの最上部にあるものなど、このすべてのネットワーキング用に別のレイヤーを作成する必要がありますか、またはこのレイヤーを既存のMVCレイヤーに統合(注入)する必要がありますModel

美しいアプローチが存在することを知っていますか、またはFacebookクライアントやLinkedInクライアントなどのモバイルモンスターが、ネットワークロジックの指数関数的に増大する複雑さにどのように対処するかを知っていますか?

私は問題に対する正確で正式な答えがないことを知っています。 この質問の目標は、経験豊富なiOS開発者から最も興味深いアプローチを収集することです。最も推奨されるアプローチは受け入れられたものとしてマークされ、評判の賞金が授与されますが、他のアプローチは支持されます。これは主に理論的および研究上の質問です。 iOSのネットワークアプリケーションの基本的、抽象的、正しいアーキテクチャアプローチを理解したいと思います。経験豊富な開発者からの詳細な説明を期待しています。

309

I want to understand basic, abstract and correct architectural approach for networking applications in iOSnoアプリケーションアーキテクチャを構築するための「最良の」または「最も正しい」アプローチがあります。 veryクリエイティブな仕事です。常に最も簡単で拡張可能なアーキテクチャを選択する必要があります。これは、プロジェクトで作業を開始する開発者やチーム内の他の開発者にとって明らかなものですが、「良い」と「悪い」があり得ることに同意します" 建築。

あなたは言った:collect the most interesting approaches from experienced iOS developers、私のアプローチが最も面白いとか正しいとは思いませんが、いくつかのプロジェクトでそれを使って満足しています。これは、あなたが上で述べたもののハイブリッドなアプローチであり、私自身の研究努力からの改善もあります。私は、いくつかのよく知られているパターンとイディオムを組み合わせたアプローチの構築の問題に興味を持っています。多くの Fowlerのエンタープライズパターン をモバイルアプリケーションに正常に適用できると思います。以下に最も興味深いもののリストを示します。iOSアプリケーションアーキテクチャの作成に適用できます(私の意見): Service Layernit Of Workリモートファサードデータ転送オブジェクトゲートウェイレイヤースーパータイプ特殊なケースドメインモデル 。モデルレイヤーを常に正しく設計し、永続性を常に忘れないでください(アプリのパフォーマンスが大幅に向上する可能性があります)。これにはCore Dataを使用できます。しかし、するべきではない忘れてください。Core DataはORMやデータベースではなく、永続性を備えたオブジェクトグラフマネージャーです。そのため、Core Dataがニーズに対して重すぎる場合が非常に多く、 RealmCouchbase Lite などの新しいソリューションを確認したり、独自の軽量オブジェクトを構築したりできます。生のSQLiteまたは LevelDB に基づくマッピング/永続層。また、 Domain Driven Design および CQRS に慣れることをお勧めします。

最初は、私たちはshouldネットワーキング用の別のレイヤーを作成します。これは、太いコントローラーや重くて圧倒的なモデルが必要ないためです。私はそれらのfat model, skinny controllerの事を信じていません。しかし、私はskinny everythingアプローチを信じる。一般に、すべてのネットワーキングはビジネスロジックとして抽象化できるため、結果として別のレイヤーを配置する必要があります。 サービス層 は必要なものです:

It encapsulates the application's business logic,  controlling transactions 
and coordinating responses in the implementation of its operations.

MVCレルムService Layerでは、ドメインモデルとコントローラー間の仲介者のようなものです。 MVCS と呼ばれるこのアプローチのかなり類似したバリエーションがあります。ここで、Storeは実際にはServiceレイヤーです。 Storeは、モデルインスタンスを提供し、ネットワーク、キャッシングなどを処理します。するべきではないすべてのネットワークとビジネスロジックをサービスレイヤーに記述してください。これも悪い設計と見なすことができます。詳細については、 Anemic および Rich ドメインモデルをご覧ください。一部のサービスメソッドとビジネスロジックはモデルで処理できるため、「リッチ」な(動作を伴う)モデルになります。

私は常に2つのライブラリを広く使用しています: AFNetworking 2. および ReactiveCocoa 。ネットワークとWebサービスとやり取りする、または複雑なUIロジックを含む最新のアプリケーションでは、must haveであると思います。

アーキテクチャ

最初に、一般的なAPIClientクラスを作成します。これは、 AFHTTPSessionManager のサブクラスです。これは、アプリケーションのすべてのネットワークの主力です。すべてのサービスクラスは、実際のREST要求をそれに委任します。特定のアプリケーションで必要なHTTPクライアントのすべてのカスタマイズが含まれています:SSLのピン留め、エラー処理、および詳細な失敗理由を含む単純なNSErrorオブジェクトの作成と、すべてのAPIおよび接続エラーの説明(このような場合、コントローラーは正しく表示できます)ユーザー向けのメッセージ)、リクエストおよびレスポンスシリアライザー、httpヘッダー、その他のネットワーク関連のものの設定。その後、すべてのAPIリクエストを論理的にサブサービスに分割します。より正確には、 マイクロサービスUserSerivcesCommonServicesSecurityServicesFriendsServicesなど、実装するビジネスロジックに応じて。これらの各マイクロサービスは個別のクラスです。それらは一緒にService Layerを形成します。これらのクラスには、各APIリクエストのメソッドが含まれ、ドメインモデルを処理し、解析された応答モデルまたはRACSignalで常にNSErrorを呼び出し元に返します。

複雑なモデルのシリアル化ロジックがある場合は、そのための別のレイヤーを作成します。 Data Mapper のようなものですが、より一般的な例です。 JSON/XML->モデルマッパー。キャッシュがある場合:別のレイヤー/サービスとしても作成します(ビジネスロジックとキャッシュを混在させないでください)。どうして?正しいキャッシングレイヤーは、独自の落とし穴により非常に複雑になる可能性があるためです。人々は複雑なロジックを実装して、有効で予測可能なキャッシュを取得します。 profunctorに基づいたプロジェクションを使用したモノイダルキャッシング。 Carlos と呼ばれるこの美しいライブラリについて読むことができます。また、Core Dataはすべてのキャッシュの問題を本当に助け、より少ないロジックを書くことができることを忘れないでください。また、NSManagedObjectContextとサーバー要求モデルの間に何らかのロジックがある場合は、 Repository パターンを使用できます。これは、データを取得し、それを処理するビジネスロジックからエンティティモデルにマッピングするロジックを分離しますモデル。したがって、Core Dataベースのアーキテクチャを使用している場合でも、リポジトリパターンを使用することをお勧めします。リポジトリは、NSFetchRequestNSEntityDescriptionNSPredicateなどのようなものを、getputなどの単純なメソッドに抽象化できます。

サービス層でのこれらすべてのアクションの後、呼び出し元(View Controller)は、応答を使用して、信号操作、チェーン、マッピングなどの複雑な非同期処理をReactiveCocoaプリミティブの助けを借りて実行するか、サブスクライブして結果を表示できますビュー。特定のサービス呼び出しを対応するAPIClientGETPOSTPUTなどの要求に変換するDELETEのすべてのこれらのサービスクラスで Dependency Injection を注入します。RESTへのリクエスト終点。この場合、APIClientはすべてのコントローラーに暗黙的に渡され、APIClientサービスクラスをパラメーター化することで明示的に指定できます。これは、特定のサービスクラスに対してAPIClientの異なるカスタマイズを使用する場合に意味がありますが、何らかの理由で余分なコピーが必要ない場合、または特定の1つのインスタンス(カスタマイズなし)を常に使用することが確実な場合APIClient-シングルトンにしますが、サービスクラスをシングルトンとして作成しないでください。

次に、DIを備えた各View Controllerは、必要なサービスクラスを再度挿入し、適切なサービスメソッドを呼び出し、UIロジックを使用して結果を作成します。依存関係の注入には、 BloodMagic またはより強力なフレームワーク Typhoon を使用します。私はシングルトン、God APIManagerWhateverクラス、またはその他の間違ったものを決して使用しません。クラスをWhateverManagerと呼ぶと、これはその目的がわからないことを示しており、 悪い設計選択 であるためです。シングルトンもアンチパターンであり、mostの場合(まれなものを除く)は間違ったの解決策です。シングルトンは、次の3つの基準がすべて満たされている場合にのみ検討する必要があります。

  1. 単一インスタンスの所有権を合理的に割り当てることはできません。
  2. 遅延初期化が望ましいです。
  3. それ以外の場合、グローバルアクセスは提供されません。

私たちの場合、単一のインスタンスの所有権は問題ではなく、また、特定のサービスが必要な専用コントローラーが1つまたは複数であるため、神マネージャーをサービスに分割した後、グローバルアクセスは必要ありません(例えばUserProfileコントローラーはUserServicesなどに)。

SOLIDS原則を常に尊重し、 懸念の分離 を使用する必要があります。したがって、すべてのサービスメソッドとネットワーク呼び出しを1つのクラスに入れないでください。特に大規模なエンタープライズアプリケーションを開発する場合。そのため、依存関係の注入とサービスのアプローチを検討する必要があります。このアプローチは現代的であり、 post-OO と考えています。この場合、アプリケーションを制御ロジック(コントローラーとイベント)とパラメーターの2つの部分に分割します。

パラメータの一種は、通常の「データ」パラメータです。それは、関数を操作し、操作し、変更し、永続化するなどです。これらは、エンティティ、集計、コレクション、ケースクラスです。他の種類は「サービス」パラメータです。これらは、ビジネスロジックをカプセル化し、外部システムとの通信を可能にし、データアクセスを提供するクラスです。

これが私のアーキテクチャの一般的なワークフローの例です。ユーザーの友達のリストを表示するFriendsViewControllerがあり、友達から削除するオプションがあるとします。 FriendsServicesクラスに次のメソッドを作成します:

- (RACSignal *)removeFriend:(Friend * const)friend

ここで、Friendはモデル/ドメインオブジェクトです(または、類似した属性がある場合は、単にUserオブジェクトにできます)。このメソッドは、JSONパラメーターfriend_idFriendNSDictionaryfriend_request_idなどのnameからsurnameを解析します。この種のボイラープレートとモデルレイヤー(前後の解析、JSONでのネストされたオブジェクト階層の管理など)には、常に Mantle ライブラリを使用します。解析後、APIClientDELETEメソッドを呼び出して実際のRESTリクエストを作成し、ResponseRACSignalを呼び出し元(この場合はFriendsViewController)に返し、ユーザーなどに適切なメッセージを表示します。

アプリケーションが非常に大きい場合、ロジックをさらに明確にする必要があります。例えば。 alwaysRepositoryまたはモデルロジックをServiceの1つと混在させるのは適切ではありません。私のアプローチを説明したとき、removeFriendメソッドはService層にあるべきだと言っていましたが、もっと用心深くなると、Repositoryに属する方が良いことに気付くことができます。リポジトリとは何かを思い出しましょう。エリック・エヴァンスは、彼の本[DDD]で正確な説明をしました。

リポジトリは、特定のタイプのすべてのオブジェクトを概念的なセットとして表します。精巧なクエリ機能を除いて、コレクションのように機能します。

したがって、Repositoryは基本的に、コレクションスタイルのセマンティクス(追加、更新、削除)を使用してデータ/オブジェクトへのアクセスを提供するファサードです。これが、getFriendsListgetUserGroupsremoveFriendのようなものがある場合に、Repositoryに配置できる理由です。コレクションのようなセマンティクスはここではかなり明確だからです。そして、次のようなコード:

- (RACSignal *)approveFriendRequest:(FriendRequest * const)request;

基本的なCRUD操作を超えており、2つのドメインオブジェクト(FriendおよびRequest)を接続するため、間違いなくビジネスロジックです。そのため、Serviceレイヤーに配置する必要があります。また、私は注意したい:不要な抽象化を作成しないでください。これらすべてのアプローチを賢く使用してください。抽象化でアプリケーションを圧倒する場合、これは増加その偶発的な複雑さ、および複雑さ より多くの問題を引き起こす 他の何よりもソフトウェアシステムで

「古い」Objective-Cの例を説明しますが、このアプローチはSwift言語に非常に簡単に適合させることができます。このライブラリを使用することを強くお勧めします: Moya 。これにより、よりエレガントなAPIClientレイヤーを作成できます(覚えているとおりです)。これで、APIClientプロバイダーは、プロトコルに準拠し、構造化パターンマッチングを活用する拡張機能を持つ値型(enum)になります。 Swift列挙型+パターンマッチングにより、従来の関数型プログラミングの場合と同様に、 代数的データ型 を作成できます。マイクロサービスでは、この改善されたAPIClientプロバイダーを通常のObjective-Cアプローチで使用します。 Mantleの代わりにモデルレイヤーの場合、 ObjectMapperライブラリ を使用できます。または、よりエレガントで機能的な Argo ライブラリを使用したいです。

それで、どんなアプリケーションにも適応できる私の一般的なアーキテクチャのアプローチを説明したと思います。もちろん、さらに多くの改善点があります。関数型プログラミングを学ぶことをお勧めします。関数型プログラミングから多くの恩恵を受けることができますが、関数型プログラミングを使いすぎないでください。過度の共有されたグローバルな可変状態を排除し、 不変ドメインモデル を作成するか、外部副作用のない純粋な関数を作成することは、一般的には良い習慣であり、新しいSwift言語はこれを奨励します。しかし、常に覚えておいてください、重い純粋な機能パターンでコードをオーバーロードし、カテゴリ理論的アプローチはbadアイデアです。なぜなら、other開発者はコードを読んでサポートするからです。不変モデルのprismatic profunctorsおよびそのような種類にイライラするか怖い。 ReactiveCocoaの場合も同じです。コードをRACifyしないでください too much 。これは、特に初心者にとっては、非常に速く読めなくなる可能性があるためです。目標とロジックを本当に簡素化できる場合に使用します。

したがって、read a lot, mix, experiment, and try to pick up the best from different architectural approaches。それは私があなたに与えることができる最高のアドバイスです。

316

この質問の目標に従って、私たちのアーキテクチャのアプローチを説明したいと思います。

アーキテクチャアプローチ

一般的なiOSアプリケーションのアーキテクチャは、次のパターンに基づいています: サービスレイヤーMVVMUI Data Binding依存性注入 ;および Functional Reactive Programming パラダイム。

一般的な消費者向けアプリケーションを次の論理層にスライスできます。

  • アセンブリ
  • モデル
  • サービス
  • ストレージ
  • マネージャー
  • コーディネーター
  • UI
  • インフラ

Assembly layer は、アプリケーションのbootstrapポイントです。 Dependency Injectionコンテナと、アプリケーションのオブジェクトとその依存関係の宣言が含まれています。このレイヤーには、アプリケーションの構成(URL、サードパーティサービスキーなど)も含まれる場合があります。この目的のために、 Typhoon ライブラリを使用します。

Model layer には、ドメインモデルクラス、検証、マッピングが含まれます。モデルのマッピングには Mantle ライブラリを使用します。これは、JSON形式およびNSManagedObjectモデルへのシリアル化/逆シリアル化をサポートします。モデルの検証とフォーム表現には、 FXForms および FXModelValidation ライブラリを使用します。

Services layer は、ドメインモデルで表されるデータを送信または受信するために、外部システムと対話するために使用するサービスを宣言します。そのため、通常、サーバーAPI(エンティティごと)、メッセージングサービス( PubNub など)、ストレージサービス(Amazon S3など)と通信するためのサービスがあります。基本的に、SDKによって提供されるサービスラップオブジェクト( PubNub SDKなど)または独自の通信ロジックを実装します。一般的なネットワークでは、 AFNetworking ライブラリを使用します。

ストレージレイヤーの目的は、デバイス上のローカルデータストレージを整理することです。このためにCore Dataまたは Realm を使用します(どちらにも長所と短所があり、使用するものの決定は具体的な仕様に基づいています)。コアデータのセットアップでは、 MDMCoreData ライブラリと、すべてのエンティティのローカルストレージへのアクセスを提供するクラス-ストレージ-(サービスに類似)の束を使用します。 Realmでは、同様のストレージを使用してローカルストレージにアクセスします。

Managersレイヤーは、抽象化/ラッパーが存在する場所です。

マネージャーの役​​割は次のとおりです。

  • さまざまな実装(キーチェーン、NSDefaultsなど)を備えたCredentials Manager
  • 現在のユーザーセッションを保持および提供する方法を知っている現在のセッションマネージャー
  • メディアデバイスへのアクセスを提供するキャプチャパイプライン(ビデオ録画、オーディオ、写真撮影)
  • Bluetoothサービスおよび周辺機器へのアクセスを提供するBLEマネージャー
  • ジオロケーションマネージャー
  • ...

そのため、マネージャーの役​​割には、アプリケーションの動作に必要な特定の側面または関心事のロジックを実装するオブジェクトがあります。

私たちはシングルトンを避けようとしますが、このレイヤーは、必要に応じて彼らが住む場所です。

コーディネーターレイヤーは、特定のモジュール(機能、画面、ユーザー)に必要な一連の作業にロジックを結合するために、他のレイヤー(サービス、ストレージ、モデル)のオブジェクトに依存するオブジェクトを提供しますストーリーまたはユーザーエクスペリエンス)。通常、非同期操作をチェーン化し、成功および失敗のケースに対応する方法を知っています。例として、メッセージング機能と対応するMessagingCoordinatorオブジェクトを想像できます。メッセージ送信操作の処理は次のようになります。

  1. メッセージの検証(モデルレイヤー)
  2. メッセージをローカルに保存(メッセージストレージ)
  3. メッセージの添付ファイルをアップロードする(Amazon s3サービス)
  4. メッセージのステータスと添付ファイルのURLを更新し、メッセージをローカルに保存します(メッセージストレージ)
  5. メッセージをJSON形式にシリアル化する(モデルレイヤー)
  6. メッセージをPubNubに発行する(PubNubサービス)
  7. メッセージのステータスと属性を更新し、ローカルに保存します(メッセージストレージ)

上記の各ステップで、エラーが対応して処理されます。

UI layer は、次のサブレイヤーで構成されます。

  1. ViewModels
  2. ViewControllers
  3. 視聴回数

Massive View Controllerを回避するために、MVVMパターンを使用し、ViewModelsのUIプレゼンテーションに必要なロジックを実装します。通常、ViewModelには、依存関係としてコーディネーターとマネージャーがあります。 ViewControllersおよびいくつかの種類のビュー(テーブルビューセルなど)で使用されるViewModel。 ViewControllersとViewModelsの間の接着剤は、データバインディングとコマンドパターンです。その接着剤を使用できるようにするために、 ReactiveCocoa ライブラリを使用します。

また、ReactiveCocoaとそのRACSignalコンセプトをインターフェイスとして使用し、すべてのコーディネーター、サービス、ストレージメソッドの値タイプを返します。これにより、操作をチェーン化し、並列またはシリアルで実行したり、ReactiveCocoaが提供する他の多くの便利な機能を使用したりできます。

UIの動作を宣言的な方法で実装しようとしています。データバインディングと自動レイアウトは、この目標を達成するのに役立ちます。

Infrastructure layer には、アプリケーション作業に必要なすべてのヘルパー、拡張機能、ユーティリティが含まれています。


このアプローチは、私たちと私たちが通常構築するこれらのタイプのアプリに適しています。しかし、これは単なる具体的なチームの目的のために /を適応/変更すべき主観的なアプローチであることを理解する必要があります。

これがあなたを助けることを願っています!

また、このブログ投稿でiOS開発プロセスに関する詳細情報を見つけることができます iOS Development as a Service

30

IOSアプリはすべて異なるため、ここではさまざまな方法で検討する必要があると思いますが、通常は次のようにします。
すべてのAPI要求(通常はAPICommunicatorという名前)を処理する中央マネージャ(シングルトン)クラスを作成します。すべてのインスタンスメソッドはAPI呼び出しです。そして1つの中心的な(非公開の)方法があります。

-(RACSignal *)sendGetToServerToSubPath:(NSString *)path withParameters:(NSDictionary *)params;

記録には、ReactiveCocoaとAFNetworkingの2つの主要なライブラリ/フレームワークを使用します。 ReactiveCocoaは非同期ネットワーク応答を完璧に処理します、あなたはすることができます(sendNext:、sendError:など)。
このメソッドはAPIを呼び出し、結果を取得してRACを介して「raw」形式で送信します(AFNetworkingが返すものと同じ)。
そして、上記のメソッドを呼び出したgetStuffList:のようなメソッドは、そのシグナルをサブスクライブし、生データを(Motisのようなもので)オブジェクトに解析し、オブジェクトを一つずつ呼び出し元に送信します(getStuffList:と同様のメソッドコントローラが加入できることを示す信号).
購読しているコントローラはsubscribeNext:のブロックによってオブジェクトを受け取り、それらを処理します。

私はさまざまなアプリでいろいろな方法を試しましたが、これはなかなかうまくいきました。最近いくつかのアプリでこれを使っています。中小規模のプロジェクトにも大規模なプロジェクトにも適しています。修正する必要があります。
これが助けになることを願っています、私のアプローチについての他の人の意見、そしておそらくこれを改善することができると他の人がどのように思っているかを聞きたいです。

18
Rickye

私の状況では、私は通常 ResKit ライブラリを使ってネットワーク層を設定しています。それは使いやすい解析を提供します。それは私が異なる応答やもののためにマッピングを設定することに対する努力を減らします。

マッピングを自動的に設定するためのコードを追加するだけです。私は自分のモデルの基本クラスを定義します(メソッドが実装されているかどうかをチェックするコードがたくさんあるためプロトコルではなく、モデル自体のコードは少なくなります)。

MappableEntry.h

@interface MappableEntity : NSObject

+ (NSArray*)pathPatterns;
+ (NSArray*)keyPathes;
+ (NSArray*)fieldsArrayForMapping;
+ (NSDictionary*)fieldsDictionaryForMapping;
+ (NSArray*)relationships;

@end

MappableEntry.m

@implementation MappableEntity

+(NSArray*)pathPatterns {
    return @[];
}

+(NSArray*)keyPathes {
    return nil;
}

+(NSArray*)fieldsArrayForMapping {
    return @[];
}

+(NSDictionary*)fieldsDictionaryForMapping {
    return @{};
}

+(NSArray*)relationships {
    return @[];
}

@end

リレーションシップは、それに応じてネストしたオブジェクトを表すオブジェクトです。

RelationshipObject.h

@interface RelationshipObject : NSObject

@property (nonatomic,copy) NSString* source;
@property (nonatomic,copy) NSString* destination;
@property (nonatomic) Class mappingClass;

+(RelationshipObject*)relationshipWithKey:(NSString*)key andMappingClass:(Class)mappingClass;
+(RelationshipObject*)relationshipWithSource:(NSString*)source destination:(NSString*)destination andMappingClass:(Class)mappingClass;

@end

RelationshipObject.m

@implementation RelationshipObject

+(RelationshipObject*)relationshipWithKey:(NSString*)key andMappingClass:(Class)mappingClass {
    RelationshipObject* object = [[RelationshipObject alloc] init];
    object.source = key;
    object.destination = key;
    object.mappingClass = mappingClass;
    return object;
}

+(RelationshipObject*)relationshipWithSource:(NSString*)source destination:(NSString*)destination andMappingClass:(Class)mappingClass {
    RelationshipObject* object = [[RelationshipObject alloc] init];
    object.source = source;
    object.destination = destination;
    object.mappingClass = mappingClass;
    return object;
}

@end

それでは、RestKitのマッピングを次のように設定します。

ObjectMappingInitializer.h

@interface ObjectMappingInitializer : NSObject

+(void)initializeRKObjectManagerMapping:(RKObjectManager*)objectManager;

@end

ObjectMappingInitializer.m

@interface ObjectMappingInitializer (Private)

+ (NSArray*)mappableClasses;

@end

@implementation ObjectMappingInitializer

+(void)initializeRKObjectManagerMapping:(RKObjectManager*)objectManager {

    NSMutableDictionary *mappingObjects = [NSMutableDictionary dictionary];

    // Creating mappings for classes
    for (Class mappableClass in [self mappableClasses]) {
        RKObjectMapping *newMapping = [RKObjectMapping mappingForClass:mappableClass];
        [newMapping addAttributeMappingsFromArray:[mappableClass fieldsArrayForMapping]];
        [newMapping addAttributeMappingsFromDictionary:[mappableClass fieldsDictionaryForMapping]];
        [mappingObjects setObject:newMapping forKey:[mappableClass description]];
    }

    // Creating relations for mappings
    for (Class mappableClass in [self mappableClasses]) {
        RKObjectMapping *mapping = [mappingObjects objectForKey:[mappableClass description]];
        for (RelationshipObject *relation in [mappableClass relationships]) {
            [mapping addPropertyMapping:[RKRelationshipMapping relationshipMappingFromKeyPath:relation.source toKeyPath:relation.destination withMapping:[mappingObjects objectForKey:[relation.mappingClass description]]]];
        }
    }

    // Creating response descriptors with mappings
    for (Class mappableClass in [self mappableClasses]) {
        for (NSString* pathPattern in [mappableClass pathPatterns]) {
            if ([mappableClass keyPathes]) {
                for (NSString* keyPath in [mappableClass keyPathes]) {
                    [objectManager addResponseDescriptor:[RKResponseDescriptor responseDescriptorWithMapping:[mappingObjects objectForKey:[mappableClass description]] method:RKRequestMethodAny pathPattern:pathPattern keyPath:keyPath statusCodes:RKStatusCodeIndexSetForClass(RKStatusCodeClassSuccessful)]];
                }
            } else {
                [objectManager addResponseDescriptor:[RKResponseDescriptor responseDescriptorWithMapping:[mappingObjects objectForKey:[mappableClass description]] method:RKRequestMethodAny pathPattern:pathPattern keyPath:nil statusCodes:RKStatusCodeIndexSetForClass(RKStatusCodeClassSuccessful)]];
            }
        }
    }

    // Error Mapping
    RKObjectMapping *errorMapping = [RKObjectMapping mappingForClass:[Error class]];
    [errorMapping addAttributeMappingsFromArray:[Error fieldsArrayForMapping]];
    for (NSString *pathPattern in Error.pathPatterns) {
        [[RKObjectManager sharedManager] addResponseDescriptor:[RKResponseDescriptor responseDescriptorWithMapping:errorMapping method:RKRequestMethodAny pathPattern:pathPattern keyPath:nil statusCodes:RKStatusCodeIndexSetForClass(RKStatusCodeClassClientError)]];
    }
}

@end

@implementation ObjectMappingInitializer (Private)

+ (NSArray*)mappableClasses {
    return @[
        [FruiosPaginationResults class],
        [FruioItem class],
        [Pagination class],
        [ContactInfo class],
        [Credentials class],
        [User class]
    ];
}

@end

MappableEntry実装のいくつかの例:

User.h

@interface User : MappableEntity

@property (nonatomic) long userId;
@property (nonatomic, copy) NSString *username;
@property (nonatomic, copy) NSString *email;
@property (nonatomic, copy) NSString *password;
@property (nonatomic, copy) NSString *token;

- (instancetype)initWithUsername:(NSString*)username email:(NSString*)email password:(NSString*)password;

- (NSDictionary*)registrationData;

@end

User.m

@implementation User

- (instancetype)initWithUsername:(NSString*)username email:(NSString*)email password:(NSString*)password {
    if (self = [super init]) {
        self.username = username;
        self.email = email;
        self.password = password;
    }
    return self;
}

- (NSDictionary*)registrationData {
    return @{
        @"username": self.username,
        @"email": self.email,
        @"password": self.password
    };
}

+ (NSArray*)pathPatterns {
    return @[
        [NSString stringWithFormat:@"/api/%@/users/register", APIVersionString],
        [NSString stringWithFormat:@"/api/%@/users/login", APIVersionString]
    ];
}

+ (NSArray*)fieldsArrayForMapping {
    return @[ @"username", @"email", @"password", @"token" ];
}

+ (NSDictionary*)fieldsDictionaryForMapping {
    return @{ @"id": @"userId" };
}

@end

さて、Requestsのラッピングについて:

すべてのAPIRequestクラスで行の長さを減らすために、ブロック定義を含むヘッダーファイルがあります。

APICallbacks.h

typedef void(^SuccessCallback)();
typedef void(^SuccessCallbackWithObjects)(NSArray *objects);
typedef void(^ErrorCallback)(NSError *error);
typedef void(^ProgressBlock)(float progress);

そして私が使っている私のAPIRequestクラスの例:

LoginAPI.h

@interface LoginAPI : NSObject

- (void)loginWithCredentials:(Credentials*)credentials onSuccess:(SuccessCallbackWithObjects)onSuccess onError:(ErrorCallback)onError;

@end

LoginAPI.m

@implementation LoginAPI

- (void)loginWithCredentials:(Credentials*)credentials onSuccess:(SuccessCallbackWithObjects)onSuccess onError:(ErrorCallback)onError {
    [[RKObjectManager sharedManager] postObject:nil path:[NSString stringWithFormat:@"/api/%@/users/login", APIVersionString] parameters:[credentials credentialsData] success:^(RKObjectRequestOperation *operation, RKMappingResult *mappingResult) {
        onSuccess(mappingResult.array);
    } failure:^(RKObjectRequestOperation *operation, NSError *error) {
        onError(error);
    }];
}

@end

そして、あなたがコードでする必要があるすべては、単にあなたがそれを必要とするときはいつでもAPIオブジェクトを初期化して、そしてそれを呼び出すだけです:

SomeViewController.m

@implementation SomeViewController {
    LoginAPI *_loginAPI;
    // ...
}

- (void)viewDidLoad {
    [super viewDidLoad];

    _loginAPI = [[LoginAPI alloc] init];
    // ...
}

// ...

- (IBAction)signIn:(id)sender {
    [_loginAPI loginWithCredentials:_credentials onSuccess:^(NSArray *objects) {
        // Success Block
    } onError:^(NSError *error) {
        // Error Block
    }];
}

// ...

@end

私のコードは完璧ではありませんが、一度設定して別のプロジェクトに使用するのは簡単です。それが誰にとっても面白いのであれば、しばらく時間をかけてGitHubとCocoaPodsのどこかに普遍的な解決策を作ることができます。

8

私の考えでは、すべてのソフトウェアアーキテクチャはニーズによって動かされています。これが学習目的または個人的な目的のためのものである場合は、主な目的を決定し、それがアーキテクチャを推進するようにします。これが雇用の仕事であるならば、ビジネスニーズは最も重要です。トリックは、光沢のあるものが本当のニーズから気をそらさないようにすることです。私はこれをするのが難しいと思います。このビジネスには常に新しい光沢のあるものが登場し、それらの多くは有用ではありませんが、それを常に前もって説明することはできません。必要性に焦点を当て、可能であれば悪い選択を放棄しても構わないと思っています。

たとえば、私は最近、地元企業向けの写真共有アプリの簡単なプロトタイプを作成しました。ビジネス上の必要性はすばやく汚いものにすることだったので、そのアーキテクチャは、カメラをポップアップするためのiOSコードと、画像をS3ストアにアップロードしてSimpleDBドメインに書き込む送信ボタンに接続されたネットワークコードになりました。コードは簡単でコストは最小限で、クライアントはREST呼び出しでWeb経由でアクセス可能なスケーラブルな写真コレクションを持っています。安くて愚かで、アプリにはたくさんの欠陥があり、UIを時々ロックするでしょうが、プロトタイプのためにもっと多くのことをするのは無駄で、スタッフにデプロイしてパフォーマンスやスケーラビリティなしに何千ものテスト画像を簡単に生成できます心配です。巧妙なアーキテクチャが、それはニーズとコストに完璧にフィットします。

別のプロジェクトでは、ネットワークが利用可能になったときにバックグラウンドで会社のシステムと同期するローカルの安全なデータベースを実装する必要がありました。必要なものがすべて揃っていると思われるので、RestKitを使用したバックグラウンドシンクロナイザを作成しました。しかし、私は、独自のJSONからCoreDataへの変換を記述することで、それをもっと早く完了できるように、RestKitが非常に多くのカスタムコードを記述しなければならなかった。しかし、顧客はこのアプリを社内に持ち込みたいと思っていたので、RestKitは他のプラットフォームで使用されていたフレームワークと似ていると思いました。それが良い決断だったかどうか私は待っています。

繰り返しになりますが、私にとっての問題は必要性に焦点を当て、それによってアーキテクチャーを決定させることです。彼らはアプリがしばらくの間現場に出ていた後に表示されるコストをもたらすので私は地獄のようにサードパーティ製のパッケージを使用しないようにしてみてください。私はクラス階層がめったに得られないのでクラス階層を作らないようにします。完璧にフィットしないパッケージを採用するのではなく、妥当な期間内に何かを書くことができれば、それを行います。私のコードはデバッグのためにうまく構成されており、適切にコメントされていますが、サードパーティのパッケージはめったにそうではありません。そうは言っても、私はAFネットワーキングが無視したり、うまく構造化したり、よくコメントしたり、維持したりするにはあまりにも有用であると感じています。 RestKitは多くの一般的なケースをカバーしていますが、私がそれを使用するとき私が闘っていたような気がします、そして私が遭遇するデータソースのほとんどはカスタムコードで最もよく扱われる癖と問題でいっぱいです。最近のいくつかのアプリケーションでは、組み込みのJSONコンバーターを使用し、いくつかのユーティリティー・メソッドを作成しました。

私がいつも使用するパターンの1つは、ネットワーク呼び出しをメインスレッドから切断することです。最後の4〜5のアプリでは、dispatch_source_createを使用してバックグラウンドタイマータスクを設定しています。これは、頻繁に起動し、必要に応じてネットワークタスクを実行します。あなたはいくつかのスレッドセーフティ作業をする必要があり、UI変更コードがメインスレッドに送信されることを確認する必要があります。また、ユーザーが負担や遅延を感じないようにオンボーディング/初期化を行うのに役立ちます。これまでのところ、これはかなりうまくいっています。これらを検討することをお勧めします。

最後に、仕事が増え、OSが進化するにつれて、より良いソリューションを開発する傾向があると思います。他の人が必須であると主張するパターンやデザインに従わなければならないという私の信念を乗り越えるには何年もかかりました。それが地域の宗教の一部であるという文脈で働いているのであれば、私は学科の最善のエンジニアリングの実践を意味します、それから私は習慣に従って手紙に従います、それが彼らが私に支払っているものです。しかし、私はめったに古いデザインやパターンに従うことが最適な解決策であるとは思いません。私は常にビジネスニーズのプリズムを通してソリューションを検討し、それに合うようにアーキテクチャーを構築し、可能な限り単純なものにするようにしています。そこに十分ではないと思うけれど、すべてが正しく機能しているなら、私は正しい方向に進んでいます。

7
Fran K.

私はここから得たアプローチを使います: https://github.com/Constantine-Fry/Foursquare-API-v2 。私はSwiftでそのライブラリを書き直しました、そしてあなたはコードのこれらの部分からアーキテクチャのアプローチを見ることができます:

typealias OpertaionCallback = (success: Bool, result: AnyObject?) -> ()

class Foursquare{
    var authorizationCallback: OperationCallback?
    var operationQueue: NSOperationQueue
    var callbackQueue: dispatch_queue_t?

    init(){
        operationQueue = NSOperationQueue()
        operationQueue.maxConcurrentOperationCount = 7;
        callbackQueue = dispatch_get_main_queue();
    }

    func checkIn(venueID: String, shout: String, callback: OperationCallback) -> NSOperation {
        let parameters: Dictionary <String, String> = [
            "venueId":venueID,
            "shout":shout,
            "broadcast":"public"]
        return self.sendRequest("checkins/add", parameters: parameters, httpMethod: "POST", callback: callback)
    }

    func sendRequest(path: String, parameters: Dictionary <String, String>, httpMethod: String, callback:OperationCallback) -> NSOperation{
        let url = self.constructURL(path, parameters: parameters)
        var request = NSMutableURLRequest(URL: url)
        request.HTTPMethod = httpMethod
        let operation = Operation(request: request, callbackBlock: callback, callbackQueue: self.callbackQueue!)
        self.operationQueue.addOperation(operation)
        return operation
    }

    func constructURL(path: String, parameters: Dictionary <String, String>) -> NSURL {
        var parametersString = kFSBaseURL+path
        var firstItem = true
        for key in parameters.keys {
            let string = parameters[key]
            let mark = (firstItem ? "?" : "&")
            parametersString += "\(mark)\(key)=\(string)"
            firstItem = false
        }
    return NSURL(string: parametersString.stringByAddingPercentEscapesUsingEncoding(NSUTF8StringEncoding))
    }
}

class Operation: NSOperation {
    var callbackBlock: OpertaionCallback
    var request: NSURLRequest
    var callbackQueue: dispatch_queue_t

    init(request: NSURLRequest, callbackBlock: OpertaionCallback, callbackQueue: dispatch_queue_t) {
        self.request = request
        self.callbackBlock = callbackBlock
        self.callbackQueue = callbackQueue
    }

    override func main() {
        var error: NSError?
        var result: AnyObject?
        var response: NSURLResponse?

        var recievedData: NSData? = NSURLConnection.sendSynchronousRequest(self.request, returningResponse: &response, error: &error)

        if self.cancelled {return}

        if recievedData{
            result = NSJSONSerialization.JSONObjectWithData(recievedData, options: nil, error: &error)
            if result != nil {
                if result!.isKindOfClass(NSClassFromString("NSError")){
                    error = result as? NSError
            }
        }

        if self.cancelled {return}

        dispatch_async(self.callbackQueue, {
            if (error) {
                self.callbackBlock(success: false, result: error!);
            } else {
                self.callbackBlock(success: true, result: result!);
            }
            })
    }

    override var concurrent:Bool {get {return true}}
}

基本的に、NSURLRequestを作成し、JSON応答を解析し、その結果を持つコールバックブロックをキューに追加するNSOperationサブクラスがあります。メインAPIクラスはNSURLRequestを構築し、そのNSOperationサブクラスを初期化してそれをキューに追加します。

4
bzz

状況に応じていくつかのアプローチを取ります。ほとんどの場合、AFNetworkingは、ヘッダーの設定、マルチパートデータのアップロード、GET、POST、PUT&DELETEの使用など、最も簡単で堅牢なアプローチです。 URL多くの呼び出しがある複雑なアプリでは、これを次のような独自の便利なメソッドに要約することがあります。

-(void)makeRequestToUrl:(NSURL *)url withParameters:(NSDictionary *)parameters success:(void (^)(id responseObject))success failure:(void (^)(AFHTTPRequestOperation *operation, NSError *error))failure;

AFNetworkingはすでに別のコードベースになっている可能性があるため、フレームワークやその他のライブラリコンポーネントを作成している場合など、AFNetworkingが適切ではない状況がいくつかあります。このような状況では、単一の呼び出しを行う場合はNSMutableURLRequestをインラインで使用するか、要求/応答クラスに抽象化します。

3
Martin

私のアプリケーションを設計するとき、私はシングルトンを避けます。それらは多くの人にとって典型的なやり方ですが、他の場所でもっとエレガントな解決策を見つけることができると思います。通常、私のしていることは、CoreDataで自分のエンティティを作成し、それからRESTコードをNSManagedObjectカテゴリに入れることです。たとえば、私が新しいユーザーを作成してPOSTしたい場合は、次のようにします。

User* newUser = [User createInManagedObjectContext:managedObjectContext];
[newUser postOnSuccess:^(...) { ... } onFailure:^(...) { ... }];

オブジェクトマッピングにRESTKitを使用し、起動時に初期化します。シングルトンを通じてすべての通話をルーティングすることは時間の浪費であり、必要とされない多くの定型句を追加することになります。

NSManagedObject + Extensions.mでは、

+ (instancetype)createInContext:(NSManagedObjectContext*)context
{
    NSAssert(context.persistentStoreCoordinator.managedObjectModel.entitiesByName[[self entityName]] != nil, @"Entity with name %@ not found in model. Is your class name the same as your entity name?", [self entityName]);
    return [NSEntityDescription insertNewObjectForEntityForName:[self entityName] inManagedObjectContext:context];
}

NSManagedObject + Networking.mの場合:

- (void)getOnSuccess:(RESTSuccess)onSuccess onFailure:(RESTFailure)onFailure blockInput:(BOOL)blockInput
{
    [[RKObjectManager sharedManager] getObject:self path:nil parameters:nil success:onSuccess failure:onFailure];
    [self handleInputBlocking:blockInput];
}

カテゴリーを介して共通基本クラスの機能を拡張できるのに、なぜ追加のヘルパークラスを追加するのですか?

あなたが私の解決策のより詳細な情報に興味があるなら私に知らせてください。共有できてうれしいです。

1
Sandy Chapman

この質問にはすでに多くの優れた広範な回答がありますが、他に誰もいないので、私はそれを言及しなければならないと思います。

Swiftのためのアラモファイア。 https://github.com/Alamofire/Alamofire

それはAFNetworkingと同じ人々によって作成されますが、Swiftを念頭に置いてより直接的に設計されています。

0

純粋にクラス設計の観点からすると、通常は次のようになります。

  • 1つ以上のビューを制御するあなたのビューコントローラ
  • データモデルクラス - 実際に扱っている実在するエンティティの数とそれらがどのように関連しているかによって異なります。

    たとえば、4つの異なる表現(リスト、チャート、グラフなど)で表示されるアイテムの配列がある場合、アイテムのリスト用に1つ、アイテム用にもう1つのデータモデルクラスがあります。 item classのリストは4つのビューコントローラ - タブバーコントローラまたはナビゲーションコントローラのすべての子 - によって共有されます。

    データモデルクラスは、データを表示するだけでなく、それらをシリアライズして、それぞれがJSON/XML/CSV(またはその他の)エクスポートメソッドを介して独自のシリアライゼーションフォーマットを公開できるようにするのにも役立ちます。

  • REST AP​​Iエンドポイントと直接マッピングするAPIリクエストビルダークラスも必要であることを理解することが重要です。ユーザーをログインさせるAPIがあるとしましょう。そのため、Login APIビルダークラスはログインAPI用のPOST JSONペイロードを作成します。別の例では、カタログアイテムリストAPI用のAPIリクエストビルダークラスは、対応するapi用のGETクエリ文字列を作成し、REST GETクエリを起動します。

    これらのAPI Request Builderクラスは通常、View Controllerからデータを受け取り、UIの更新やその他の操作のために同じデータをView Controllerに返します。 View Controllerは、そのデータでData Modelオブジェクトを更新する方法を決定します。

  • 最後に、 RESTクライアントの中心 - APIデータフェッチャークラス =これはあなたのアプリが行うあらゆる種類のAPIリクエストには関係ありません。このクラスはおそらくシングルトンになるでしょうが、他の人が指摘したように、それはシングルトンである必要はありません。

    このリンクは単なる典型的な実装であり、セッション、クッキーなどのシナリオは考慮に入れていませんが、サードパーティのフレームワークを使用せずにうまくいくようにするには十分です。

0
Nirav Bhatt

試してみてください https://github.com/kevin0571/STNetTaskQueue

別々のクラスでAPIリクエストを作成します。

STNetTaskQueueはスレッド化とデリゲート/コールバックを扱います。

異なるプロトコルに拡張可能.

0
Kevin