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.
Related
Here, we have a scenario where I am facing issue for parsing the model class using "JSONDecoder".
Let me share you what I have done in this example and where I am facing issue:
There's a model protocol derived through the Codable.
A struct model and A class GenericExample
GenericExample class have following things:
shared instance
typeContainer which is the type of Protocol
There's two more methods staticClassParsing and dynamicClassParsing with one parameter dynamicType
In the last, One generic method which return the parsed data model.
Calling method for parsing the model:
GenericExample.shared.dynamicClassParsing(Numbers.self,data: "[\r\n{\"one\": \"1\"},\r\n{\"two\":\"2\"}\r\n]")
Compile Time Error:
Generic parameter 'T' could not be inferred
Occurred here
returnModelType: typeContainer.self
**For more details, please go through following code: **
protocol BaseMapModel : Codable { }
struct Numbers: BaseMapModel {
var one: String?
var two: String?
}
class GenericExample: NSObject {
static let shared = GenericExample()
var typeContainer : BaseMapModel.Type?
private override init() {
super.init()
}
}
extension GenericExample {
// Static Class Parsing passed through the conversion
func staticClassParsing() {
let dataJson = "[\r\n{\"one\": \"1\"},\r\n{\"two\":\"2\"}\r\n]"
convertTypeContainer(data: Data(dataJson.utf8), returnModelType: Numbers.self) { (mappedResult) in
print(mappedResult?.one ?? "")
print(mappedResult?.two ?? "")
}
}
// Dynamic Class Parsing can't passed through the conversion
// Error:- Generic parameter 'T' could not be inferred
// Error Parameter:- in "returnModelType: typeContainer.self"
func dynamicClassParsing(_ dynamicType: BaseMapModel.Type, data:String) {
typeContainer = dynamicType.self
convertTypeContainer(data: Data(data.utf8), returnModelType: typeContainer.self) { (mappedResult) in
print(mappedResult?.one ?? "")
print(mappedResult?.two ?? "")
}
}
}
extension GenericExample {
private func convertTypeContainer<T : BaseMapModel>(data:Data, returnModelType: T.Type, completion: ((_ result:T?)->Void)) {
guard let responseModel = try? JSONDecoder().decode(returnModelType.self, from: data) else {
completion(nil)
return
}
completion(responseModel)
}
}
typeContainer must be a concrete type, it cannot be a protocol. And the completion handler is pointless as JSONDecoder works synchronously.
To decode JSON with generics you have to use something like this, it's highly recommended to handle also the Decoding error
struct Numbers: Decodable {
var one: String?
var two: String?
}
class GenericExample: NSObject {
static let shared = GenericExample()
}
extension GenericExample {
func dynamicClassParsing<T : Decodable>(_ dynamicType: T.Type, data: String) -> Result<T,Error> {
return Result { try JSONDecoder().decode(T.self, from: Data(data.utf8)) }
}
}
let dataJson = """
[{"one": "1"},{"two":"2"}]
"""
let result = GenericExample.shared.dynamicClassParsing([Numbers].self, data: dataJson)
switch result {
case .success(let numbers): print(numbers)
case .failure(let error): print(error)
}
First of all, Thanks to All for your support. And Yes, I would like to post answer to my question.
class BaseMapModel : Codable { }
class Numbers: BaseMapModel {
var one: String?
var two: String?
enum CodingKeys: String, CodingKey {
case one = "one"
case two = "two"
}
// Decoding
required init(from decoder: Decoder) throws {
let response = try decoder.container(keyedBy: CodingKeys.self)
one = try? response.decode(String.self, forKey: .one)
two = try? response.decode(String.self, forKey: .two)
let superDecoder = try response.superDecoder()
try super.init(from: superDecoder)
}
}
class GenericExample: NSObject {
static let shared = GenericExample()
var defaultTypeContainer : Numbers.Type!
var typeContainer : BaseMapModel.Type?
private override init() {
super.init()
}
}
extension GenericExample {
// Static Class Parsing passed through the conversion
func staticClassParsing() {
let dataJson = "[\r\n{\"one\": \"1\"},\r\n{\"two\":\"2\"}\r\n]"
convertTypeContainer(data: Data(dataJson.utf8), returnModelType: Numbers.self) { (mappedResult) in
print(mappedResult?.one ?? "")
print(mappedResult?.two ?? "")
}
}
// Dynamic Class Parsing passed through the conversion
func dynamicClassParsing(_ dynamicType: BaseMapModel.Type, data:String) {
typeContainer = dynamicType.self
convertTypeContainer(data: Data(data.utf8), returnModelType: (typeContainer ?? defaultTypeContainer).self) { (mappedResult) in
print((mappedResult as? Numbers)?.one ?? "")
print((mappedResult as? Numbers)?.two ?? "")
}
}
}
extension GenericExample {
private func convertTypeContainer<T : BaseMapModel>(data:Data, returnModelType: T.Type, completion: ((_ result:T?)->Void)) {
guard let responseModel = try? JSONDecoder().decode(returnModelType.self, from: data) else {
completion(nil)
return
}
completion(responseModel)
}
}
I'm trying to inject dependency using Swinject, and I have no clue what I'm doing wrong.
I have protocol, that handles registring user.
protocol AuthServiceProtocol {
func registerUser(email: String, password: String, completion: #escaping CompletionHandler) }
and a class that conforms to this protocol make all the logic:
class AuthService: AuthServiceProtocol {
func registerUser(email: String, password: String, completion: #escaping CompletionHandler) {
let lowerCaseMail = email.lowercased()
let body: [String: Any] = [
"email": lowerCaseMail,
"password" : password
]
Alamofire.request(URL_REGISTER, method: .post, parameters: body, encoding: JSONEncoding.default, headers: HEADER).responseString { (response) in
if response.result.error == nil {
completion(true)
} else {
completion(false)
debugPrint(response.result.error as Any)
}
}
}
}
so, in AppDelegate we register container and it looks like:
let container = Container() { container in
container.register(AuthServiceProtocol.self) { _ in AuthService() }.inObjectScope(.container)
container.register(CreateAccountVC.self) { r in
let controller = CreateAccountVC()
controller.authService = r.resolve(AuthServiceProtocol.self)
return controller
}
}
but in CreateAccountVC authService is empty. Any ideas how can i do it?
CreateAccountVC is a subclass of ViewController, i have try'ed it by property, and constructors, but it's nil all the time.
Check your code:
var container : Container {
let container = Container()
container.register(AuthServiceProtocol.self) { _ in AuthService() }.inObjectScope(.container)
container.register(CreateAccountVC.self) { r in
let controller = CreateAccountVC()
controller.authService = r.resolve(AuthServiceProtocol.self)
print(r.resolve(AuthServiceProtocol.self))
return controller
}
return container
}
You have computed property and every time you call it, it creates a NEW Container object.
Refactor your code to have a single Container and I believe you will be good to go.
EDIT:
Here's a working code snippet.
Below is a small wrapper class to abstract concrete DI service (in case Swinject is one day replace by something else):
import Swinject
public class ConfigurationProvider {
// Currently using Swinject
private let backingService = Container()
// Singleton
public static let shared = ConfigurationProvider()
// Hidden initializer
private init() {}
// MARK: - Bind / Resolve
public func bind<T>(interface: T.Type, to assembly: T) {
backingService.register(interface) { _ in assembly }
}
public func resolve<T>(interface: T.Type) -> T! {
return backingService.resolve(interface)
}
}
// Extension methods to ignore 'shared.' call, like:
// ConfigurationProvider.bind(interface: IAssembly, to: Assembly())
// ConfigurationProvider.resolve(interface: IAssembly)
public extension ConfigurationProvider {
static func bind<T>(interface: T.Type, to assembly: T) {
ConfigurationProvider.shared.bind(interface: interface, to: assembly)
}
static func resolve<T>(interface: T.Type) -> T! {
return ConfigurationProvider.shared.resolve(interface: interface)
}
}
Usage:
class RSAuthLoginModuleAssembly: IAuthLoginModuleAssembly {
}
// Register:
ConfigurationProvider.bind(interface: IAuthLoginModuleAssembly.self, to: ConcreteAuthLoginModuleAssembly())
// Resolve:
guard let assembly = ConfigurationProvider.resolve(interface: IAuthLoginModuleAssembly.self) else {
throw NSError(domain: "Assembly cannot be nil", code: 999, userInfo: nil)
}
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.
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]"
}
}
Im trying to achieve dependency injection with protocols in Swift :)
My system under test calls .rx.signIn() on the dependency, meaning that it has extended Reactive where Base : ConcreteObject.
Is it possible to setup a protocol ready to receive .rx calls?
Any small example or alternative would be greatly appreciated.
I had the same problem during the upgrade from RxSwift2 to RxSwift3. In RxSwift2 I simply used a protocol for the rx-methods in my production code (ex. rx_JSON()) and injected a simple mock object adhering to that protocol in my unit tests.
After a short detour into generics, associated type and type erasure I settled with the following setup, which closely matches my RxSwift2-setup:
For example to hide URLSession as the concrete implementation of my HTTP-calls, I created a protocol RemoteClient:
protocol RemoteClient {
func json(url: URL) -> Observable<Any>
}
Then in my production code I reference only that protocol:
class SomeService {
var remoteClient: RemoteClient
init(remoteClient: RemoteClient) {
self.remoteClient = remoteClient
}
func getSomeData(_ id: String) -> Observable<JSONDictionary> {
let urlString = "..."
return remoteClient
.json(url: URL(string: urlString)!)
.map { data in
guard let show = data as? JSONDictionary else {
throw ParsingError.json
}
return show
}
}
}
In my unit tests I created mock objects which adhere to the RemoteClient protocol:
class RemoteClientMock: RemoteClient {
var output: Any?
var verifyInput: ((URL) -> Void)?
func json(url: URL) -> Observable<Any> {
verifyInput?(url)
return Observable.create { observer in
if let data = self.output {
observer.on(.next(data))
}
observer.on(.completed)
return Disposables.create()
}
}
}
And inject them in the system under test:
class SomeServiceTest: TestCase {
let disposeBag = DisposeBag()
func testGetSomeData() {
// setup
let expectation = self.expectation(description: "request succeeded")
let remoteClientMock = RemoteClientMock()
remoteClientMock.verifyInput = { url in
XCTAssertEqual(URL(string: "some url"), url)
}
remoteClientMock.output = ["id": 4711]
let service = SomeService(remoteClient: remoteClientMock)
// exercise
let observable = service.getSomeData("4711")
// verify
observable
.subscribe(onNext: { data in
XCTAssertEqual(4711, data["id"] as? Int)
expectation.fulfill()
})
.addDisposableTo(disposeBag)
waitForExpectations(timeout: 1, handler: nil)
}
}
So I simply hide the rx-namespace behind my protocol. I do that for now by using an adapter:
let remoteClient = URLSessionRemoteClientAdapter(urlSession: URLSession.shared)
let service = SomeService(remoteClient: remoteClient)
struct URLSessionRemoteClientAdapter: RemoteClient {
let urlSession: URLSession
init(urlSession: URLSession) {
self.urlSession = urlSession
}
func json(url: URL) -> Observable<Any> {
return urlSession.rx.json(url: url)
}
}