Halo, lumayan lama juga tidak mengupdate web ini. Selain kesibukan sehari-hari dan persiapan nikah, juga kadang aga males nerusin tulisan-tulisan yang ada di draft (ada sekitar 10an draft yang ga diterusin, yang kadang dikarenakan males, ataupun sudah out of date).

Untuk kali ini, akan coba membahas mengenai Moya . Dan bagaimana biasanya saya menggunakan Moya pada project-project iOS. Secara singkat, moya bisa dijabarkan sebagai abstraksi untuk urusan koneksi ke server atau backend atau apapun yang nyebrang-nyebrangin data via internet ataupun intranet pada sebuah aplikasi iOS. Jadi pada prakteknya developer tinggal membuat sebuah abstraksi atau skeleton atau protocol yang merupakan turunan protocol tertentu dari salah satu protocol-nya Moya. Kedengerannya ribet? emang iya, tapi kalo udah biasa simple dan ngebantu banget.

Pra Moya

Dulu, ketika masih menggunakan objective-c sebagai bahasa utama dalam development selalu menggunakan AFNetworking untuk urusan koneksi, untuk mempermudah dibuat ‘wrapper’-nya berupa pod yang mengelompokan task-task tertentu (misal post, get, upload, download, dll). Pun ketika beralih ke swift dan menggunakan Alamofire juga sama, wrapper yang pernah saya karang-karang sendiri sebelum kenal moya saya beri nama pheConnector dan bisa dilihat di github. Oh iya, untuk pheConnector yang di publish pada github juga menggunakan SwiftyJSON untuk mempermudah memproses JSON.

Moya

Sekarang, setelah kenal Moya, project-project belakangan lebih banyak menggunakan Moya ketimbang pheConnector karangan saya sendiri. Karena memang Moya lebih rapih secara struktur, dan Moya juga sudah didukung dan diawasi serta diperbaiki oleh banyak manusia (Open source rules 🤘). Ya pada intinya apalah si aphe ini, cuma serpihan debu processor 😭

Untuk kali ini, saya ingin berbagi bagaimana saya menggunakan Moya pada project-project yang sedang saya kerjakan. Silahkan dikomentari apabila dianggap kurang tepat dan juga kurang bagus.

Skenario

Anggaplah akan dibuat satu view controller untuk login sebuah aplikasi. httpbin akan digunakan sebagai backend-nya. Buat project baru, setup pod seperti yang pernah saya jabarkan di post sebelumnya dengan requirement yaiut Moya dan SwiftyJSON.

Buat view sederhana yang berisi kolom username, kolom password dan button sign in seperti di bawah ini

Tampilan

Sebelum melakukan logic pada action yang akan dilakukan (login) Moya perlu setup tertentu. Menurut saya ini yang membuat developer yang tidak terbiasa dengan setup atau abstraksi koneksi beranggapan Moya ini “ribet”. Sebetulnya tidak juga, dengan memanfaatkan teknik protocol yang digembor-gemborkan pada developer swift, membuat Moya sangat fleksibel, walaupun kadang memang saya tidak membuat satu abstraksi keseluruhan melainkan membaginya menjadi sesuai dengan logic dan action aplikasi yang dibuat.

Oke, daripada keder dengan bahasa dan istilah lebih enak liat kodenya kan.

Saya akan membuat 2 file swift yaitu Connector.swift dan User.swift diberi nama User.swift karena mengasumsikan untuk seluruh abstraksi koneksi untuk logic yang berkaitan dengan ‘user’ (login, logout, fetch user, dll) menggunakan logic yang ada pada file ini.

Untuk Connector.swift merupakan abstraksi global untuk abstraksi-abstraksi lainnya. Contoh, main url dan header, yang secara taktis tidak akan berubah banyak pada pengaplikasiannya. Untuk main url dan header akan saya tempatkan pada info.plist sehingga baris pertama dari Connector.swift menjadi seperti berikut

extension Bundle {
    
    var baseURL: URL {
        guard let info = self.infoDictionary,
            let urlString = info["Base URL"] as? String,
            let url = URL(string: urlString) else {
                fatalError("Cannot get base url from info.plist")
        }
        return url
    }
    
    var AppKey: String {
        guard let info = self.infoDictionary,
            let appKey = info["App Key"] as? String else {
                fatalError("Cannot get App Key from info.plist")
        }
        
        return appKey
    }
}

extension URL {
    static var baseURL: URL {
        return Bundle.main.baseURL
    }
}

func joinDictionary(from arrayDict: Array<Dictionary<String, Any>>) -> Dictionary<String, Any> {
    var some = Dictionary<String, Any>()
    for dict in arrayDict {
        some += dict
    }
    return some
}

func += <K, V> (left: inout [K : V], right: [K : V]) {
    for (k, v) in right {
        left.updateValue(v, forKey: k)
    }
}

AppKey akan digunakan sebagai header, biasanya sih untuk logging di server. function joinDictionary dan += untuk menggabungkan beberapa header (apabila nantinya diperlukan header lebih dari satu)

Kemudian ganti import Foundation dengan import Moya dan buat satu protocol baru yang merupakan turunan dari protocol TargetType milik Moya. Dan buat extension dari protocol tersebut untuk mengisi baseURL dan headers secara default sehingga pada logic tidak diperlukan lagi menambahkan variable tersebut (asumsi baseURL dan headers tidak berubah-ubah untuk masing-masing logic). Kali ini nama protocol yang saya gunakan CustomTarget

import Moya

protocol CustomTarget: TargetType {
}

extension CustomTarget {
    var baseURL: URL {
        return Bundle.main.baseURL
    }
    
    var headers: [String : String]? {
        return joinDictionary(from: [
            ["App-key": Bundle.main.AppKey],
            ["User-Agent": "\(Bundle.main.infoDictionary?["CFBundleShortVersionString"] as! String) (app for \(UIDevice.current.model); iOS version \(UIDevice.current.systemVersion)) ~ phe"]
            ]) as? [String: String]
    }
}


Oke, untuk selanjutnya import Moya dan SwiftyJSON pada file User.swift Kemudian buat sebuah enum untuk logic-logic yang diperlukan pada Action user (untuk kali ini saya hanya mencontohkan login). Kenapa menggunakan enum dan bukan struct atau class atau variable biasa saja? nanti akan dijelaskan di bawah. Nama enum yang saya gunakan adalah UserServices

import Moya
import SwiftyJSON

enum UserServices {
    case login(username:String, password: String)
}

Kemudian, buat extension dari enum tersebut yang merupakan turunan dari CustomTarget maka xcode pun akan protes karena variable-variable yang diturunkan dari TargetType Moya tidak sepenuhnya diimplementasi pada extension tersebut, klik tanda bulatan merah dan xcode akan menambakan bedasarkan stubs yang telah dideklarasi pada protocol TargetType.

Rubah template tersebut sehingga menjadi sebagai berikut.

extension UserServices: CustomTarget {
    var path: String {
        switch self {
        case .login:
            return "post"
        }
    }
    
    var method: Moya.Method {
        switch self {
        case .login:
            return .post
        }
    }
    
    var sampleData: Data {
        return Data()
    }
    
    var task: Task {
        switch self {
        case .login(let username, let password):
            return .requestParameters(parameters: ["username": username, "password": password], encoding: URLEncoding.default)
        }
    }
}

Pada bagian

    var path: String {
        switch self {
        case .login:
            return "post"
        }
    }

Merupakan URL akhir dari API atau alamat yang dituju. Karena untuk backend yang digunakan adalah httpbin maka untuk method post URL yang dituju adalah post, kemudian var method method untuk logic tersebut (post, get, put, dll) var sampleData biasanya digunakan untuk unit testing, tapi untuk unit testing Moya akan saya jabarkan pada post berikutnya. Dan var task merupakan tempat pembuatan data atau parameter apa saja yang akan dikirim ke server.

Dari baris script di atas, dapat dilihat dengan menggunakan enum, apabila ada penambahan logic pada User, maka tinggal ditambahkan pada enum tersebut, sebagai contoh apabila User perlu aktifasi maka pada UserService cukup ditambahkan case baru yaitu activation(username: String) seperti berikut

enum UserServices {
    case login(username:String, password: String)
    case activation(username: String)
}

Dan karena secara sengaja saya tidak menambahakan default pada masing-masing switch-case maka secara otomatis xcode akan protes karena tidak semua case dijabarkan pada switch tersebut.

Kemudian, bagaimana caranya men-triger Moya untuk memanggil Alamofire dan nembak API atau URL yang ingin kita tuju? Moya menyediakan satu class untuk hal tersebut, yaitu MoyaProvider, tapi sebelum ke sana buat sebuah struct pada User.swift sehingga ketika perlu memanggil logic hanya perlu memanggil struct tersebut,

struct userDo {
    static let provider = MoyaProvider<UserServices>()
    
    static func request(
        target: UserServices,
        success successCallback: @escaping (JSON) -> Void,
        error errorCallback: @escaping (_ statusCode: Error) -> Void,
        failure failureCallback: @escaping (MoyaError) -> Void,
        finish isFinish: @escaping () -> Void
        ) {
        provider.request(target) { (_response) in
            defer {
                isFinish()
            }
            switch _response {
            case .success(let result):
                do {
                    let _ = try result.filterSuccessfulStatusCodes()
                    let json = try JSON(result.mapJSON())
                    successCallback(json)
                } catch let err {
                    errorCallback(err)
                }
            case .failure(let err):
                failureCallback(err)
            }
        }
    }
}

Karena untuk koneksi biasanya tidak diperlukan feedback apakah fungsi tersebut telah selesai dieksekusi maka digunakan system callback yang menjalakan perintah asynchronous.

Dengan sedikit tweak fungsi di atas bisa dibuat lebih menarik dan reusable seperti berikut.

Pada file Connector.swift tambahkan protocol seperti berikut

protocol Request {
    associatedtype T
    associatedtype callBackType
    func request(target: T,
                 success successCallback: @escaping (callBackType) -> Void,
                 error errorCallback: @escaping (_ statusCode: Error) -> Void,
                 failure failureCallback: @escaping (MoyaError) -> Void,
                 finish isFinish: @escaping () -> Void)
}

lalu pada User.swift dirubah menjadi sebagai berikut

struct userDo: Request {
    typealias T = UserServices
    typealias callBackType = JSON
    
    let provider = MoyaProvider<UserServices>()
    
    func request(target: UserServices,
                 success successCallback: @escaping (JSON) -> Void,
                 error errorCallback: @escaping (Error) -> Void,
                 failure failureCallback: @escaping (MoyaError) -> Void, finish isFinish: @escaping () -> Void) {
        self.provider.request(target) { (_response) in
            defer {
                isFinish()
            }
            switch _response {
            case .success(let result):
                do {
                    let _ = try result.filterSuccessfulStatusCodes()
                    let json = try JSON(result.mapJSON())
                    successCallback(json)
                } catch let err {
                    errorCallback(err)
                }
            case .failure(let err):
                failureCallback(err)
            }
        }
    }
}

Hal ini akan mempermudah pembacaan dan pengelompokan tiap abstraksi untuk masing-masing abstraksi apabila bertambah.

Oke, kembali ke file ViewController.swift yang sebelumnya telah dihubungkan masing-masing untuk UITextfield dan button actionnya. Untuk hal ini saya menggunakan nama fungsi doLogin untuk button action dan uname serta pswd untuk uitexfield username dan password, sehingga dapat dijabarkan sebagai berikut.

    @IBAction func doLogin(_ sender: UIButton) {
        let (username, password) = (uname.text, pswd.text)
        userDo().request(target: .login(username: username ?? "empty", password: password ?? "empty"),
                         success: { (val) in
                            print(val) },
                         error: { (err) in
                            print(err) },
                         failure: { (err) in
                            print(err)
        }) {}
    }

Saya tidak melakukan pengecekan pada textfield. Apabila ingin melakukan textfield secara otomatis bisa membacanya kembali di postingan sebelumnya , oleh karena itu apabila textfield nil maka saya unwarp dengan string empty (sebenernya bisa dengan force unwarp dengan menggunakan ! tapi saya pribadi tidak begitu suka).

Oke, terakhir adalah dengan menambahakan property list Base URL dan App Key pada file Info.plist

info.plist

Maka apabila pada username saya isi sakura dan password saya isi minamoto maka balikan dari httpbin yang di print out pada console seperti berikut,

{
	"files": {

	},
	"origin": "*IP Mu*",
	"data": "",
	"headers": {
		"Content-Type": "application\/x-www-form-urlencoded; charset=utf-8",
		"Connection": "close",
		"Host": "httpbin.org",
		"App-Key": "cizzu",
		"Accept": "*\/*",
		"Content-Length": "33",
		"User-Agent": "1.0 (app for iPhone; iOS version 11.2.2) ~ phe",
		"Accept-Language": "en-US;q=1.0, id-US;q=0.9, de-US;q=0.8, ja-JP;q=0.7",
		"Accept-Encoding": "gzip;q=1.0, compress;q=0.5"
	},
	"json": null,
	"form": {
		"username": "sakura",
		"password": "minamoto"
	},
	"args": {

	},
	"url": "https:\/\/httpbin.org\/post"
}

Bisa dilihat diatas data yang di-post dalam bentuk form dan bukan json, untuk mengganti supaya Moya mengirim dalam format JSON maka tinggal rubah var task pada UserServices bagian encoding menjadi JSONEncoding.default.

    var task: Task {
        switch self {
        case .login(let username, let password):
            return .requestParameters(parameters: ["username": username, "password": password], encoding: JSONEncoding.default)
        }
    }

Yap, demikian kiranya post kali ini, pada post berikutnya saya akan coba menjabarkan unit test pada Moya. Semoga bermanfaat.

Source untuk post ini dapat dilihat di github cizzu pada link berikut ini

Terima kasih.