iOS Uygulamalarında İstemci-Sunucu Etkileşim Mantığını Yalıtma

Yayınlanan: 2022-03-11

Günümüzde çoğu mobil uygulama, büyük ölçüde istemci-sunucu etkileşimlerine dayanmaktadır. Bu, yalnızca ağır görevlerinin çoğunu arka uç sunuculara yükleyebilecekleri anlamına gelmez, aynı zamanda bu mobil uygulamaların yalnızca internet üzerinden sağlanabilecek her türlü özellik ve işlevi sunmasına izin verir.

Arka uç sunucular genellikle hizmetlerini RESTful API'ler aracılığıyla sunmak üzere tasarlanmıştır. Daha basit uygulamalar için, genellikle spagetti kodu oluşturarak almak isteriz; API'yi uygulama mantığının geri kalanıyla çağıran karıştırma kodu. Ancak uygulamalar karmaşıklaştıkça ve giderek daha fazla API ile uğraştıkça, bu API'lerle yapılandırılmamış, plansız bir şekilde etkileşim kurmak sıkıntı yaratabilir.

İyi tasarlanmış bir REST istemci ağ modülü ile iOS uygulama kodunuzu dağınıklıktan uzak tutun.

İyi tasarlanmış bir REST istemci ağ modülü ile iOS uygulama kodunuzu dağınıklıktan uzak tutun.
Cıvıldamak

Bu makalede, tüm istemci-sunucu etkileşim mantığınızı uygulama kodunuzun geri kalanından yalıtılmış halde tutmanıza olanak tanıyan iOS uygulamaları için temiz bir REST istemci ağ iletişimi modülü oluşturmaya yönelik mimari bir yaklaşım anlatılmaktadır.

İstemci-Sunucu Uygulamaları

Tipik bir istemci-sunucu etkileşimi şuna benzer:

  1. Bir kullanıcı bazı eylemler gerçekleştirir (örneğin, bir düğmeye dokunmak veya ekranda başka bir hareket yapmak).
  2. Uygulama, kullanıcı eylemine yanıt olarak bir HTTP/REST isteği hazırlar ve gönderir.
  3. Sunucu, isteği işler ve uygulamaya göre yanıt verir.
  4. Uygulama yanıtı alır ve buna göre kullanıcı arabirimini günceller.

Hızlı bir bakışta, genel süreç basit görünebilir, ancak ayrıntıları düşünmemiz gerekiyor.

Bir arka uç sunucu API'sinin ilan edildiği gibi çalıştığını varsaysak bile (ki bu her zaman böyle değildir !), çoğu zaman yetersiz tasarlanarak verimsiz ve hatta kullanımı zor olabilir. Yaygın bir sıkıntı, API'ye yapılan tüm çağrıların, arayanın aynı bilgileri (örneğin, istek verilerinin nasıl biçimlendirildiği, sunucunun şu anda oturum açmış olan kullanıcıyı tanımlamak için kullanabileceği bir erişim belirteci vb.) sağlamasını gerektirmesidir.

Mobil uygulamaların, farklı amaçlar için aynı anda birden çok arka uç sunucu kullanması gerekebilir. Örneğin bir sunucu, kullanıcı kimlik doğrulaması için ayrılmış olabilirken, bir diğeri yalnızca analitik toplama ile ilgilenebilir.

Ayrıca, tipik bir REST istemcisinin uzak API'leri çağırmaktan çok daha fazlasını yapması gerekecektir. Bekleyen istekleri iptal etme yeteneği veya hataların ele alınmasına yönelik temiz ve yönetilebilir bir yaklaşım, herhangi bir sağlam mobil uygulamaya yerleştirilmesi gereken işlevsellik örnekleridir.

Mimariye Genel Bakış

REST istemcimizin çekirdeği aşağıdaki bileşenler üzerine inşa edilecektir:

  • Modeller: Arka uç sunucularından alınan veya arka uç sunucularına gönderilen verilerin yapısını yansıtan, uygulamamızın veri modellerini açıklayan sınıflar.
  • Ayrıştırıcılar: Sunucu yanıtlarının kodunun çözülmesinden ve model nesnelerinin üretilmesinden sorumludur.
  • Hatalar: Hatalı sunucu yanıtlarını temsil eden nesneler.
  • İstemci: Arka uç sunucularına istek gönderir ve yanıtları alır.
  • Hizmetler: Mantıksal olarak bağlantılı işlemleri yönetin (örn. kimlik doğrulama, kullanıcıyla ilgili verileri yönetme, analitik vb.).

Bu bileşenlerin her biri şu şekilde etkileşime girer:

Yukarıdaki resimde 1'den 10'a kadar olan oklar, bir hizmeti çağıran uygulama ile nihai olarak istenen verileri bir model nesnesi olarak döndüren hizmet arasındaki ideal bir işlem dizisini göstermektedir. Bu akıştaki her bileşenin, modül içindeki endişelerin ayrılmasını sağlayan belirli bir rolü vardır.

uygulama

REST istemcimizi, şu anda oturum açmış olan kullanıcının arkadaşlarının bir listesini yükleyeceğimiz hayali sosyal ağ uygulamamızın bir parçası olarak uygulayacağız. Uzak sunucumuzun yanıtlar için JSON kullandığını varsayacağız.

Modellerimizi ve ayrıştırıcılarımızı uygulayarak başlayalım.

Raw JSON'dan Model Nesnelerine

İlk modelimiz User , sosyal ağın herhangi bir kullanıcısı için bilgi yapısını tanımlar. İşleri basit tutmak için, yalnızca bu eğitim için kesinlikle gerekli olan alanları ekleyeceğiz (gerçek bir uygulamada, yapı genellikle çok daha fazla özelliğe sahip olacaktır).

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

API aracılığıyla arka uç sunucusundan tüm kullanıcı verilerini alacağımızdan, API yanıtını geçerli bir User nesnesine ayrıştırmanın bir yoluna ihtiyacımız var. Bunu yapmak için, ayrıştırılmış bir JSON nesnesini ( Dictionary ) parametre olarak kabul eden User bir kurucu ekleyeceğiz. JSON nesnemizi takma adlı bir tür olarak tanımlayacağız:

 typealias JSON = [String: Any]

Daha sonra yapıcı işlevini User yapımıza aşağıdaki gibi ekleyeceğiz:

 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 öğesinin orijinal varsayılan yapıcısını korumak için, yapıcıyı User türündeki bir uzantı aracılığıyla ekliyoruz.

Ardından, ham API yanıtından bir User nesnesi oluşturmak için aşağıdaki iki adımı gerçekleştirmemiz gerekir:

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

Kolaylaştırılmış Hata İşleme

Arka uç sunucularıyla etkileşime girmeye çalışırken oluşabilecek farklı hataları temsil edecek bir tür tanımlayacağız. Tüm bu tür hataları üç temel kategoriye ayırabiliriz:

  • İnternet bağlantısı yok
  • Yanıtın bir parçası olarak bildirilen hatalar (örn. doğrulama hataları, yetersiz erişim hakları vb.)
  • Sunucunun yanıtın bir parçası olarak rapor edemediği hatalar (örn. sunucu çökmesi, yanıtların zaman aşımına uğraması vb.)

Hata nesnelerimizi bir numaralandırma türü olarak tanımlayabiliriz. ServiceError türümüzü Error protokolüne uygun hale getirmek iyi bir fikirdir. Bu, Swift tarafından sağlanan standart mekanizmaları kullanarak bu hata değerlerini kullanmamıza ve işlememize izin verecektir (bir hatayı throw için atmak gibi).

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

noInternetConnection ve other hatalardan farklı olarak, özel hatanın kendisiyle ilişkili bir değeri vardır. Bu, sunucudan gelen hata yanıtını hatanın kendisi için ilişkili bir değer olarak kullanmamıza ve böylece hataya daha fazla bağlam vermemize olanak tanır.

Şimdi, hataları daha açıklayıcı hale getirmek için ServiceError numaralandırmasına bir errorDescription özelliği ekleyelim. noInternetConnection ve other hatalar için sabit kodlanmış mesajlar ekleyeceğiz ve ilgili değeri custom hatalar için mesaj olarak kullanacağız.

 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 numaralandırmamızda uygulamamız gereken bir şey daha var. custom bir hata olması durumunda, sunucu JSON verilerini bir hata nesnesine dönüştürmemiz gerekir. Bunu yapmak için, modellerde kullandığımız yaklaşımı kullanıyoruz:

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

Uygulama ile Arka Uç Sunucusu Arasındaki Boşluğu Kapatmak

İstemci bileşeni, uygulama ile arka uç sunucusu arasında bir aracı olacaktır. Uygulamanın ve sunucunun nasıl iletişim kuracağını tanımlayacak kritik bir bileşendir, ancak veri modelleri ve yapıları hakkında hiçbir şey bilmeyecek. İstemci, sağlanan parametrelerle belirli URL'leri çağırmaktan ve JSON nesneleri olarak ayrıştırılan gelen JSON verilerini döndürmekten sorumlu olacaktır.

 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 } }

Yukarıdaki kodda neler olduğunu inceleyelim…

İlk olarak, dört yaygın HTTP yöntemini açıklayan RequestMethod bir numaralandırma türü bildirdik. Bunlar REST API'lerinde kullanılan yöntemler arasındadır.

WebClient sınıfı, aldığı tüm göreli URL'leri çözümlemek için kullanılacak baseURL özelliğini içerir. Uygulamamızın birden çok sunucuyla etkileşime girmesi gerekiyorsa, her biri baseURL için farklı bir değere sahip birden çok WebClient örneği oluşturabiliriz.

İstemci, parametre, istek yöntemi, istek parametreleri ve tamamlama kapanışı olarak baseURL göre bir yol alan tek bir yöntem load sahiptir. Tamamlama kapanışı, parametre olarak ayrıştırılmış JSON ve ServiceError ile çağrılır. Şimdilik, yukarıdaki yöntem, birazdan ele alacağımız bir uygulamadan yoksundur.

load yöntemini uygulamadan önce, yöntemde mevcut olan tüm bilgilerden bir URL oluşturmanın bir yoluna ihtiyacımız var. URL sınıfını bu amaçla genişleteceğiz:

 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! } }

Burada sadece yolu temel URL'ye ekliyoruz. GET ve DELETE HTTP yöntemleri için, URL dizesine sorgu parametrelerini de ekliyoruz.

Ardından, verilen parametrelerden URLRequest örnekleri oluşturabilmemiz gerekiyor. Bunu yapmak için URL için yaptığımıza benzer bir şey yapacağız:

 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 } } }

Burada ilk önce uzantıdaki yapıcıyı kullanarak bir URL oluşturuyoruz. Ardından, bu URL ile bir URLRequest örneğini başlatırız, gerektiği gibi birkaç HTTP başlığı belirleriz ve ardından POST veya PUT HTTP yöntemleri durumunda, istek gövdesine parametreler ekleriz.

Artık tüm önkoşulları ele aldığımıza göre, load yöntemini uygulayabiliriz:

 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 } }

Yukarıdaki load yöntemi aşağıdaki adımları gerçekleştirir:

  1. İnternet bağlantısının kullanılabilirliğini kontrol edin. İnternet bağlantısı mevcut değilse, parametre olarak noInternetConnection hatasıyla tamamlama kapatmasını hemen çağırırız. (Not: Reachability , İnternet bağlantısını kontrol etmek için yaygın yaklaşımlardan birini kullanan özel bir sınıftır.)
  2. Ortak parametreler ekleyin. . Bu, bir uygulama belirteci veya kullanıcı kimliği gibi ortak parametreleri içerebilir.
  3. Uzantıdaki yapıcıyı kullanarak URLRequest nesnesini oluşturun .
  4. İsteği sunucuya gönderin. Sunucuya veri göndermek için URLSession nesnesini kullanıyoruz.
  5. Gelen verileri ayrıştırın. Sunucu yanıt verdiğinde, önce yanıt yükünü JSONSerialization kullanarak bir JSON nesnesine ayrıştırırız. Ardından yanıtın durum kodunu kontrol ederiz. Eğer bir başarı kodu ise (yani, 200 ile 299 aralığındaysa), JSON nesnesi ile tamamlama kapanışını çağırırız. Aksi takdirde, JSON nesnesini bir ServiceError nesnesine dönüştürürüz ve o hata nesnesiyle tamamlama kapatmasını çağırırız.

Mantıksal Bağlantılı İşlemler için Hizmetlerin Tanımlanması

Uygulamamız durumunda, bir kullanıcının arkadaşlarıyla ilgili görevlerle ilgilenecek bir hizmete ihtiyacımız var. Bunun için bir FriendsService sınıfı oluşturuyoruz. İdeal olarak, bunun gibi bir sınıf, arkadaş listesi alma, yeni bir arkadaş ekleme, bir arkadaşı silme, bazı arkadaşları bir kategoride gruplandırma vb. işlemlerden sorumlu olacaktır. Bu öğreticide basitlik için sadece bir yöntem uygulayacağız. :

 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 sınıfı, WebClient türünde bir client özelliği içerir. Arkadaşları yönetmekten sorumlu olan uzak sunucunun temel URL'si ile başlatılır. Daha önce belirtildiği gibi, diğer hizmet sınıflarında, gerekirse farklı bir URL ile başlatılmış farklı bir WebClient örneğine sahip olabiliriz.

Yalnızca bir sunucuyla çalışan bir uygulama olması durumunda, WebClient sınıfına o sunucunun URL'si ile başlayan bir kurucu verilebilir:

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

loadFriends yöntemi, çağrıldığında, gerekli tüm parametreleri hazırlar ve bir API isteği yapmak için FriendService WebClient örneğini kullanır. WebClient aracılığıyla sunucudan yanıt aldıktan sonra JSON nesnesini User modellerine dönüştürür ve bunlarla tamamlama kapanışını parametre olarak çağırır.

FriendService tipik bir kullanımı aşağıdaki gibi görünebilir:

 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 } } } }

Yukarıdaki örnekte, kullanıcı, ağdaki arkadaşlarının bir listesini göstermek amacıyla bir düğmeye her dokunduğunda friendsButtonTapped işlevinin çağrıldığını varsayıyoruz. Ayrıca, friendsTask?.cancel() çağırarak isteği herhangi bir zamanda iptal edebilmemiz için, friendsTask özelliğinde göreve bir referans tutarız.

Bu, bekleyen isteklerin yaşam döngüsü üzerinde daha fazla kontrole sahip olmamızı sağlayarak, ilgisiz hale geldiklerini belirlediğimizde bunları sonlandırabilmemizi sağlar.

Çözüm

Bu makalede, iOS uygulamanız için hem uygulanması önemsiz hem de çoğu iOS uygulamasının karmaşık ağ gereksinimlerine uyarlanabilen basit bir ağ modülü mimarisini paylaştım. Bununla birlikte, bundan önemli çıkarım, uygun şekilde tasarlanmış bir REST istemcisi ve ona eşlik eden – uygulama mantığınızın geri kalanından izole edilmiş – bileşenlerin, uygulamanın kendisi giderek daha karmaşık hale gelse bile, uygulamanızın istemci-sunucu etkileşim kodunu basit tutmaya yardımcı olabileceğidir. .

Umarım bu makaleyi bir sonraki iOS uygulamanızı oluştururken faydalı bulursunuz. Bu ağ modülünün kaynak kodunu GitHub'da bulabilirsiniz. Kodu kontrol et, çatalla, değiştir, onunla oyna.

Siz ve projeniz için başka bir mimariyi daha çok tercih ederseniz, lütfen aşağıdaki yorumlar bölümünde ayrıntıları paylaşın.

İlgili: Mantle ve Realm ile iOS'ta RESTful API Kullanımını ve Veri Kalıcılığını Basitleştirme