Cara Mengisolasi Logika Interaksi Client-Server di Aplikasi iOS
Diterbitkan: 2022-03-11Saat ini, sebagian besar aplikasi seluler sangat bergantung pada interaksi klien-server. Ini tidak hanya berarti bahwa mereka dapat memindahkan sebagian besar tugas berat mereka ke server back-end, tetapi juga memungkinkan aplikasi seluler ini menawarkan semua jenis fitur dan fungsionalitas yang hanya dapat tersedia melalui internet.
Server back-end biasanya dirancang untuk menawarkan layanan mereka melalui RESTful API. Untuk aplikasi yang lebih sederhana, kita sering tergoda untuk membuat kode spaghetti; mencampur kode yang memanggil API dengan logika aplikasi lainnya. Namun karena aplikasi tumbuh kompleks dan berurusan dengan semakin banyak API, interaksi dengan API ini dapat menjadi gangguan dengan cara yang tidak terstruktur dan tidak terencana.
Artikel ini membahas pendekatan arsitektur untuk membangun modul jaringan klien REST yang bersih untuk aplikasi iOS yang memungkinkan Anda menjaga semua logika interaksi klien-server terisolasi dari kode aplikasi lainnya.
Aplikasi Client-Server
Interaksi klien-server yang khas terlihat seperti ini:
- Seorang pengguna melakukan beberapa tindakan (misalnya, mengetuk beberapa tombol atau melakukan beberapa gerakan lain di layar).
- Aplikasi menyiapkan dan mengirimkan permintaan HTTP/REST sebagai tanggapan atas tindakan pengguna.
- Server memproses permintaan dan merespons sesuai dengan aplikasi.
- Aplikasi menerima respons dan memperbarui antarmuka pengguna berdasarkan itu.
Sekilas, proses keseluruhan mungkin terlihat sederhana, tetapi kita harus memikirkan detailnya.
Bahkan dengan asumsi bahwa API server backend berfungsi seperti yang diiklankan (yang tidak selalu demikian!), seringkali dapat dirancang dengan buruk sehingga tidak efisien, atau bahkan sulit, untuk digunakan. Satu gangguan umum adalah bahwa semua panggilan ke API mengharuskan pemanggil untuk memberikan informasi yang sama secara berlebihan (misalnya, bagaimana data permintaan diformat, token akses yang dapat digunakan server untuk mengidentifikasi pengguna yang saat ini masuk, dan seterusnya).
Aplikasi seluler mungkin juga perlu menggunakan beberapa server back-end secara bersamaan untuk tujuan yang berbeda. Satu server dapat, misalnya, didedikasikan untuk otentikasi pengguna sementara yang lain hanya berurusan dengan pengumpulan analitik.
Selain itu, klien REST biasa perlu melakukan lebih dari sekadar memanggil API jarak jauh. Kemampuan untuk membatalkan permintaan yang tertunda, atau pendekatan yang bersih dan mudah dikelola untuk menangani kesalahan, adalah contoh fungsionalitas yang perlu dibangun ke dalam aplikasi seluler yang kuat.
Gambaran Umum Arsitektur
Inti dari klien REST kami akan dibangun di atas komponen berikut ini:
- Model: Kelas yang menggambarkan model data aplikasi kita, yang mencerminkan struktur data yang diterima dari, atau dikirim ke, server backend.
- Parser: Bertanggung jawab untuk mendekode respons server dan memproduksi objek model.
- Errors: Objek untuk mewakili respons server yang salah.
- Klien: Mengirim permintaan ke server backend dan menerima tanggapan.
- Layanan: Kelola operasi yang terhubung secara logis (misalnya otentikasi, mengelola data terkait pengguna, analitik, dll).
Berikut adalah bagaimana masing-masing komponen ini akan berinteraksi satu sama lain:
Panah 1 hingga 10 pada gambar di atas menunjukkan urutan operasi yang ideal antara aplikasi yang meminta layanan dan layanan yang pada akhirnya mengembalikan data yang diminta sebagai objek model. Setiap komponen dalam aliran itu memiliki peran khusus yang memastikan pemisahan perhatian di dalam modul.
Penerapan
Kami akan mengimplementasikan klien REST kami sebagai bagian dari aplikasi jejaring sosial imajiner kami di mana kami akan memuat daftar teman pengguna yang saat ini masuk. Kami akan menganggap server jarak jauh kami menggunakan JSON untuk tanggapan.
Mari kita mulai dengan mengimplementasikan model dan parser kita.
Dari JSON Mentah ke Objek Model
Model pertama kami, User
, mendefinisikan struktur informasi untuk setiap pengguna jejaring sosial. Untuk mempermudah, kami hanya akan menyertakan bidang yang benar-benar diperlukan untuk tutorial ini (dalam aplikasi nyata, struktur biasanya memiliki lebih banyak properti).
struct User { var id: String var email: String? var name: String? }
Karena kami akan menerima semua data pengguna dari server backend melalui API-nya, kami memerlukan cara untuk mengurai respons API menjadi objek User
yang valid. Untuk melakukan ini, kami akan menambahkan konstruktor ke User
yang menerima objek JSON yang diurai ( Dictionary
) sebagai parameter. Kami akan mendefinisikan objek JSON kami sebagai tipe alias:
typealias JSON = [String: Any]
Kami kemudian akan menambahkan fungsi konstruktor ke struct User
kami sebagai berikut:
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 } }
Untuk mempertahankan konstruktor default asli User
, kami menambahkan konstruktor melalui ekstensi pada tipe User
.
Selanjutnya, untuk membuat objek User
dari respons API mentah, kita perlu melakukan dua langkah berikut:
// 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)
Penanganan Kesalahan yang Efisien
Kami akan menentukan jenis untuk mewakili kesalahan berbeda yang mungkin terjadi saat mencoba berinteraksi dengan server backend. Kita dapat membagi semua kesalahan tersebut menjadi tiga kategori dasar:
- Tidak ada konektivitas Internet
- Kesalahan yang dilaporkan sebagai bagian dari tanggapan (misalnya kesalahan validasi, hak akses yang tidak memadai, dll.)
- Kesalahan yang gagal dilaporkan oleh server sebagai bagian dari respons (mis. server mogok, waktu respons habis, dll.)
Kita dapat mendefinisikan objek kesalahan kita sebagai tipe enumerasi. Dan sementara kita melakukannya, ada baiknya untuk membuat tipe ServiceError
kita sesuai dengan protokol Error
. Ini akan memungkinkan kita untuk menggunakan dan menangani nilai kesalahan ini menggunakan mekanisme standar yang disediakan oleh Swift (seperti menggunakan throw
untuk melempar kesalahan).
enum ServiceError: Error { case noInternetConnection case custom(String) case other }
Tidak seperti noInternetConnection
dan kesalahan other
, kesalahan khusus memiliki nilai yang terkait dengannya. Ini akan memungkinkan kami untuk menggunakan respons kesalahan dari server sebagai nilai terkait untuk kesalahan itu sendiri, sehingga memberikan lebih banyak konteks kesalahan.
Sekarang, mari tambahkan properti errorDescription
ke enumartion ServiceError
untuk membuat kesalahan lebih deskriptif. Kami akan menambahkan pesan hardcode untuk noInternetConnection
dan kesalahan other
dan menggunakan nilai terkait sebagai pesan untuk kesalahan 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 } } }
Hanya ada satu hal lagi yang perlu kami terapkan dalam enumerasi ServiceError
kami. Dalam kasus kesalahan custom
, kita perlu mengubah data JSON server menjadi objek kesalahan. Untuk melakukan ini, kami menggunakan pendekatan yang sama yang kami gunakan dalam kasus model:
extension ServiceError { init(json: JSON) { if let message = json["message"] as? String { self = .custom(message) } else { self = .other } } }
Menjembatani Kesenjangan Antara Aplikasi dan Server Backend
Komponen klien akan menjadi perantara antara aplikasi dan server backend. Ini adalah komponen penting yang akan menentukan bagaimana aplikasi dan server akan berkomunikasi, namun tidak akan tahu apa-apa tentang model data dan strukturnya. Klien akan bertanggung jawab untuk memanggil URL tertentu dengan parameter yang disediakan dan mengembalikan data JSON masuk yang diurai sebagai objek 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 } }
Mari kita periksa apa yang terjadi pada kode di atas…

Pertama, kami mendeklarasikan jenis enumerasi, RequestMethod
, yang menjelaskan empat metode HTTP umum. Ini adalah salah satu metode yang digunakan dalam REST API.
Kelas WebClient
berisi properti baseURL
yang akan digunakan untuk menyelesaikan semua URL relatif yang diterimanya. Jika aplikasi kita perlu berinteraksi dengan beberapa server, kita dapat membuat beberapa instance WebClient
masing-masing dengan nilai berbeda untuk baseURL
.
Klien memiliki satu metode load
, yang mengambil jalur relatif ke baseURL
sebagai parameter, metode permintaan, parameter permintaan, dan penutupan penyelesaian. Penutupan penyelesaian dipanggil dengan JSON dan ServiceError
yang diurai sebagai parameter. Untuk saat ini, metode di atas tidak memiliki implementasi, yang akan segera kita bahas.
Sebelum menerapkan metode load
, kita memerlukan cara untuk membuat URL
dari semua informasi yang tersedia untuk metode tersebut. Kami akan memperluas kelas URL
untuk tujuan ini:
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! } }
Di sini kita cukup menambahkan path ke URL dasar. Untuk metode HTTP GET dan DELETE, kami juga menambahkan parameter kueri ke string URL.
Selanjutnya, kita harus dapat membuat instance URLRequest
dari parameter yang diberikan. Untuk melakukan ini, kami akan melakukan sesuatu yang mirip dengan apa yang kami lakukan untuk 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 } } }
Di sini, pertama-tama kita membuat URL
menggunakan konstruktor dari ekstensi. Kemudian kami menginisialisasi instance URLRequest
dengan URL
ini , menetapkan beberapa header HTTP seperlunya, dan kemudian dalam kasus metode HTTP POST atau PUT, tambahkan parameter ke badan permintaan.
Sekarang kita telah membahas semua prasyarat, kita dapat mengimplementasikan metode 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 } }
Metode load
di atas melakukan langkah-langkah berikut:
- Periksa ketersediaan koneksi Internet. Jika konektivitas Internet tidak tersedia, kami segera memanggil penutupan penyelesaian dengan kesalahan
noInternetConnection
sebagai parameter. (Catatan:Reachability
dalam kode adalah kelas khusus, yang menggunakan salah satu pendekatan umum untuk memeriksa koneksi Internet.) - Tambahkan parameter umum. . Ini dapat mencakup parameter umum seperti token aplikasi atau id pengguna.
- Buat objek
URLRequest
, menggunakan konstruktor dari ekstensi. - Kirim permintaan ke server. Kami menggunakan objek
URLSession
untuk mengirim data ke server. - Mengurai data yang masuk. Saat server merespons, pertama-tama kita mengurai payload respons ke objek JSON menggunakan
JSONSerialization
. Kemudian kami memeriksa kode status respons. Jika itu adalah kode sukses (yaitu, dalam kisaran antara 200 dan 299), kami memanggil penutupan penyelesaian dengan objek JSON. Jika tidak, kami mengubah objek JSON menjadi objekServiceError
dan memanggil penutupan penyelesaian dengan objek kesalahan itu.
Mendefinisikan Layanan untuk Operasi yang Terhubung Secara Logis
Dalam kasus aplikasi kami, kami membutuhkan layanan yang akan menangani tugas-tugas yang berhubungan dengan teman-teman pengguna. Untuk ini, kami membuat kelas FriendsService
. Idealnya, kelas seperti ini akan bertanggung jawab atas operasi seperti mendapatkan daftar teman, menambah teman baru, menghapus teman, mengelompokkan beberapa teman ke dalam kategori, dll. Untuk mempermudah dalam tutorial ini, kita akan menerapkan satu metode saja. :
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) } } }
Kelas FriendsService
berisi properti client
bertipe WebClient
. Ini diinisialisasi dengan URL dasar dari server jarak jauh yang bertugas mengelola teman. Seperti yang disebutkan sebelumnya, di kelas layanan lain, kita dapat memiliki instance WebClient
yang berbeda yang diinisialisasi dengan URL yang berbeda jika perlu.
Dalam kasus aplikasi yang bekerja dengan hanya satu server, kelas WebClient
dapat diberikan konstruktor yang menginisialisasi dengan URL server tersebut:
final class WebClient { // ... init() { self.baseUrl = "https://your_server_base_url" } // ... }
Metode loadFriends
, ketika dipanggil, menyiapkan semua parameter yang diperlukan dan menggunakan instance FriendService
dari WebClient
untuk membuat permintaan API. Setelah menerima respons dari server melalui WebClient
, itu mengubah objek JSON menjadi model User
dan memanggil penutupan penyelesaian dengan mereka sebagai parameter.
Penggunaan khas FriendService
mungkin terlihat seperti berikut ini:
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 } } } }
Dalam contoh di atas, kami mengasumsikan bahwa fungsi friendsButtonTapped
dipanggil setiap kali pengguna mengetuk tombol yang dimaksudkan untuk menampilkan daftar teman mereka di jaringan. Kami juga menyimpan referensi tugas di properti friendsTask
sehingga kami dapat membatalkan permintaan kapan saja dengan memanggil friendsTask?.cancel()
.
Hal ini memungkinkan kami untuk memiliki kontrol yang lebih besar terhadap siklus hidup permintaan yang tertunda, memungkinkan kami untuk menghentikannya saat kami menentukan bahwa permintaan tersebut menjadi tidak relevan.
Kesimpulan
Dalam artikel ini, saya telah membagikan arsitektur sederhana dari modul jaringan untuk aplikasi iOS Anda yang sepele untuk diterapkan dan dapat disesuaikan dengan kebutuhan jaringan yang rumit dari sebagian besar aplikasi iOS. Namun, kunci pengambilan dari ini adalah bahwa klien REST yang dirancang dengan benar dan komponen yang menyertainya – yang diisolasi dari logika aplikasi Anda yang lain – dapat membantu menjaga kode interaksi klien-server aplikasi Anda tetap sederhana, bahkan ketika aplikasi itu sendiri menjadi semakin kompleks .
Saya harap Anda menemukan artikel ini membantu dalam membangun aplikasi iOS Anda berikutnya. Anda dapat menemukan kode sumber modul jaringan ini di GitHub. Lihat kodenya, garpu, ubah, mainkan.
Jika Anda menemukan beberapa arsitektur lain yang lebih disukai untuk Anda dan proyek Anda, silakan bagikan detailnya di bagian komentar di bawah.