iOS Networking - Moya & Protocol Buffer Serialisation - ios

Can anyone elaborate me on the possibility of using Moya for communicating with web services that use ProtoBuf Serialisation instead of JSON notation? Is it possible? Is it already implemented or is there any extensions for it?
Any information is really appreciated :)

Yes it is possible
Here is the process how you can use protobuf with Moya.
import Foundation
import Moya
import SwiftProtobuf
enum Currency {
case exchangeRate(param: SwiftProtobuf.Message)
}
extension Currency: TargetType {
var validationType: ValidationType {
return .successCodes
}
var headers: [String: String]? {
return [
"Accept": "application/octet-stream",
"Content-Type": "application/octet-stream",
"Authorization": [use auth token here if any],
]
}
var baseURL: URL {
return URL(string:"https:www.currency.com")
}
var path: String {
switch self {
case .exchangeRate:
return "/exchangeRate"
}
}
var method: Moya.Method {
switch self {
case .exchangeRate:
return .post
}
}
var parameters: Data? {
switch self {
case let .exchangeRate(param):
return try! param.serializedData()
}
}
var task: Task {
switch self {
case .exchangeRate:
return .requestData(self.parameters!)
}
}
}
You can access an API like this:
provider = MoyaProvider<GitHub>()
provider.request(.exchangeRate(param:[here will be protobuf model object])) {
result in
switch result {
case let .success(moyaResponse):
//do something with the response data
case let .failure(error):
//error occured
}
}

Related

Yelp Fusion API

I am a beginner trying to incorporate yelp's fusions API into my app but I couldn't find any proper resources on how to use the API. I am trying to use the business Search endpoint. can someone help me? Thanks.
I am using yelp for my location based restaurant viewing application.
I use Moya for the network requests. I have example for you:
In your codebase define your own api key!
import Foundation
import Moya
private let apiKey = MyConstants.shared.apiKey
enum YelpService {
enum BusinessProvider: TargetType {
case search(lat: Double, long: Double)
case details(id: String)
var baseURL: URL {
return URL(string: "https://api.yelp.com/v3/businesses")!
}
var path: String {
switch self {
case .search:
return "/search"
case let .details(id):
return "/\(id)"
}
}
var method: Moya.Method {
return .get
}
var sampleData: Data {
return Data()
}
var task: Task {
switch self {
case let .search(lat, long):
return .requestParameters(parameters: ["latitude": lat, "longitude": long, "limit": 10], encoding: URLEncoding.queryString)
case .details:
return .requestPlain
}
}
var headers: [String: String]? {
return ["Authorization": "Bearer \(apiKey)"]
}
}
}
Your api link should look like this : https://api.yelp.com/v3/businesses/search

Chaining login operations with RxSwift

I'm creating an app that has a specific two-way authentication process:
First, a REST based login with credentials, which returns a websocket endpoint on the server, plus an authtoken to use in order to connect to it.
Only after the websocket has successfully connected to the server, an I supposed to switch to the next view.
Trying to implement under MVVM, I have created an API struct for the server net calls:
My LoginViewController binds the username and password to the LoginViewModel, which in return binds a login Action to the login button:
func onLogin() -> CocoaAction {
return CocoaAction { _ in
self.loginService.login(username: self.usernameText.value, password: self.passwordText.value)
let MainViewModel = MainViewModel(sceneCoordinator: self.sceneCoordinator)
return self.sceneCoordinator.transition(to: Scene.mainView(mainViewModel), type: .modal).asObservable().map { _ in }
}
}
The LoginService should return a Completable for the login, in order to indicate a successful login (and move the view to the main app screen) or an error to show the user.
protocol LoginServiceType {
#discardableResult
func login(username: String, password: String) -> Completable
}
I'm having an issue with the implementation of this function. It should first call the REST login API, and after getting the response, start the connection to the websocket. The implementation for the server API is as follows (under recommended RxSwift MVVM examples):
struct Server: ServerProtocol {
// MARK: - API Errors
enum Errors: Error {
case requestFailed
}
// MARK: - API Endpoint requests
static func login(for username: String, password: String) -> Observable<JSONObject> {
let parameters = [
"username": username,
"password": password
]
return request(address: Server.Address.login, parameters: parameters)
}
// MARK: - generic request to API
static private func request<T: Any>(address: Address, parameters: [String: String] = [:]) -> Observable<T> {
return Observable.create { observer in
let request = Alamofire.request(address.url.absoluteString,
method: .post,
parameters: parameters,
encoding: JSONEncoding.default,
headers: nil)
request.responseJSON { response in
guard response.result.isSuccess == true, let data = response.data,
let json = try? JSONSerialization.jsonObject(with: data, options: []) as? T, let result = json else {
observer.onError(Errors.requestFailed)
return
}
observer.onNext(result)
observer.onCompleted()
}
return Disposables.create {
request.cancel()
}
}
}
}
So I'm trying to figure out how to connect the REST call, its response with the need endpoint+Token, to creating the Websocket and subscribing to it's connect callback, which then should return the Completable back to the LoginViewModel.
Any advice would be welcome.
I think "flatmap" is what you're looking for.
Have a look:
var mynum = Variable(0)
let disposeBag = DisposeBag()
func login() -> Observable<(String,String)> {
return Observable.create { observer in
// Place your server access code
if (<some_condition>) { // if error
observer.on(.error(<custome error>))
}
observer.on(.next(("websocket", "authtoken")))
observer.on(.completed)
return Disposables.create()
}
}
func API(webSiteData: (String, String)) -> Observable<Int> {
return Observable.create { observer in
// Place your second server access code
print (webSiteData.0)
print (webSiteData.1)
observer.on(.next(1)) // assiging "1" just as an example, you may ignore
observer.on(.completed)
return Disposables.create()
}
}
func combine() {
self.login().catchError({ (currentError) -> Observable<(String, String)> in
print (currentError.localizedDescription)
return Observable.just(("",""))
})
.flatMap(self.API)
.bind(to: mynum)
.disposed(by: self.disposeBag)
}

Enum element cannot be referenced as an instance member

I'm creating an API layer using Moya and keep getting the above mentioned error for the .updateMyWeightGoal target when I'm creating a request for an endpoint.
goalAPI.request(target: .updateMyWeightGoal(weightGoalData: goalInfo), success: { (response) in
//
}){ (response: [String : Any]) in
print(response)
}
I've created another Moya API of the same type and am calling it using the same goalAPI as this and it's working fine.
Any ideas what might be causing this issue
For reference here is the class definition for the weightGoalData type
class UpdatedWeightGoalInfo: Mappable {
var consumerUserID: Int?
var height: String?
var weight: String?
var goalWeight: String?
init() {
}
convenience init(userId: Int, weightGoalData: WeightGoalResponse) {
self.init()
consumerUserID = userId
height = "\(weightGoalData.currentHeight)"
weight = "\(weightGoalData.currentWeight)"
goalWeight = "\(weightGoalData.goalWeight)"
}
required init?(map: Map) {
}
func mapping(map: Map) {
consumerUserID <- map["consumerUserId"]
height <- map["height"]
weight <- map["weight"]
goalWeight <- map["goalWeight"]
}
}
And the definition of the API:
enum GoalSettingAPI: AccessTokenAuthorizable {
case updateMyWeightGoal(weightGoalData: UpdatedWeightGoalInfo)
}
extension GoalSettingAPI: TargetType {
var parameterEncoding: ParameterEncoding {
switch self {
default:
return JSONEncoding.default
}
}
var baseURL: URL { return URL(string: appBaseURL + "*hidden*/")! }
var path: String {
switch self {
case .updateMyWeightGoal(_):
return "updateMyWeightGoal"
}
}
var method: Moya.Method {
switch self {
case .updateMyWeightGoal(_):
return .post
}
}
var parameters: [String: Any]? {
switch self {
case .updateMyWeightGoal(let weightGoalData):
return weightGoalData.toJSON()
}
}
var sampleData: Data {
switch self {
default:
return Data()
}
}
var task: Task {
switch self {
default:
return .request
}
}
var shouldAuthorize: Bool {
switch self {
default:
return false
}
}
}
This is the stupidest thing.
As it turns out the error was coming, not from the enum, but from the success block. It was expecting an object of type Mappable, which I wasn't providing.
You're referring to .updateMyWeightGoal as an instance member (.) when it is declared as an enum. Try changing:
goalAPI.request(target: .updateMyWeightGoal(weightGoalData: goalInfo)
To
goalAPI.request(target: GoalSettingAPI.updateMyWeightGoal(weightGoalData: goalInfo)
Had the same error. In my case the issue was that I had typed:
if someVariable = .AnEnumValue
What I meant was:
if someVariable == .AnEnumValue
The key difference being = vs ==.
Not sure how the compiler landed on this particular error message, but this fixed the problem.

Construct Moya TargetType in iOS

I'm trying to use Moya with RxSwift in my project
I'm facing the problem with url contain "?"
This TargetType I have create
private extension String {
var URLEscapedString: String {
return self.addingPercentEncoding(withAllowedCharacters: CharacterSet.urlHostAllowed)!
}
}
enum TMDb {
case discoverMovieList(page: Int)
case discoverMovieDetail(moive: Movie)
}
extension TMDb: TargetType {
var baseURL: URL { return URL(string: BASE_URL)! }
var path: String {
switch self {
case .discoverMovieList(page: let page):
return "discover/movie?api_key=\(API_KEY)&sort_by=release_date.desc&page=\(page)"
}
}
var method: Moya.Method {
return .get
}
var parameters: [String: Any]? {
return nil
}
var sampleData: Data {
switch self {
case .discoverMovieList(page: _):
return "test".data(using: .utf8)!
case .discoverMovieDetail(moive: _):
return "test1".data(using: .utf8)!
}
}
var task: Task {
return .request
}
var parameterEncoding: ParameterEncoding {
return URLEncoding.default
}
}
The problem is when I make the request. The url path return is somehow not correct
This is the url i got from console
Optional("https://api.themoviedb.org/3/discover/movie%3Fapi_key=58da429caf2e25e8ff9436665e2f0e36&sort_by=release_date.desc&page=1")
But the correct one should be
https://api.themoviedb.org/3/discover/movie?api_key=58da429caf2e25e8ff9436665e2f0e36&sort_by=release_date.desc&page=1
There something wrong when handle the "?" (it become %3F" character in the url. How can we make it work normally?
Update
This is how I call my Moya
let provider: RxMoyaProvider<TMDb>
let persistentContainer: PersistentContainer
func discoverMoiveList(for page: Int) {
self.provider.request(TMDb.discoverMovieList(page: 1)) { (result) in
print(result.value?.request?.url?.absoluteString ?? "no url")
}
}
}
Here you've got a topic how to get an optional variable without Opt(). I prefer that way where you create an extension:
extension Optional {
func asStringOrNilText() -> String {
switch self {
case .some(let value):
return String(describing: value)
case _:
return "(nil)"
}
}
}
print(result.value?.request?.url?.absoluteString.asStringOrNilText())
The problem is not about "?". It occurred because of you used optional values in your path URL. Control BASE_URL and API_KEY, they must not be optional values.
There is no problem with the Moya. Here Moya-Logger result for your provider:
Moya_Logger: [29/04/2017 15:48:22] Request: https://api.themoviedb.org/3/discover/movie%3Fapi_key=58da429caf2e25e8ff9436665e2f0e36&sort_by=release_date.desc&page=1
Moya_Logger: [29/04/2017 15:48:22] Request Headers: [:]
Moya_Logger: [29/04/2017 15:48:22] HTTP Request Method: GET
Moya_Logger: [29/04/2017 15:48:23] Response: <NSHTTPURLResponse: 0x618000220360> { URL: https://api.themoviedb.org/3/discover/movie%3Fapi_key=58da429caf2e25e8ff9436665e2f0e36&sort_by=release_date.desc&page=1 } { status code: 200, headers {
Connection = "keep-alive";
"Content-Length" = 89;
"Content-Type" = "application/octet-stream";
Date = "Sat, 29 Apr 2017 12:48:23 GMT";
Server = openresty;
"X-RateLimit-Limit" = 40;
"X-RateLimit-Remaining" = 39;
"X-RateLimit-Reset" = 1493470113;
} }
You should pass anything after the ? as parameter, this way Moya knows how to construct the URL correctly.
var path: String {
switch self {
case .discoverMovieList(page: let page):
return "discover/movie"
}
}
var method: Moya.Method {
return .get
}
var parameters: [String: Any]? {
switch self {
case .discoverMovieList(page: let page):
return "["api_key":"\(API_KEY)",
"sort_by":"release_date.desc",
"page":page]"
}
}

Using an enum in generic function

I want to create a generic function with an enum as generic parameters.
I have the following generic function:
func buildProvider<T>(service: T) -> RxMoyaProvider<T> {
let endpointClosure = { (target: T) -> Endpoint<T> in
let url = target.baseURL.appendingPathComponent(target.path).absoluteString
let endpoint = Endpoint<T>(URL: url, sampleResponseClosure: {.networkResponse(200, target.sampleData)}, method: target.method, parameters: target.parameters)
return endpoint
}
return RxMoyaProvider<T>(endpointClosure: endpointClosure)
}
I call it like this:
self.userProvider = buildProvider(service: UserService)
Below you see the declaration of userProvider:
var userProvider: RxMoyaProvider?
And below you see an example of UserService:
import Foundation
import Moya
enum UserService {
case login(qrKey: String, language: String, fcmToken: String)
}
extension UserService: TargetType {
var baseURL: URL {
let appConfig = AppConfig()
let hostname = try! appConfig.hostname()
return URL(string: hostname)!
}
var path: String {
switch self {
case .login(_,_,_):
return "/v1/login"
}
}
var method: Moya.Method {
switch self {
case .login(_,_,_):
return .post
}
}
var parameters: [String: Any]? {
switch self {
case .login(let qrKey, let language, let fcmToken):
let params = ["qr-key": qrKey, "language": language, "os": "ios", "fcm-token": fcmToken]
return params
}
}
var sampleData: Data {
switch self {
case .login(_, _, _):
return "".data(using: .utf8)!
}
}
var task: Task {
return .request
}
}
And I get the following error:
Cannot convert value of type 'UserService.Type' to expected argument type 'UserService'
Can someone help me with this?
Thanks !
I have no experience with Moya and can not know for sure how it works and what you're trying to achieve, but it seems like you're trying to pass type into function. If so you must do something like:
func buildProvider<T>(service: T.Type) -> RxMoyaProvider<T> {
...
return RxMoyaProvider<T>(endpointClosure: endpointClosure)
}
and call it with .self:
self.userProvider = buildProvider(service: UserService.self)
but it's not your only problem. How do you know without context that type <T> would have baseURL property:
let endpointClosure = { (target: T) -> Endpoint<T> in
let url = target.baseURL.appendingPathComponent(target.path).absoluteString
?
Seems like you've messed up with overall architecture, and I would strongly advice to reconsider it.

Resources