Contents
Chào bạn đến với series Lập trình iOS cho mọi người. Bài viết này sẽ phần kế tiếp của bài Connect Networking. Nó vẫn liên quan tới việc tương tác với Server. Tuy nhiên, chúng ta sẽ tìm cách đưa nó vào mô hình MVVM trong iOS Project.
Trước tiên thì cần chuẩn bị một số kiến thức cơ bản để vào bài:
Nếu mọi thứ đã okay thì …
Bắt đầu thôi!
Chuẩn bị
- MacOS 10.14.4
- Xcode 11.0
- Swift 5.1
1. Tư tưởng
1.1. CoreAPI
Core API
Cái tên nghe qua thì thấy khá hư cấu. Nhưng ngẫm lại thì có chút thuyết phục. Vì hầu hết việc tương tác Networking trong iOS Project bây giờ thì phải thông qua các API do các Server cung cấp. Dường như nó trở thành chuẩn chung của thế giới rồi.
Nên mình xin đặt tên nó là CoreAPI
. Vậy tư tưởng tạo ra nó nhằm mục đích gì?
Với như cách tách model của bài trước, nếu sử dụng đại trà trong mô hình MVVM thì có nhiều vấn đề phát sinh thêm:
- Nhiều đoạn code xử lý connect bị lặp
- Số lượng function cho connect tạo ra nhiều
- Không có ý nghĩa về mặt sử dụng chung và kế thừa
- Thiếu đi các phần quản lí lỗi
- Bộ nhớ dễ bị tràn hay không giải phóng được
- Khó khăn trong việc thay đổi ip/domain của server
- Các thành phần liên kết quá chặt kĩ
- ….
Vâng vâng có rất nhiều thứ quay đây. Và cái mong muốn của bạn chính là:
Tái sử dụng được cho các project khác.
Vì vậy, bài này sẽ cho ra một mô hình phục vụ cho việc tương tác với Server. Mình gọi tên là CoreAPI
.
Lưu ý: mô hình này đã được sử dụng trong rất nhiều dự án của mình. Và nó mang tính chất tham khảo.
1.2. Ý nghĩa của Model
Rất rất nhiều bạn khi gặp câu hỏi “Model là gì?” thì câu trả lời rất rất là nhanh: “Chính là Database“. Tới đây, mình không dám ý kiến nữa. Nhưng bạn cần phải biết rằng, model chính là bản thân ứng dụng của bạn, trừ đi các phần hiển thị. Đại loại chúng có rất nhiều loại, tuỳ thuộc vào từng chức năng mà nó đảm đương. (theo dõi bài MVC để hiểu hơn về Model)
Quan tâm chính của chúng ta là mặt ý nghĩa của Model. Đó là sự hoạt động độc lập. Gọi là các Service trong project.
Vì vậy, với việc tương tác Networking thì ta có Networking Service. Trong service này hoặc bất cứ service khác thì luôn có 2 thành phần chính.
- Logic Model
- Đây là trái tim của cả Service.
- Kỹ thuật mà bạn sử dụng (chính chủ Apple hay thư viện bên ngoài …)
- Hoạt động theo logic
- Mang tính chất bất biến trong ứng dụng.
- Business Model
- Đây chính là tạo nên sự khác biệt giữa các project với nhau.
- Không có kỹ thuật nào sử dụng. Mà là chính là yêu cầu của khách hàng đối với project
- Hoạt động theo yêu cầu, mong muốn của khách hàng …
- Mang tính chất thay đổi, thường sẽ bị thay đổi rất nhiều và thường xuyên.
Trước tiên cho CoreAPI, thì chúng ta lại sử dụng bộ mã nguồn của bài Connect Networking. Và bắt đầu bằng việc định nghĩa các thành phần có trong bộ core này.
2. Định nghĩa
Chúng ta tạo 1 file với tên là API.swift
. Các define
sẽ được code tại file này.
2.1. Error
Thêm đoạn code sau vào file API.swift
//MARK: - Defines enum APIError: Error { case error(String) case errorURL var localizedDescription: String { switch self { case .error(let string): return string case .errorURL: return "URL String is error." } } }
Trong bài trước, mình cũng giải thích sơ về nó, mang ý nghĩa là gì rồi.
Nó tập trung việc quản lý các error.
Khi có 1 lỗi nhận được từ Server, thì sẽ có 1 mã error_code
kèm theo. Tất nhiên, có rất nhiều mã lỗi cho rất nhiều trường hợp khác nhau. Cộng thêm với các lỗi phát sinh từ logic code của bạn trong project …
Sẽ là rất áp lực khi bạn muốn xử lý riêng 1 loại mã lỗi với 1 xử lý cách khác. Ví dụ:
- Có
error
thì sẽ hiển thịAlert
nhằm thông báo cho người dùng biết. - Nhưng trong các
error
, thì có 1 lỗi liên quan tớipermission denied
. Với lỗi này thì bạn không hiển thị gì cả. Và cố gắnglogout
user ra khỏi hệ thống và yêu cầu đăng nhập trở lại.
Vâng, nó sẽ rất mệt mỏi khi bạn phải so sánh từng trường hợp với nhau. Và với đoạn code trên thì mang ý nghĩa:
- Tạo ra 1
enum
, kế thừa lại ProtocolError
- Với kiểu dữ liệu là enum, thì tăng tính tường minh cho code. Bạn sẽ không cần phải ghi nhớ
error_code
nào cho error nào. - Các
case
:- error = String : là cơ bản nhất. Tương tự như các Error truyền thống khác
- error = url : mô tả 1 trường hợp riêng, khi url của bạn không đúng.
- …
- Bạn có thể tự định nghĩa thêm các
case
của riêng bạn. localizedDescription
mô tả lỗi như thế nào. Trong này bạn sử dụngswitch case
, nên đảm bảo không sót trường hợp nào hết.
Đi vào cách dùng nó một chút
let error = APIError.errorURL let error = APIError.error("Server not found") print(error.localizedDescription)
2.2. Completion
Tiếp tục với file API.swift
. Và tới phần định nghĩa cho các call back
typealias APICompletion<T> = (Result<T, APIError>) -> Void
Bạn đã biết nhiều về call back
trong bài trước. Trong bài bày, chúng ta sẽ phải định nghĩa chúng sao cho nó dùng được cho tất cả trường hợp.
Giải thích:
typealias
là từ khoá để định danh cho một kiểu dữ liệu, với tên của bạn tự đặt.<T>
thì bạn có thể cho T bằng bất cứ kiểu dữ liệu nào cũng được. Gọi là mộtgeneric
- Phần được gán cho
APICompletion
là một closure- Có 1 tham số duy nhất
- Không có giá trị trả về
- Về tham số duy nhất đó. Thì nó là 1 kiểu
Result
của hệ thống cung cấp- Nó là 1
enum
- Có 2 case là
success
vàfailure
- Bạn cần cung cấp dữ liệu cho 2 trường hợp đó. Trong code trên thì là
- success =
T
- failure =
APIError
- success =
- Nó là 1
Phần này bạn cần phải nhớ kĩ. Rất dễ sai ở đây.
Bạn muốn thay đổi dữ liệu trả về cho từng View/ViewController/ViewMode … khi gọi tương tác API thì chỉ cần thay vào chỗ <T>
là xong.
2.3. Result
Tiếp tục vẫn là ở file API.swift
. Tới phần định nghĩa cho kiểu dữ liệu dùng trong việc tương tác giữa 2 thành phần trong CoreAPI.
enum APIResult { case success(Data?) case failure(APIError) }
Tương tự như trên, nhưng cái này cụ thể hơn và do người lập trình tự khai báo.
2.4. Connect
Chính là trái tim của cả hệ thống này. Và nó sẽ quyết định dùng kỹ thuật nào sử dụng cho project. Cũng tại file API.swift
, tạo thêm 1 struct
như sau:
struct API { //singleton private static var shareAPI: API = { let shareAPI = API() return shareAPI }() static func shared() -> API { return shareAPI } //init private init() {} }
Struct này có 1 singleton và private init
. Để đảm bảo luôn có 1 đối tượng sử dụng cho toàn project. Nó sẽ phát huy hiệu quả khi bạn:
- Request API khá nhiều và cùng lúc
- Có thể tạo hàng đợi để giải quyết từng request
- Có thể cancel tất cả các request chưa được chạy
- …
2.5. Request
Phần này là phần extension
của API trên. Chúng sẽ định nghĩa các cách mà bạn sẽ request tới Server. Tạo 1 file mới tên là API.Request.swift
và tham khảo định nghĩa một số function sau:
extension API { //with url string func request(urlString: String, completion: @escaping (APIResult) -> Void) { } //with url func request(url: URL, completion: @escaping (APIResult) -> Void) { } //with request func request(request: URLRequest, completion: @escaping (APIResult) -> Void) { } }
Với bài trước thì bạn cũng biết, chúng ta có thể tương tác với Server thông qua đối tượng URLSession
. Và bạn có thể cung cấp cho nó:
- Một
string
là giá trị text của 1 url - Một đối tượng
URL
- Một đối tượng
Request
Nếu bạn không muốn dùng URLSession
, thay vào đó bạn sử dụng một thư viện khác, như alamofire chẵng hạn. Thì bạn có thể thay đổi nội dung bên trong các function trên. Còn lại về định nghĩa thì không cần thay đổi gì hết.
Tham khảo thêm đoạn code sau:
import Foundation extension API { //with url string func request(urlString: String, completion: @escaping (APIResult) -> Void) { guard let url = URL(string: urlString) else { return } let config = URLSessionConfiguration.ephemeral config.waitsForConnectivity = true let session = URLSession.shared let dataTask = session.dataTask(with: url) { (data, _, error) in DispatchQueue.main.async { if let error = error { completion(.failure(.error(error.localizedDescription))) } else { if let data = data { completion(.success(data)) } } } } dataTask.resume() } //with url func request(url: URL, completion: @escaping (APIResult) -> Void) { let config = URLSessionConfiguration.ephemeral config.waitsForConnectivity = true let session = URLSession.shared let dataTask = session.dataTask(with: url) { (data, _, error) in DispatchQueue.main.async { if let error = error { completion(.failure(.error(error.localizedDescription))) } else { if let data = data { completion(.success(data)) } } } } dataTask.resume() } //with request func request(request: URLRequest, completion: @escaping (APIResult) -> Void) { let config = URLSessionConfiguration.ephemeral config.waitsForConnectivity = true let session = URLSession.shared let dataTask = session.dataTask(with: request) { (data, _, error) in DispatchQueue.main.async { if let error = error { completion(.failure(.error(error.localizedDescription))) } else { if let data = data { completion(.success(data)) } } } } dataTask.resume() } }
Chú ý về
@escaping
, trong tương tác bất đồng bộ. VàDispatchQueue.main
để quay vềMain Thread
.
3. Thành phần của CoreAPI
3.1. Core connection
Đó chính là các phần bạn vừa tạo.
- Struct API
- function request
- Define Error, Completion, Result
3.2. Manager
Đây là phần quản lý việc kết nối giữa CoreAPI với các thành phần khác trong ứng dụng. Tiếp tục, bạn tạo thêm 1 file tên là API.Manager.swift
import Foundation struct APIManager { //MARK: Config struct Path { } //MARK: - Domain struct Music {} struct Downloader {} }
Đây là 1 struct
, trong đó có chứa thêm các struct khác.
- Config : chưa các cấu hình cho server.
Ví dụ:
Ta có link sử dụng như sau: https://rss.itunes.apple.com/api/v1/us/itunes-music/hot-tracks/all/10/explicit.json
, thì bạn có thể tách các thành phần chung nhất như:
struct Path { static let base_domain = "https://rss.itunes.apple.com" static let base_path = "/api/v1/us" static let music_path = "/itunes-music" static let music_hot = "/hot-tracks" }
- Domain model : để giải thích về nó thì mình không chuyên sâu cho lắm. Nhưng nó sẽ là ánh xạ từ Server.
Ví dụ: Hệ thông của bạn, ở Server quản lý các Domain như User, Post, Friend, Commen … thì phần CoreAPI của bạn cũng sẽ có những struct tương tự như vậy. Trong bài thì chúng ta có 2 domain là Music
và Downloader
.
3.3. Domain API
Về Domain model thì nó sẽ có rất nhiều API liên quan tới 1 domain. Đó chính là nghiệp vụ của hệ thống của bạn. Hay chính là phần Business Model
trong project.
Ví dụ: Bạn có 1 domain là Music
. Nhưng bạn sẽ có rất nhiều api liên quan tới Music. Như:
- Lấy danh sách các bài hát trending
- Xem thông tin một bài hát
- Xoá một bài hát
- Thêm một bài hát
- …
Để có cái nhìn thiện cảm hơn, ta sẽ mô tả nó bằng code swift. Tạo thêm mới 1 file và đặt tên là API.Music.swift
. Thêm đoạn code sau vào:
import Foundation extension APIManager.Music { struct QueryString { func hotMusic(limit: Int) -> String { return APIManager.Path.base_domain + APIManager.Path.base_path + APIManager.Path.music_path + APIManager.Path.music_hot + "/all/\(limit)/explicit.json" } } struct QueryParam { } struct MusicResult { var musics: [Music] var copyright: String var updated: String } static func getHotMusic(limit: Int = 10, completion: @escaping APICompletion<MusicResult>) { let urlString = QueryString().hotMusic(limit: limit) } }
Trong đó:
QueryString
: là struct để tạo các url String với đầy đủ tham số cần thiết choGET
QueryParam
: là struct để tạo các dictionary cho body của các requestPOST
(trong bài chưa cần)MusicResult
: là struct định nghĩa các dữ liệu cần lấy từ response từ Server.- Một static function là
getHotMusic
, để thực hiện việc request tới link api lấy danh sách các bài hát đang hot.
Hoàn thiện phần connect trong function trên
static func getHotMusic(limit: Int = 10, completion: @escaping APICompletion<MusicResult>) { let urlString = QueryString().hotMusic(limit: limit) API.shared().request(urlString: urlString) { (result) in switch result { case .failure(let error): //call back completion(.failure(error)) case .success(let data): if let data = data { //parse data // code sau //call back //code sau } else { //call back completion(.failure(.error("Data is not format."))) } } } }
Để xử lí completion
từ API trả về, thì sử dụng switch case
. Vì kiểu dữ liệu của nó là một enum
. Bạn tham khảo cách dùng ở trên cho 2 trường hợp
- failure
- success
Và từ đó, chúng ta lại dùng completion
của APIManager để call back
về đối tượng gọi nó thực thi việc request.
4. Request Handler
View/ViewController vs. ViewModel vs. CoreAPI
Đó chính là các tương tác giữa 3 thế lực hùng mạnh trong mô hình MVVM:
- View/ViewController vs. ViewModel
@objc func loadAPI() { print("LOAD API") viewmodel.loadAPI { (done, msg) in if done { self.updateUI() } else { print("API ERROR: \(msg)") } } }
Vẫn chính là cách tương tác như ở bài trước, trong file HomeViewController
. Không khó khăn hay thay đổi gì nhiều.
- ViewModel vs. CoreAPI
func loadAPI(completion: @escaping Completion) { APIManager.Music.getHotMusic { (result) in switch result { case .failure(let error): //call back completion(false, error.localizedDescription) case .success(let musicResult): self.musics.append(contentsOf: musicResult.musics) //call back completion(true, "") } } }
Vẫn là function của bài trước, trong file HomeViewModel
. Nhưng sử dụng lại bộ CoreAPI vừa mới tạo. Qua ví dụ đoạn code trên thì:
- Đơn giản, gọn nhẹ hơn so với cách cũ
- Ẩn đi được các
link
api trong code - Dễ hiểu và dễ sử dụng hơn, khi bạn không cần phải quan tâm gì nhiều tới việc
parse data
tại ViewModel.
Bạn cần chú ý việc sử dụng
result
vàmusicResult
cho chính xác.
5. Parse data
Tới đây, nhiệm vụ của bạn là sẽ phân tích từng giá trị trong JSON nhận được. Tới phần code của function getHotMusic
của Music
. Tiến hành phần tích và chuyển đổi dữ liệu. Thêm đoạn code sau vào phần xử lý success
.
//parse data let json = data.toJSON() let feed = json["feed"] as! JSON let results = feed["results"] as! [JSON] // musics var musics: [Music] = [] for item in results { let music = Music(json: item) musics.append(music) } //informations let copyright = "....." let updated = "....." // result let musicResult = MusicResult(musics: musics, copyright: copyright, updated: updated) //call back completion(.success(musicResult))
Trong đó:
- Array
musics
chính là dữ liệu, mà mình cần quan tâm nhiều nhất - Các thông tin khác như:
copyright
vàupdated
… là các thông tin phụ cần lấy thêm. Tuỳ thuộc vào yêu cầu của dự án, mà bạn sẽ tự định nghĩa và lấy theo ý bạn. musicResult
là đối tượng, mà bạn sẽ cần phải gởi đi trong trường hợpsuccess
.
Tới đây thì bạn cần kiểm tra hết các function và các lời gọi hàm. Sau đó build project và cảm nhận kết quả.
OKAY, bạn đã hoàn thành việc tạo bộ CoreAPI cho project của bạn. Mã nguồn thì bạn có thể checkout tại đây. Để xử lý các API khác, thì bạn có thể tạo ra tương tự ví dụ Music
trên… Cuối cùng, chúc bạn thành công!
Tạm kết
- Tìm hiểu và phân tích các thành phần của một Service trong project
- Xây dựng bộ CoreAPI phục vụ cho việc tương tác với Server
- Tách biệt ra các phần về Logic và Business trong project
- Việc thao tác với phần connect được tách riêng
- Parse data cũng được tách riêng
- Đơn giản hoá việc sử dụng ở ViewModel
- Không ảnh hưởng gì tới các thành phần khác trong project.
Cảm ơn bạn đã đọc bài viết này!
Related Posts:
Written by chuotfx
Hãy ngồi xuống, uống miếng bánh và ăn miếng trà. Chúng ta cùng nhau đàm đạo về đời, về code nhóe!
1 comment
Leave a Reply Cancel reply
Fan page
Tags
Recent Posts
- Charles Proxy – Phần 1 : Giới thiệu, cài đặt và cấu hình
- Complete Concurrency với Swift 6
- 300 Bài code thiếu nhi bằng Python – Ebook
- Builder Pattern trong 10 phút
- Observer Pattern trong 10 phút
- Memento Pattern trong 10 phút
- Strategy Pattern trong 10 phút
- Automatic Reference Counting (ARC) trong 10 phút
- Autoresizing Masks trong 10 phút
- Regular Expression (Regex) trong Swift
You may also like:
Archives
- September 2024 (1)
- July 2024 (1)
- June 2024 (1)
- May 2024 (4)
- April 2024 (2)
- March 2024 (5)
- January 2024 (4)
- February 2023 (1)
- January 2023 (2)
- November 2022 (2)
- October 2022 (1)
- September 2022 (5)
- August 2022 (6)
- July 2022 (7)
- June 2022 (8)
- May 2022 (5)
- April 2022 (1)
- March 2022 (3)
- February 2022 (5)
- January 2022 (4)
- December 2021 (6)
- November 2021 (8)
- October 2021 (8)
- September 2021 (8)
- August 2021 (8)
- July 2021 (9)
- June 2021 (8)
- May 2021 (7)
- April 2021 (11)
- March 2021 (12)
- February 2021 (3)
- January 2021 (3)
- December 2020 (3)
- November 2020 (9)
- October 2020 (7)
- September 2020 (17)
- August 2020 (1)
- July 2020 (3)
- June 2020 (1)
- May 2020 (2)
- April 2020 (3)
- March 2020 (20)
- February 2020 (5)
- January 2020 (2)
- December 2019 (12)
- November 2019 (12)
- October 2019 (19)
- September 2019 (17)
- August 2019 (10)