.key, value: String(describing:

iOSアプリケーションでクライアント/サーバー相互作用ロジックを分離する方法

今日、ほとんどのモバイルアプリケーションは、クライアントとサーバーの相互作用に大きく依存しています。これは、重いタスクのほとんどをバックエンドサーバーにオフロードできることを意味するだけでなく、これらのモバイルアプリケーションがインターネット経由でのみ利用できるあらゆる種類の機能を提供できるようにします。

バックエンドサーバーは通常、サービスを提供するように設計されています RESTfulAPI 。より単純なアプリケーションの場合、スパゲッティコードを作成して取得したくなることがよくあります。 APIを呼び出すコードを残りのアプリケーションロジックと混合します。ただし、アプリケーションが複雑になり、ますます多くのAPIを処理するようになると、構造化されていない、計画されていない方法でこれらのAPIと対話することが厄介になる可能性があります。

適切に設計されたRESTクライアントネットワークモジュールを使用して、iOSアプリケーションコードをすっきりさせます。



適切に設計されたRESTクライアントネットワークモジュールを使用して、iOSアプリケーションコードをすっきりさせます。 つぶやき

この記事では、クリーンなRESTクライアントネットワーキングモジュールを構築するためのアーキテクチャアプローチについて説明します。 iOSアプリケーション これにより、すべてのクライアント/サーバー相互作用ロジックを残りのアプリケーションコードから分離しておくことができます。

クライアントサーバーアプリケーション

典型的なクライアント/サーバーの相互作用は次のようになります。

  1. ユーザーが何らかのアクションを実行します(たとえば、ボタンをタップしたり、画面上で他のジェスチャーを実行したりします)。
  2. アプリケーションは、ユーザーアクションに応答してHTTP / REST要求を準備して送信します。
  3. サーバーは要求を処理し、それに応じてアプリケーションに応答します。
  4. アプリケーションは応答を受信し、それに基づいてユーザーインターフェイスを更新します。

一見、全体的なプロセスは単純に見えるかもしれませんが、詳細について考える必要があります。

バックエンドサーバーAPIがアドバタイズされたとおりに機能すると仮定しても( ない 常に当てはまります!)、設計が不十分な場合が多く、非効率的であるか、使用が困難ですらあります。一般的な煩わしさの1つは、APIへのすべての呼び出しで、呼び出し元が同じ情報を冗長的に提供する必要があることです(たとえば、要求データのフォーマット方法、サーバーが現在サインインしているユーザーを識別するために使用できるアクセストークンなど)。

モバイルアプリケーションでは、さまざまな目的で複数のバックエンドサーバーを同時に利用する必要がある場合もあります。たとえば、1つのサーバーがユーザー認証専用で、別のサーバーが分析の収集のみを処理する場合があります。

さらに、一般的なRESTクライアントは、リモートAPIを呼び出すだけではありません。保留中のリクエストをキャンセルする機能、またはエラーを処理するためのクリーンで管理しやすいアプローチは、堅牢なモバイルアプリケーションに組み込む必要がある機能の例です。

アーキテクチャの概要

RESTクライアントのコアは、次のコンポーネント上に構築されます。

これは、これらの各コンポーネントが相互に作用する方法です。

上の画像の矢印1から10は、サービスを呼び出すアプリケーションと、最終的に要求されたデータをモデルオブジェクトとして返すサービスとの間の理想的な一連の操作を示しています。そのフローの各コンポーネントには、特定の役割があります。 関心事の分離 モジュール内。

実装

現在ログインしているユーザーの友達のリストをロードする架空のソーシャルネットワークアプリケーションの一部として、RESTクライアントを実装します。リモートサーバーが応答にJSONを使用すると仮定します。

モデルとパーサーを実装することから始めましょう。

生のJSONからモデルオブジェクトへ

最初のモデルUserは、ソーシャルネットワークのすべてのユーザーの情報の構造を定義します。簡単にするために、このチュートリアルに絶対に必要なフィールドのみを含めます(実際のアプリケーションでは、構造には通常、より多くのプロパティがあります)。

struct User { var id: String var email: String? var name: String? }

APIを介してバックエンドサーバーからすべてのユーザーデータを受信するため、次の方法が必要です。 API応答を解析します 有効なUserにオブジェクト。これを行うには、Userにコンストラクターを追加します。解析されたJSONオブジェクト(Dictionary)をパラメーターとして受け入れます。 JSONオブジェクトをエイリアスタイプとして定義します。

typealias JSON = [String: Any]

次に、コンストラクター関数をUserに追加します。次のように構造体を作成します。

extension User { init?(json: JSON) { guard let id = json['id'] as? String else { return nil } self.id = id self.email = json['email'] as? String self.name = json['name'] as? String } }

Userの元のデフォルトコンストラクターを保持するために、Userの拡張機能を介してコンストラクターを追加します。タイプ。

次に、Userを作成します生のAPI応答からのオブジェクトの場合、次の2つの手順を実行する必要があります。

// Transform raw JSON data to parsed JSON object using JSONSerializer (part of standard library) let userObject = (try? JSONSerialization.jsonObject(with: data, options: [])) as? JSON // Create an instance of `User` structure from parsed JSON object let user = userObject.flatMap(User.init)

合理化されたエラー処理

バックエンドサーバーとの対話を試みるときに発生する可能性のあるさまざまなエラーを表すタイプを定義します。このようなエラーはすべて、次の3つの基本的なカテゴリに分類できます。

エラーオブジェクトを列挙型として定義できます。そして、私たちがそれに取り組んでいる間、私たちのServiceErrorを作るのは良い考えですタイプはに準拠します Errorプロトコル 。これにより、Swiftが提供する標準メカニズム(throwを使用してエラーをスローするなど)を使用して、これらのエラー値を使用および処理できるようになります。

enum ServiceError: Error { case noInternetConnection case custom(String) case other }

noInternetConnectionとは異なりおよびotherエラーの場合、カスタムエラーには値が関連付けられています。これにより、サーバーからのエラー応答をエラー自体の関連値として使用できるようになり、エラーのコンテキストが増えます。

それでは、errorDescriptionを追加しましょうServiceErrorへのプロパティエラーをより説明的にするための列挙。 noInternetConnectionのハードコードされたメッセージを追加しますおよびotherエラーが発生し、関連する値をcustomのメッセージとして使用しますエラー。

extension ServiceError: LocalizedError { var errorDescription: String? { switch self { case .noInternetConnection: return 'No Internet connection' case .other: return 'Something went wrong' case .custom(let message): return message } } }

ServiceErrorに実装する必要があるものがもう1つあります列挙。 customの場合エラーの場合、サーバーのJSONデータをエラーオブジェクトに変換する必要があります。これを行うには、モデルの場合に使用したのと同じアプローチを使用します。

extension ServiceError { init(json: JSON) { if let message = json['message'] as? String { self = .custom(message) } else { self = .other } } }

アプリケーションとバックエンドサーバー間のギャップを埋める

クライアントコンポーネントは、アプリケーションとバックエンドサーバーの間の仲介役になります。これは、アプリケーションとサーバーが通信する方法を定義する重要なコンポーネントですが、データモデルとその構造については何も知りません。クライアントは、提供されたパラメーターを使用して特定のURLを呼び出し、JSONオブジェクトとして解析された受信JSONデータを返す責任があります。

enum RequestMethod: String { case get = 'GET' case post = 'POST' case put = 'PUT' case delete = 'DELETE' } final class WebClient { private var baseUrl: String init(baseUrl: String) { self.baseUrl = baseUrl } func load(path: String, method: RequestMethod, params: JSON, completion: @escaping (Any?, ServiceError?) -> ()) -> URLSessionDataTask? { // TODO: Add implementation } }

上記のコードで何が起こっているのかを調べてみましょう…

最初に、4つの一般的なHTTPメソッドを記述する列挙型RequestMethodを宣言しました。これらは、RESTAPIで使用されるメソッドの1つです。

WebClientクラスにはbaseURLが含まれます受信したすべての相対URLを解決するために使用されるプロパティ。アプリケーションが複数のサーバーと対話する必要がある場合は、WebClientの複数のインスタンスを作成できます。それぞれbaseURLの値が異なります。

クライアントには、loadに相対的なパスをとる単一のメソッドbaseURLがあります。パラメーター、要求メソッド、要求パラメーター、および完了クロージャーとして。完了クロージャは、解析されたJSONとServiceErrorを使用して呼び出されますパラメータとして。今のところ、上記の方法には実装がありません。これについては後ほど説明します。

loadを実装する前にメソッドには、URLを作成する方法が必要です。メソッドで利用可能なすべての情報から。 URLを拡張しますこの目的のためのクラス:

extension URL { init(baseUrl: String, path: String, params: JSON, method: RequestMethod) { var components = URLComponents(string: baseUrl)! components.path += path switch method { case .get, .delete: components.queryItems = params.map { URLQueryItem(name: $0.key, value: String(describing: $0.value)) } default: break } self = components.url! } }

ここでは、ベースURLへのパスを追加するだけです。 GETおよびDELETEHTTPメソッドの場合、クエリパラメータもURL文字列に追加します。

次に、URLRequestのインスタンスを作成できる必要があります与えられたパラメータから。これを行うには、URLに対して行ったのと同様のことを行います。

extension URLRequest { init(baseUrl: String, path: String, method: RequestMethod, params: JSON) { let url = URL(baseUrl: baseUrl, path: path, params: params, method: method) self.init(url: url) httpMethod = method.rawValue setValue('application/json', forHTTPHeaderField: 'Accept') setValue('application/json', forHTTPHeaderField: 'Content-Type') switch method { case .post, .put: httpBody = try! JSONSerialization.data(withJSONObject: params, options: []) default: break } } }

ここでは、最初にURLを作成します拡張機能のコンストラクターを使用します。次に、URLRequestのインスタンスを初期化します。このURLを使用して、必要に応じていくつかのHTTPヘッダーを設定し、POSTまたはPUT HTTPメソッドの場合は、リクエストの本文にパラメーターを追加します。

すべての前提条件をカバーしたので、loadを実装できます。方法:

final class WebClient { private var baseUrl: String init(baseUrl: String) { self.baseUrl = baseUrl } func load(path: String, method: RequestMethod, params: JSON, completion: @escaping (Any?, ServiceError?) -> ()) -> URLSessionDataTask? { // Checking internet connection availability if !Reachability.isConnectedToNetwork() { completion(nil, ServiceError.noInternetConnection) return nil } // Adding common parameters var parameters = params if let token = KeychainWrapper.itemForKey('application_token') { parameters['token'] = token } // Creating the URLRequest object let request = URLRequest(baseUrl: baseUrl, path: path, method: method, params: params) // Sending request to the server. let task = URLSession.shared.dataTask(with: request) { data, response, error in // Parsing incoming data var object: Any? = nil if let data = data { object = try? JSONSerialization.jsonObject(with: data, options: []) } if let httpResponse = response as? HTTPURLResponse, (200..<300) ~= httpResponse.statusCode { completion(object, nil) } else { let error = (object as? JSON).flatMap(ServiceError.init) ?? ServiceError.other completion(nil, error) } } task.resume() return task } }

load上記の方法は、次の手順を実行します。

  1. インターネット接続の可用性を確認してください。 インターネット接続が利用できない場合は、noInternetConnectionですぐに完了クロージャと呼びます。パラメータとしてのエラー。 (注:コード内のReachabilityはカスタムクラスであり、 一般的なアプローチの1つ インターネット接続を確認します。)
  2. 共通パラメータを追加します。 。これには、アプリケーショントークンやユーザーIDなどの一般的なパラメーターを含めることができます。
  3. URLRequestを作成しますオブジェクト、 拡張機能のコンストラクターを使用します。
  4. サーバーにリクエストを送信します。 URLSessionを使用しますサーバーにデータを送信するオブジェクト。
  5. 受信データを解析します。 サーバーが応答すると、最初にJSONSerializationを使用して応答ペイロードをJSONオブジェクトに解析します。次に、応答のステータスコードを確認します。それが成功コード(つまり、200から299の範囲)である場合、JSONオブジェクトを使用して完了クロージャを呼び出します。それ以外の場合は、JSONオブジェクトをServiceErrorに変換しますオブジェクトを作成し、そのエラーオブジェクトを使用して完了クロージャを呼び出します。

論理的にリンクされた操作のためのサービスの定義

私たちのアプリケーションの場合、ユーザーの友達に関連するタスクを処理するサービスが必要です。このために、FriendsServiceを作成しますクラス。理想的には、このようなクラスは、友達のリストの取得、新しい友達の追加、友達の削除、一部の友達のカテゴリへのグループ化などの操作を担当します。このチュートリアルでは、簡単にするために1つのメソッドのみを実装します。 :

final class FriendsService { private let client = WebClient(baseUrl: 'https://your_server_host/api/v1') @discardableResult func loadFriends(forUser user: User, completion: @escaping ([User]?, ServiceError?) -> ()) -> URLSessionDataTask? { let params: JSON = ['user_id': user.id] return client.load(path: '/friends', method: .get, params: params) { result, error in let dictionaries = result as? [JSON] completion(dictionaries?.flatMap(User.init), error) } } }

FriendsServiceクラスにはclientが含まれますタイプWebClientのプロパティ。友達の管理を担当するリモートサーバーのベースURLで初期化されます。前述のように、他のサービスクラスでは、WebClientの異なるインスタンスを持つことができます。必要に応じて別のURLで初期化されます。

1台のサーバーのみで動作するアプリケーションの場合、WebClientクラスには、そのサーバーのURLで初期化するコンストラクターを指定できます。

final class WebClient { // ... init() { self.baseUrl = 'https://your_server_base_url' } // ... }

loadFriendsメソッドは、呼び出されると、必要なすべてのパラメーターを準備し、FriendServiceWebClientのインスタンスを使用します。 APIリクエストを作成します。 WebClientを介してサーバーから応答を受信した後、JSONオブジェクトをUserに変換します。それらをパラメーターとしてモデル化し、完了クロージャーを呼び出します。

FriendServiceの典型的な使用法次のようになります。

let friendsTask: URLSessionDataTask! let activityIndicator: UIActivityIndicatorView! var friends: [User] = [] func friendsButtonTapped() { friendsTask?.cancel() //Cancel previous loading task. activityIndicator.startAnimating() //Show loading indicator friendsTask = FriendsService().loadFriends(forUser: currentUser) {[weak self] friends, error in DispatchQueue.main.async { self?.activityIndicator.stopAnimating() //Stop loading indicators if let error = error { print(error.localizedDescription) //Handle service error } else if let friends = friends { self?.friends = friends //Update friends property self?.updateUI() //Update user interface } } } }

上記の例では、関数friendsButtonTappedを想定しています。ユーザーがネットワーク内の友達のリストを表示することを目的としたボタンをタップするたびに呼び出されます。また、friendsTaskにタスクへの参照を保持しますプロパティを使用して、friendsTask?.cancel()を呼び出していつでもリクエストをキャンセルできます。

これにより、保留中のリクエストのライフサイクルをより細かく制御できるようになり、リクエストが無関係になったと判断したときにリクエストを終了できるようになります。

結論

この記事では、iOSアプリケーション用のネットワークモジュールのシンプルなアーキテクチャを共有しました。これは、実装が簡単で、ほとんどのiOSアプリケーションの複雑なネットワークニーズに適合させることができます。ただし、これからの重要なポイントは、適切に設計されたRESTクライアントとそれに付随するコンポーネント(アプリケーションロジックの残りの部分から分離されている)が、アプリケーション自体がますます複雑になった場合でも、アプリケーションのクライアント/サーバー相互作用コードを単純に保つのに役立つことです。 。

この記事が次のiOSアプリケーションの構築に役立つことを願っています。このネットワークモジュールのソースコードを見つけることができます GitHubで 。コードをチェックして、フォークして、変更して、遊んでください。

あなたとあなたのプロジェクトにとってより好ましい他のアーキテクチャを見つけた場合は、以下のコメントセクションで詳細を共有してください。

関連: MantleとRealmを使用したiOSでのRESTfulAPIの使用とデータの永続性の簡素化 .value)) } default: break } self = components.url! } }

ここでは、ベースURLへのパスを追加するだけです。 GETおよびDELETEHTTPメソッドの場合、クエリパラメータもURL文字列に追加します。

次に、URLRequestのインスタンスを作成できる必要があります与えられたパラメータから。これを行うには、URLに対して行ったのと同様のことを行います。

extension URLRequest { init(baseUrl: String, path: String, method: RequestMethod, params: JSON) { let url = URL(baseUrl: baseUrl, path: path, params: params, method: method) self.init(url: url) httpMethod = method.rawValue setValue('application/json', forHTTPHeaderField: 'Accept') setValue('application/json', forHTTPHeaderField: 'Content-Type') switch method { case .post, .put: httpBody = try! JSONSerialization.data(withJSONObject: params, options: []) default: break } } }

ここでは、最初にURLを作成します拡張機能のコンストラクターを使用します。次に、URLRequestのインスタンスを初期化します。このURLを使用して、必要に応じていくつかのHTTPヘッダーを設定し、POSTまたはPUT HTTPメソッドの場合は、リクエストの本文にパラメーターを追加します。

すべての前提条件をカバーしたので、loadを実装できます。方法:

final class WebClient { private var baseUrl: String init(baseUrl: String) { self.baseUrl = baseUrl } func load(path: String, method: RequestMethod, params: JSON, completion: @escaping (Any?, ServiceError?) -> ()) -> URLSessionDataTask? { // Checking internet connection availability if !Reachability.isConnectedToNetwork() { completion(nil, ServiceError.noInternetConnection) return nil } // Adding common parameters var parameters = params if let token = KeychainWrapper.itemForKey('application_token') { parameters['token'] = token } // Creating the URLRequest object let request = URLRequest(baseUrl: baseUrl, path: path, method: method, params: params) // Sending request to the server. let task = URLSession.shared.dataTask(with: request) { data, response, error in // Parsing incoming data var object: Any? = nil if let data = data { object = try? JSONSerialization.jsonObject(with: data, options: []) } if let httpResponse = response as? HTTPURLResponse, (200..<300) ~= httpResponse.statusCode { completion(object, nil) } else { let error = (object as? JSON).flatMap(ServiceError.init) ?? ServiceError.other completion(nil, error) } } task.resume() return task } }

load上記の方法は、次の手順を実行します。

  1. インターネット接続の可用性を確認してください。 インターネット接続が利用できない場合は、noInternetConnectionですぐに完了クロージャと呼びます。パラメータとしてのエラー。 (注:コード内のReachabilityはカスタムクラスであり、 一般的なアプローチの1つ インターネット接続を確認します。)
  2. 共通パラメータを追加します。 。これには、アプリケーショントークンやユーザーIDなどの一般的なパラメーターを含めることができます。
  3. URLRequestを作成しますオブジェクト、 拡張機能のコンストラクターを使用します。
  4. サーバーにリクエストを送信します。 URLSessionを使用しますサーバーにデータを送信するオブジェクト。
  5. 受信データを解析します。 サーバーが応答すると、最初にJSONSerializationを使用して応答ペイロードをJSONオブジェクトに解析します。次に、応答のステータスコードを確認します。それが成功コード(つまり、200から299の範囲)である場合、JSONオブジェクトを使用して完了クロージャを呼び出します。それ以外の場合は、JSONオブジェクトをServiceErrorに変換しますオブジェクトを作成し、そのエラーオブジェクトを使用して完了クロージャを呼び出します。

論理的にリンクされた操作のためのサービスの定義

私たちのアプリケーションの場合、ユーザーの友達に関連するタスクを処理するサービスが必要です。このために、FriendsServiceを作成しますクラス。理想的には、このようなクラスは、友達のリストの取得、新しい友達の追加、友達の削除、一部の友達のカテゴリへのグループ化などの操作を担当します。このチュートリアルでは、簡単にするために1つのメソッドのみを実装します。 :

final class FriendsService { private let client = WebClient(baseUrl: 'https://your_server_host/api/v1') @discardableResult func loadFriends(forUser user: User, completion: @escaping ([User]?, ServiceError?) -> ()) -> URLSessionDataTask? { let params: JSON = ['user_id': user.id] return client.load(path: '/friends', method: .get, params: params) { result, error in let dictionaries = result as? [JSON] completion(dictionaries?.flatMap(User.init), error) } } }

FriendsServiceクラスにはclientが含まれますタイプWebClientのプロパティ。友達の管理を担当するリモートサーバーのベースURLで初期化されます。前述のように、他のサービスクラスでは、WebClientの異なるインスタンスを持つことができます。必要に応じて別のURLで初期化されます。

1台のサーバーのみで動作するアプリケーションの場合、WebClientクラスには、そのサーバーのURLで初期化するコンストラクターを指定できます。

VisualStudioでiOSアプリを作成する
final class WebClient { // ... init() { self.baseUrl = 'https://your_server_base_url' } // ... }

loadFriendsメソッドは、呼び出されると、必要なすべてのパラメーターを準備し、FriendServiceWebClientのインスタンスを使用します。 APIリクエストを作成します。 WebClientを介してサーバーから応答を受信した後、JSONオブジェクトをUserに変換します。それらをパラメーターとしてモデル化し、完了クロージャーを呼び出します。

FriendServiceの典型的な使用法次のようになります。

let friendsTask: URLSessionDataTask! let activityIndicator: UIActivityIndicatorView! var friends: [User] = [] func friendsButtonTapped() { friendsTask?.cancel() //Cancel previous loading task. activityIndicator.startAnimating() //Show loading indicator friendsTask = FriendsService().loadFriends(forUser: currentUser) {[weak self] friends, error in DispatchQueue.main.async { self?.activityIndicator.stopAnimating() //Stop loading indicators if let error = error { print(error.localizedDescription) //Handle service error } else if let friends = friends { self?.friends = friends //Update friends property self?.updateUI() //Update user interface } } } }

上記の例では、関数friendsButtonTappedを想定しています。ユーザーがネットワーク内の友達のリストを表示することを目的としたボタンをタップするたびに呼び出されます。また、friendsTaskにタスクへの参照を保持しますプロパティを使用して、friendsTask?.cancel()を呼び出していつでもリクエストをキャンセルできます。

これにより、保留中のリクエストのライフサイクルをより細かく制御できるようになり、リクエストが無関係になったと判断したときにリクエストを終了できるようになります。

結論

この記事では、iOSアプリケーション用のネットワークモジュールのシンプルなアーキテクチャを共有しました。これは、実装が簡単で、ほとんどのiOSアプリケーションの複雑なネットワークニーズに適合させることができます。ただし、これからの重要なポイントは、適切に設計されたRESTクライアントとそれに付随するコンポーネント(アプリケーションロジックの残りの部分から分離されている)が、アプリケーション自体がますます複雑になった場合でも、アプリケーションのクライアント/サーバー相互作用コードを単純に保つのに役立つことです。 。

この記事が次のiOSアプリケーションの構築に役立つことを願っています。このネットワークモジュールのソースコードを見つけることができます GitHubで 。コードをチェックして、フォークして、変更して、遊んでください。

あなたとあなたのプロジェクトにとってより好ましい他のアーキテクチャを見つけた場合は、以下のコメントセクションで詳細を共有してください。

関連: MantleとRealmを使用したiOSでのRESTfulAPIの使用とデータの永続性の簡素化