Skip to content
  • Home
  • Code
  • iOS & Swift
  • Combine
  • RxSwift
  • SwiftUI
  • Flutter & Dart
  • Tutorials
  • Art
  • Blog
Fx Studio
  • Home
  • Code
  • iOS & Swift
  • Combine
  • RxSwift
  • SwiftUI
  • Flutter & Dart
  • Tutorials
  • Art
  • Blog
Written by chuotfx on December 18, 2019

Basic iOS tutorial : Core API

iOS & Swift

Contents

  • Chuẩn bị
  • 1. Tư tưởng
    • 1.1. CoreAPI
    • 1.2. Ý nghĩa của Model
  • 2. Định nghĩa
    • 2.1. Error
    • 2.2. Completion
    • 2.3. Result
    • 2.4. Connect
    • 2.5. Request
  • 3. Thành phần của CoreAPI
    • 3.1. Core connection
    • 3.2. Manager
    • 3.3. Domain API
  • 4. Request Handler
  • 5. Parse data
  • Tạm kết

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:

    • Closure trong 10 phút
    • Grand Central Dispatch – Managing Task
    • Basic iOS tutorial : MVVM

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ới permission denied. Với lỗi này thì bạn không hiển thị gì cả. Và cố gắng logout 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 Protocol Error
  • 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ụng switch 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ột generic
  • 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

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 cho GET
  • QueryParam: là struct để tạo các dictionary cho body của các request POST (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ợp success.

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!

 

FacebookTweetPinYummlyLinkedInPrintEmailShares30

Related Posts:

  • RxCocoa Basic – Binding Observables
    RxCocoa Basic – Binding Observables
  • RxCocoa Basic - Working with multi UI Control
    RxCocoa Basic - Working with multi UI Control
  • RxSwift vs. UIKit - Fetching Data from API
    RxSwift vs. UIKit - Fetching Data from API
  • RxCocoa Basic – Delegate Proxy
    RxCocoa Basic – Delegate Proxy
Tags: api, basic ios tutorial, iOS
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

Your email address will not be published. Required fields are marked *

Donate – Buy me a coffee!

Fan page

Fx Studio

Tags

Actor Advanced Swift api AppDistribution Asynchronous autolayout basic ios tutorial blog callback ci/cd closure collectionview combine concurrency CoreData Core Location crashlytics darkmode dart dart basic dart tour Declarative decoding delegate deploy fabric fastlane firebase flavor flutter GCD iOS mapview MVVM optional protocol rxswift Swift Swift 5.5 SwiftUI SwiftUI Notes tableview testing TravisCI unittest

Recent Posts

  • Raw String trong 10 phút
  • Dispatch Semaphore trong 10 phút
  • Tổng kết năm 2022
  • KeyPath trong 10 phút – Swift
  • Make color App Flutter
  • Ứng dụng Flutter đầu tiên
  • Cài đặt Flutter SDK & Hello world
  • Coding Conventions – người hùng hay kẻ tội đồ?
  • Giới thiệu về Flutter
  • Tìm hiểu về ngôn ngữ lập trình Dart

You may also like:

  • async/await to Fetch REST API - Swift 5.5
    async/await to Fetch REST API - Swift 5.5
  • Basic Grid Layout - SwiftUI Notes #52
    Basic Grid Layout - SwiftUI Notes #52
  • RxCocoa Basic – Merge Observables Input
    RxCocoa Basic – Merge Observables Input
  • RxCocoa Basic – Delegate Proxy
    RxCocoa Basic – Delegate Proxy
  • RxCocoa Basic - Display Data
    RxCocoa Basic - Display Data

Archives

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

About me

Education, Mini Game, Digital Art & Life of coders
Contacts:
contacts@fxstudio.dev

Fx Studio

  • Home
  • About me
  • Contact us
  • Mail
  • Privacy Policy
  • Donate
  • Sitemap

Categories

  • Art (1)
  • Blog (22)
  • Code (4)
  • Combine (22)
  • Flutter & Dart (24)
  • iOS & Swift (86)
  • RxSwift (37)
  • SwiftUI (76)
  • Tutorials (70)

Newsletter

Stay up to date with our latest news and posts.
Loading

    Copyright © 2023 Fx Studio - All rights reserved.

    Share this ArticleLike this article? Email it to a friend!

    Email sent!