.key, value: String(describing:

วิธีแยกลอจิกการโต้ตอบระหว่างไคลเอนต์ - เซิร์ฟเวอร์ในแอปพลิเคชัน iOS

ปัจจุบันแอปพลิเคชันมือถือส่วนใหญ่ต้องพึ่งพาการโต้ตอบระหว่างไคลเอนต์กับเซิร์ฟเวอร์เป็นอย่างมาก ไม่เพียงหมายความว่าพวกเขาสามารถถ่ายโอนงานหนักส่วนใหญ่ไปยังเซิร์ฟเวอร์ส่วนหลังได้ แต่ยังช่วยให้แอปพลิเคชันมือถือเหล่านี้มีคุณสมบัติและฟังก์ชันการทำงานทุกประเภทที่สามารถใช้งานได้ผ่านทางอินเทอร์เน็ตเท่านั้น

เซิร์ฟเวอร์ส่วนหลังมักได้รับการออกแบบมาเพื่อให้บริการผ่าน RESTful API . สำหรับการใช้งานที่ง่ายขึ้นเรามักจะรู้สึกอยากได้โดยการสร้างรหัสสปาเก็ตตี้ ผสมรหัสที่เรียกใช้ API ด้วยตรรกะของแอปพลิเคชันที่เหลือ อย่างไรก็ตามเมื่อแอปพลิเคชันมีความซับซ้อนและจัดการกับ API มากขึ้นเรื่อย ๆ การโต้ตอบกับ API เหล่านี้ในลักษณะที่ไม่มีโครงสร้างและไม่ได้วางแผนไว้อาจเป็นเรื่องที่น่ารำคาญ

รักษารหัสแอปพลิเคชัน iOS ของคุณให้ไม่เกะกะด้วยโมดูลเครือข่ายไคลเอ็นต์ REST ที่ออกแบบมาอย่างดี



รักษารหัสแอปพลิเคชัน iOS ของคุณให้ไม่เกะกะด้วยโมดูลเครือข่ายไคลเอ็นต์ REST ที่ออกแบบมาอย่างดี ทวีต

บทความนี้กล่าวถึงวิธีการทางสถาปัตยกรรมสำหรับการสร้างโมดูลเครือข่ายไคลเอ็นต์ REST ที่สะอาดสำหรับ แอปพลิเคชัน iOS ที่ช่วยให้คุณสามารถแยกตรรกะการโต้ตอบไคลเอนต์เซิร์ฟเวอร์ทั้งหมดออกจากโค้ดแอปพลิเคชันที่เหลือของคุณ

แอปพลิเคชันไคลเอนต์เซิร์ฟเวอร์

การโต้ตอบกับไคลเอ็นต์เซิร์ฟเวอร์โดยทั่วไปมีลักษณะดังนี้:

  1. ผู้ใช้ดำเนินการบางอย่าง (เช่นแตะที่ปุ่มบางปุ่มหรือแสดงท่าทางอื่น ๆ บนหน้าจอ)
  2. แอปพลิเคชันเตรียมและส่งคำร้องขอ HTTP / REST เพื่อตอบสนองการดำเนินการของผู้ใช้
  3. เซิร์ฟเวอร์ประมวลผลคำขอและตอบสนองตามแอปพลิเคชัน
  4. แอปพลิเคชันได้รับการตอบสนองและอัปเดตอินเทอร์เฟซผู้ใช้ตาม

เมื่อมองอย่างรวดเร็วกระบวนการโดยรวมอาจดูเรียบง่าย แต่เราต้องคิดถึงรายละเอียด

แม้ว่าจะสมมติว่า API ของเซิร์ฟเวอร์แบ็กเอนด์ทำงานตามที่โฆษณาไว้ (ซึ่งก็คือ ไม่ มักจะเป็นกรณี!) มักจะได้รับการออกแบบมาไม่ดีทำให้ไม่มีประสิทธิภาพหรือใช้งานยาก สิ่งที่น่ารำคาญอย่างหนึ่งคือการเรียก API ทั้งหมดต้องการให้ผู้โทรให้ข้อมูลซ้ำ ๆ กัน (เช่นวิธีการจัดรูปแบบข้อมูลคำขอโทเค็นการเข้าถึงที่เซิร์ฟเวอร์สามารถใช้เพื่อระบุผู้ใช้ที่ลงชื่อเข้าใช้ในปัจจุบันเป็นต้น)

แอปพลิเคชันมือถืออาจจำเป็นต้องใช้เซิร์ฟเวอร์ส่วนหลังหลายเครื่องพร้อมกันเพื่อวัตถุประสงค์ที่แตกต่างกัน ตัวอย่างเช่นเซิร์ฟเวอร์หนึ่งอาจทุ่มเทให้กับการพิสูจน์ตัวตนผู้ใช้ในขณะที่อีกเซิร์ฟเวอร์หนึ่งเกี่ยวข้องกับการรวบรวมการวิเคราะห์เท่านั้น

นอกจากนี้ไคลเอนต์ REST ทั่วไปจะต้องทำมากกว่าแค่เรียกใช้ API ระยะไกล ความสามารถในการยกเลิกคำขอที่รอดำเนินการหรือวิธีการจัดการข้อผิดพลาดที่สะอาดและสามารถจัดการได้เป็นตัวอย่างของฟังก์ชันการทำงานที่จำเป็นต้องมีอยู่ในแอปพลิเคชันมือถือที่มีประสิทธิภาพ

ภาพรวมของสถาปัตยกรรม

แกนหลักของไคลเอนต์ REST ของเราจะสร้างขึ้นจากส่วนประกอบต่อไปนี้:

นี่คือวิธีที่แต่ละองค์ประกอบเหล่านี้จะโต้ตอบซึ่งกันและกัน:

ลูกศร 1 ถึง 10 ในภาพด้านบนแสดงลำดับการดำเนินการที่เหมาะสมที่สุดระหว่างแอปพลิเคชันที่เรียกใช้บริการและบริการส่งคืนข้อมูลที่ร้องขอในรูปแบบวัตถุในที่สุด แต่ละองค์ประกอบในโฟลว์นั้นมีบทบาทเฉพาะที่ทำให้มั่นใจได้ แยกความกังวล ภายในโมดูล

การนำไปใช้

เราจะใช้ไคลเอ็นต์ REST ของเราเป็นส่วนหนึ่งของแอปพลิเคชันเครือข่ายสังคมในจินตนาการของเราซึ่งเราจะโหลดรายชื่อเพื่อนของผู้ใช้ที่ล็อกอินอยู่ เราจะถือว่าเซิร์ฟเวอร์ระยะไกลของเราใช้ JSON สำหรับการตอบสนอง

ให้เราเริ่มต้นด้วยการใช้โมเดลและตัวแยกวิเคราะห์ของเรา

จาก Raw JSON ไปจนถึง Model Objects

แบบจำลองแรกของเรา 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 ดิบเราต้องดำเนินการสองขั้นตอนต่อไปนี้:

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

การจัดการข้อผิดพลาดที่คล่องตัว

เราจะกำหนดประเภทเพื่อแสดงข้อผิดพลาดต่างๆที่อาจเกิดขึ้นเมื่อพยายามโต้ตอบกับเซิร์ฟเวอร์แบ็กเอนด์ เราสามารถแบ่งข้อผิดพลาดทั้งหมดออกเป็นสามประเภทพื้นฐาน:

เราสามารถกำหนดออบเจ็กต์ข้อผิดพลาดของเราเป็นประเภทการแจงนับ และในขณะที่เราอยู่ที่นั่นเป็นความคิดที่ดีที่จะทำให้ ServiceError ของเรา ประเภทสอดคล้องกับ Error มาตรการ . สิ่งนี้จะช่วยให้เราสามารถใช้และจัดการกับค่าความผิดพลาดเหล่านี้โดยใช้กลไกมาตรฐานที่ Swift มีให้ (เช่นใช้ throw เพื่อโยนข้อผิดพลาด)

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

ไม่เหมือน noInternetConnection และ other ข้อผิดพลาดข้อผิดพลาดที่กำหนดเองมีค่าที่เกี่ยวข้อง สิ่งนี้จะช่วยให้เราใช้การตอบสนองข้อผิดพลาดจากเซิร์ฟเวอร์เป็นค่าที่เกี่ยวข้องสำหรับข้อผิดพลาดนั้นเองจึงทำให้ข้อผิดพลาดมีบริบทมากขึ้น

ตอนนี้เรามาเพิ่ม errorDescription คุณสมบัติของ ServiceError enumartion เพื่อให้ข้อผิดพลาดมีความหมายมากขึ้น เราจะเพิ่มข้อความที่เข้ารหัสสำหรับ 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 ของเรา การแจงนับ ในกรณีของ 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 } }

ลองตรวจสอบสิ่งที่เกิดขึ้นในโค้ดด้านบน ...

อันดับแรกเราได้ประกาศประเภทการแจงนับ RequestMethod ซึ่งอธิบายวิธี HTTP ทั่วไปสี่วิธี นี่เป็นหนึ่งในวิธีการที่ใช้ใน REST API

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 และ DELETE HTTP เรายังเพิ่มพารามิเตอร์การสืบค้นลงในสตริง 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 ในโค้ดคือคลาสที่กำหนดเองซึ่งใช้ หนึ่งในแนวทางทั่วไป เพื่อตรวจสอบการเชื่อมต่ออินเทอร์เน็ต)
  2. เพิ่มพารามิเตอร์ทั่วไป . ซึ่งอาจรวมถึงพารามิเตอร์ทั่วไปเช่นโทเค็นของแอปพลิเคชันหรือรหัสผู้ใช้
  3. สร้าง URLRequest วัตถุ, โดยใช้ตัวสร้างจากส่วนขยาย
  4. ส่งคำขอไปยังเซิร์ฟเวอร์ เราใช้ URLSession วัตถุในการส่งข้อมูลไปยังเซิร์ฟเวอร์
  5. แยกวิเคราะห์ข้อมูลที่เข้ามา เมื่อเซิร์ฟเวอร์ตอบสนองอันดับแรกเราจะแยกวิเคราะห์เพย์โหลดการตอบสนองเป็นออบเจ็กต์ JSON โดยใช้ JSONSerialization จากนั้นเราตรวจสอบรหัสสถานะของการตอบกลับ หากเป็นรหัสความสำเร็จ (เช่นอยู่ในช่วงระหว่าง 200 ถึง 299) เราจะเรียกการปิดท้ายด้วยออบเจ็กต์ JSON มิฉะนั้นเราจะแปลงวัตถุ JSON ให้เป็น ServiceError วัตถุและเรียกการปิดสมบูรณ์ด้วยวัตถุข้อผิดพลาดนั้น

การกำหนดบริการสำหรับการดำเนินการที่เชื่อมโยงเชิงตรรกะ

ในกรณีของแอปพลิเคชันของเราเราต้องการบริการที่จะจัดการกับงานที่เกี่ยวข้องกับเพื่อนของผู้ใช้ สำหรับสิ่งนี้เราสร้าง FriendsService ชั้นเรียน ตามหลักการแล้วชั้นเรียนเช่นนี้จะรับผิดชอบการดำเนินการเช่นการรับรายชื่อเพื่อนการเพิ่มเพื่อนใหม่การลบเพื่อนการจัดกลุ่มเพื่อนเป็นหมวดหมู่เป็นต้นเพื่อความง่ายในบทช่วยสอนนี้เราจะใช้เพียงวิธีเดียว :

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 อื่นหากจำเป็น

ในกรณีของแอปพลิเคชันที่ทำงานกับเซิร์ฟเวอร์เพียงเครื่องเดียวให้ใช้ WebClient คลาสสามารถกำหนดคอนสตรัคเตอร์ที่เริ่มต้นด้วย URL ของเซิร์ฟเวอร์นั้น:

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

loadFriends เมื่อเรียกใช้จะเตรียมพารามิเตอร์ที่จำเป็นทั้งหมดและใช้ FriendService อินสแตนซ์ของ WebClient เพื่อทำการร้องขอ 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 . ตรวจสอบรหัสแยกมันเปลี่ยนเล่นกับมัน

หากคุณพบว่าสถาปัตยกรรมอื่น ๆ เหมาะสำหรับคุณและโครงการของคุณมากกว่าโปรดแบ่งปันรายละเอียดในส่วนความคิดเห็นด้านล่าง

ที่เกี่ยวข้อง: ลดความซับซ้อนในการใช้ RESTful API และความคงอยู่ของข้อมูลบน iOS ด้วย Mantle และ Realm .value)) } default: break } self = components.url! } }

ที่นี่เราเพิ่มเส้นทางไปยัง URL พื้นฐาน สำหรับเมธอด GET และ DELETE HTTP เรายังเพิ่มพารามิเตอร์การสืบค้นลงในสตริง 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 ในโค้ดคือคลาสที่กำหนดเองซึ่งใช้ หนึ่งในแนวทางทั่วไป เพื่อตรวจสอบการเชื่อมต่ออินเทอร์เน็ต)
  2. เพิ่มพารามิเตอร์ทั่วไป . ซึ่งอาจรวมถึงพารามิเตอร์ทั่วไปเช่นโทเค็นของแอปพลิเคชันหรือรหัสผู้ใช้
  3. สร้าง URLRequest วัตถุ, โดยใช้ตัวสร้างจากส่วนขยาย
  4. ส่งคำขอไปยังเซิร์ฟเวอร์ เราใช้ URLSession วัตถุในการส่งข้อมูลไปยังเซิร์ฟเวอร์
  5. แยกวิเคราะห์ข้อมูลที่เข้ามา เมื่อเซิร์ฟเวอร์ตอบสนองอันดับแรกเราจะแยกวิเคราะห์เพย์โหลดการตอบสนองเป็นออบเจ็กต์ JSON โดยใช้ JSONSerialization จากนั้นเราตรวจสอบรหัสสถานะของการตอบกลับ หากเป็นรหัสความสำเร็จ (เช่นอยู่ในช่วงระหว่าง 200 ถึง 299) เราจะเรียกการปิดท้ายด้วยออบเจ็กต์ JSON มิฉะนั้นเราจะแปลงวัตถุ JSON ให้เป็น ServiceError วัตถุและเรียกการปิดสมบูรณ์ด้วยวัตถุข้อผิดพลาดนั้น

การกำหนดบริการสำหรับการดำเนินการที่เชื่อมโยงเชิงตรรกะ

ในกรณีของแอปพลิเคชันของเราเราต้องการบริการที่จะจัดการกับงานที่เกี่ยวข้องกับเพื่อนของผู้ใช้ สำหรับสิ่งนี้เราสร้าง FriendsService ชั้นเรียน ตามหลักการแล้วชั้นเรียนเช่นนี้จะรับผิดชอบการดำเนินการเช่นการรับรายชื่อเพื่อนการเพิ่มเพื่อนใหม่การลบเพื่อนการจัดกลุ่มเพื่อนเป็นหมวดหมู่เป็นต้นเพื่อความง่ายในบทช่วยสอนนี้เราจะใช้เพียงวิธีเดียว :

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 อื่นหากจำเป็น

ในกรณีของแอปพลิเคชันที่ทำงานกับเซิร์ฟเวอร์เพียงเครื่องเดียวให้ใช้ WebClient คลาสสามารถกำหนดคอนสตรัคเตอร์ที่เริ่มต้นด้วย URL ของเซิร์ฟเวอร์นั้น:

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

loadFriends เมื่อเรียกใช้จะเตรียมพารามิเตอร์ที่จำเป็นทั้งหมดและใช้ FriendService อินสแตนซ์ของ WebClient เพื่อทำการร้องขอ API หลังจากได้รับการตอบสนองจากเซิร์ฟเวอร์ผ่านทาง WebClient มันจะแปลงวัตถุ JSON เป็น User แบบจำลองและเรียกการปิดเสร็จสมบูรณ์โดยใช้เป็นพารามิเตอร์

การใช้งานทั่วไปของ FriendService อาจมีลักษณะดังนี้:

หน้าวัตถุรุ่นซีลีเนียม java
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 . ตรวจสอบรหัสแยกมันเปลี่ยนเล่นกับมัน

หากคุณพบว่าสถาปัตยกรรมอื่น ๆ เหมาะสำหรับคุณและโครงการของคุณมากกว่าโปรดแบ่งปันรายละเอียดในส่วนความคิดเห็นด้านล่าง

ที่เกี่ยวข้อง: ลดความซับซ้อนในการใช้ RESTful API และความคงอยู่ของข้อมูลบน iOS ด้วย Mantle และ Realm