Is there anyway I can avoid the use of what appears to be some "boilerplate" code? - swift

I am starting with a function that looks like this:
func getUser(command: APICommand, id: Int, handler: (apiResponse: APIResponse<User>) -> Void ) {
let url = apiPath + "users/\(id)"
Alamofire.request(.GET, url, parameters: requestParameters)
.responseObject { (a:NSURLRequest, b:NSHTTPURLResponse?, c:User?, d:AnyObject?, e:NSError? ) in
let apiResponse = APIResponse(command: command,request: a, response: b, swiftObject: c, rawObject: d, error: e)
if AlamofireAPIRequestRepository.resultIsRetryableError(e, command: command) {
println("retrying request")
command.retry()
} else {
handler(apiResponse: apiResponse)
}
}
}
I am going to have a number of functions that look very similar such as, getUserList() for example.
Looking at this I realized the entire Alamofire.request call is going to be pretty much boiler plate code. The only difference will be the Type of argument c passed into the closure that gets called by the responseObject() method. In this case it is User? , in the getUserList() method it will be UserList?
Is there any way I can make this more generic and avoid what appears to be just "boilerplate" code?
I Here is what I have tried.
func alamofireGetRequest<T>(url: URLStringConvertible, parameters: [String: AnyObject]?,
command: APICommand, handler: (apiResponse: APIResponse<T>) -> Void) -> Void {
Alamofire.request(.GET, url, parameters: parameters)
.responseObject { (a:NSURLRequest, b:NSHTTPURLResponse?, c:T?, d:AnyObject?, e:NSError? ) in
let apiResponse = APIResponse(command: command,request: a, response: b, swiftObject: c, rawObject: d, error: e)
if AlamofireAPIRequestRepository.resultIsRetryableError(e, command: command) {
println("retrying request")
command.retry()
} else {
handler(apiResponse: apiResponse)
}
}
}
but the compiler complains with:
Cannot invoke 'responseObject' with an argument list of type
'((NSURLRequest, NSHTTPURLResponse?, T?, AnyObject?, NSError?) -> _)'
and if I replace the c:T? above with c:User? it is happy.
A comment below referred to this question, which may explain why the solution I tried does not work, but does really answer my intended question as to how to avoid this duplicated code.

I think I found an acceptable solution.
Below is the relevant parts of the code, but basically I end up passing in an argument type of AnyObject in to the closure rather than a generic. Before you say that I am cheating you have to understand that my original call would have looked something like this.
func testGetUserRequestSuccess() {
let getUserCommand=commandFactory.createGetUserCommandUsing(apiRepo, id: 1) {
if let swiftObject = $0.swiftObject {
XCTAssertEqual(swiftObject.id!, 1, "id is correct")
}
self.expectation.fulfill()
}
commandProcessor.processCommand(getUserCommand)
wait(5)
}
now it looks like this:
func testGetUserRequestSuccess() {
let getUserCommand=commandFactory.createGetUserCommandUsing(apiRepo, id: 1) {
if let swiftObject = $0.swiftObject as? User {
XCTAssertEqual(swiftObject.id!, 1, "id is correct")
}
self.expectation.fulfill()
}
commandProcessor.processCommand(getUserCommand)
wait(5)
}
so I had to add the as? User cast to the if let statement, which seems reasonable.
relevant code for anyone interested.
The "specialty" code (what will be repeated for each operation)
func getUser(retriesUse retryProcessor: APICommandProcessor, command: APICommand, id: Int, handler: (apiResponse: APIResponse) -> Void ) {
let url = apiPath + "users/\(id)"
Alamofire.request(.GET, url, parameters: requestParameters)
.responseObject { (a:NSURLRequest, b:NSHTTPURLResponse?, c:User?, d:AnyObject?, e:NSError? ) in
let apiResponse = APIResponse(command: command,request: a, response: b, swiftObject: c, rawObject: d, error: e)
self.finishResponse(command, retryProcessor: retryProcessor, apiResponse: apiResponse, handler: handler)
}
}
The common part that I wanted to factor out (mainly a placeholder at this point)
func finishResponse(command: APICommand, retryProcessor: APICommandProcessor, apiResponse: APIResponse, handler: (apiResponse: APIResponse) -> Void) -> Void {
if AlamofireAPIRequestRepository.resultIsRetryableError(apiResponse.error, retryProcessor: retryProcessor, command: command) {
println("retrying \(command)")
retryProcessor.retryCommand(command)
} else {
println("request completed")
handler(apiResponse: apiResponse)
}
}
a supporting struct used above:
struct APIResponse {
let command: APICommand
let request: NSURLRequest
let response: NSHTTPURLResponse?
let swiftObject: AnyObject?
let rawObject: AnyObject?
let error: NSError?
}

Related

Swift Combine: Cannot refactor repetitive code

My API returns this format, where data can contain all kinds of responses.
{
status: // http status
error?: // error handle
data?: // your response data
meta?: // meta data, eg. pagination
debug?: // debuging infos
}
I have made a Codable Response type with a generic for the optional data, of which we do not know the type.
struct MyResponse<T: Codable>: Codable {
let status: Int
let error: String?
let data: T?
let meta: Paging?
let debug: String?
}
I am now trying to write API convenience methods as concisely as possible. So I have a function to return a generic publisher that I can use for all these responses, i.e. one that pre-parses the response and catches any errors upfront.
First, I get a dataTaskPublisher that processes the parameter inputs, if any. Endpoint is just a convenience String enum for my endpoints, Method is similar. MyRequest returns a URLRequest with some necessary headers etc.
Notice the way I define the parameters: params: [String:T]. This is standard JSON so it could be strings, numbers etc.
It seems this T is the problem somehow..
static fileprivate func publisher<T: Encodable>(
_ path: Endpoint,
method: Method,
params: [String:T] = [:]) throws
-> URLSession.DataTaskPublisher
{
let url = API.baseURL.appendingPathComponent(path.rawValue)
var request = API.MyRequest(url: url)
if method == .POST && params.count > 0 {
request.httpMethod = method.rawValue
do {
let data = try JSONEncoder().encode(params)
request.httpBody = data
return URLSession.shared.dataTaskPublisher(for: request)
}
catch let err {
throw MyError.encoding(description: String(describing: err))
}
}
return URLSession.shared.dataTaskPublisher(for: request)
}
Next, I am parsing the response.
static func myPublisher<T: Encodable, R: Decodable>(
_ path: Endpoint,
method: Method = .GET,
params: [String:T] = [:])
-> AnyPublisher<MyResponse<R>, MyError>
{
do {
return try publisher(path, method: method, params: params)
.map(\.data)
.mapError { MyError.network(description: "\($0)")}
.decode(type: MyResponse<R>.self, decoder: self.agent.decoder)
.mapError { MyError.encoding(description: "\($0)")} //(2)
.tryMap {
if $0.status > 204 {
throw MyError.network(description: "\($0.status): \($0.error!)")
}
else {
return $0 // returns a MyResponse
}
}
.mapError { $0 as! MyError }
//(1)
.eraseToAnyPublisher()
}
catch let err {
return Fail<MyResponse<R>,MyError>(error: err as? MyError ??
MyError.undefined(description: "\(err)"))
.eraseToAnyPublisher()
}
}
Now I can write an endpoint method easily. Here are two examples.
static func documents() -> AnyPublisher<[Document], MyError> {
return myPublisher(.documents)
.map(\.data!)
.mapError { MyError.network(description: $0.errorDescription) }
.receive(on: DispatchQueue.main)
.eraseToAnyPublisher() as AnyPublisher<[Document], MyError>
}
and
static func user() -> AnyPublisher<User, MyError> {
return myPublisher(.user)
.map(\.data!)
.mapError { MyError.network(description: $0.errorDescription) }
.receive(on: DispatchQueue.main)
.eraseToAnyPublisher() as AnyPublisher<User, MyError>
}
All this is working well. Please note that each time, I have to specify my exact return type twice. I think I can live with that.
I should be able to simplify this so that I do not have to repeat the same three operators (map, mapError, receive) in exactly the same way each time.
But when I insert .map(\.data!) at the location //(1) above I get the error Generic parameter T could not be inferred. at the location //(2).
This is really confusing. Why does the generic type in the input parameters play any role here? This must be related to the call to the .decode operator just above, where the generic in question is called R, not T.
Can you explain this? How can I refactor these operators upstream?
This code has a number of small problems. You're right that one is [String: T]. That means that for a given set of parameters, all the values must be of the same type. That's not "JSON." This will accept a [String: String] or a [String: Int], but you can't have both Int and String values in the same dictionary if you do it this way. And it will also accept [String: Document], and it doesn't seem like you really want that.
I'd recommend switching this to just Encodable, which would allow you to pass structs if that were convenient, or Dictionaries if that were convenient:
func publisher<Params: Encodable>(
_ path: Endpoint,
method: Method,
params: Params?) throws
-> URLSession.DataTaskPublisher
func myPublisher<Params: Encodable, R: Decodable>(
_ path: Endpoint,
method: Method = .GET,
params: Params?)
-> AnyPublisher<MyResponse<R>, MyError>
Then modify your params.count to check for nil instead.
Note that I didn't make params = nil a default parameter. That's because this would recreate a second problem you have. T (and Params) can't be inferred in the default case. For = [:], what is T? Swift has to know, even though it's empty. So instead of a default, you use an overload:
func myPublisher<R: Decodable>(
_ path: Endpoint,
method: Method = .GET)
-> AnyPublisher<MyResponse<R>, MyError> {
let params: String? = nil // This should be `Never?`, see https://twitter.com/cocoaphony/status/1184470123899478017
return myPublisher(path, method: method, params: params)
}
Now, when you don't pass any parameters, Params automatically becomes String.
So now your code is fine, and you don't need the as at the end
func documents() -> AnyPublisher<[Document], MyError> {
myPublisher(.documents)
.map(\.data!)
.mapError { MyError.network(description: $0.errorDescription) }
.receive(on: DispatchQueue.main)
.eraseToAnyPublisher() // <== Removed `as ...`
}
Now, that .map(\.data!) makes me sad. If you get back corrupt data from the server, the app will crash. There are lots of good reasons to crash apps; bad server data is never one of them. But fixing that isn't really related to this question (and is a little bit complicated because Failure types other than Error make things hard currently), so I'll leave it for now. My general recommendation is to use Error as your Failure type, and allow unexpected errors to just bubble up rather than wrapping them in an .undefined case. If you need some catch-all "other" anyway, you might as well do that with types ("is") rather than an extra enum case (which just moves the "is" to a switch). At the very least, I would move the Error->MyError mapping as late as possible, which will make handling this much easier.
One more tweak to make later things a little more general, I suspect MyResponse only needs to be Decodable, not Encodable (the rest of this works either way, but it makes it a little more flexible):
struct MyResponse<T: Decodable>: Decodable { ... }
And to your original question of how to make this reusable, you can now pull out a generic function:
func fetch<DataType, Params>(_: DataType.Type,
from endpoint: Endpoint,
method: Method = .GET,
params: Params?) -> AnyPublisher<DataType, MyError>
where DataType: Decodable, Params: Encodable
{
myPublisher(endpoint, method: method, params: params)
.map(\.data!)
.mapError { MyError.network(description: $0.errorDescription) }
.receive(on: DispatchQueue.main)
.eraseToAnyPublisher()
}
// Overload to handle no parameters
func fetch<DataType>(_ dataType: DataType.Type,
from endpoint: Endpoint,
method: Method = .GET) -> AnyPublisher<DataType, MyError>
where DataType: Decodable
{
fetch(dataType, from: endpoint, method: method, params: nil as String?)
}
func documents() -> AnyPublisher<[Document], MyError> {
fetch([Document].self, from: .documents)
}

Swift Generics - Generic parameter 'T' could not be inferred

I am trying to develop a generic API fetch method but I am getting above compilation error. After several hours of trying I am still not able to fix it.
Type Alias
typealias RealmResponse<T> = (Int, Results<T>?, ErrorResponse?) where T:Object, T:Mappable
Class Declaration
class Profile:Object, Mappable{
}
API Fetch Method
#discardableResult
func requestRealmObject<T>(endPoint:String,
disposeBag:DisposeBag,
method:HTTPMethod = .get,
query:[String:Any] = [:],
params:[String:Any] = [:]) -> Observable<RealmResponse<T>> where T:Object, T:Mappable{
let realmResponse:Observable<ObjectResponse<T>> = APIManager.requestObject(endpoint: endPoint,
method: method,
query: query,
params: params)
.shareReplay(1)
realmResponse
.filter{ 200..<300 ~= $0.0 }
.map{ response:ObjectResponse<Profile> -> [Profile] in
if let profile = response.1{
return [profile]
}
return []
}
.subscribe(APIManager.shared.realm.rx.add(update: true, onError: nil))
.disposed(by: disposeBag)
let realmObjects = APIManager.shared.realm.objects(T.self)
return Observable.zip(realmResponse, Observable.collection(from: realmObjects)){ (response, results) in
return (response.0, results, response.2)
}
}
Usage
let profileObserver:Observable<RealmResponse<Profile>> = requestRealmObject(endpoint: APIManager.userProfileEndPoint,
disposeBag:disposeBag,
method: .post,
query:[:],
params:["username":loggedInUser.value])
.shareReplay(1)
Any help in this regard would be highly appreciated.

Swift completion handler - escaping trailing closure

I've been reading quite a few articles now about swift functions with closures, trailing closures and escaping functions. They all seem to give examples which is sufficiently different that I'm not understanding what Im doing wrong with my own function.
My main problem is with how to execute a trailing closure function.
I've created this function to upload an image to firebase. It takes two inputs and are supposed to return a string (imageURL). I do belive this function is ok.
func uploadImageToFirebaseAndReturnImageURL(directory: String, image: UIImage!, handler: #escaping(_ imageURL: (ImageURL)) -> ()) {
let imageName = NSUUID().uuidString // create unique image name
if let uploadData = UIImagePNGRepresentation(image) {
DB_STORE.child(directory).putData(uploadData, metadata: nil, completion: { (metadata, error) in
if error != nil {
print(error)
return
}
if let profileImageUrl = metadata?.downloadURL()?.absoluteString {
let d = ImageURL(imageURL: profileImageUrl)
handler (d)
}
return
})
}
}
My issue is how to execute this function correctly when it comes to the handler.
I want to first execute the function then when complete I want to get the imageURL and use this variable into another nested function that upload this variable(String) into a firebase database.
uploadImageToFirebaseAndReturnImageURL(directory: "profileImage", image: selectedImageFromPicker!, handler: { imageURL in
guard let uid = Auth.auth().currentUser.uid else { print("User is not logged in"); return }
DataService.instance.updateUserWithProfileImageURL(uid: uid, imageURL: imageURL)
print("")
}
What am I doing wrong?
To pass a trailing closure you need to end/close your function call and omit the closure argument label. For instance:
func foo(first: Int, second: Int, handler: (Int) -> Void) {
...
}
call syntax:
foo(first: 10, second: 20) { result in
/* trailing closure body */
}
By the way, you should simply your handler argument declaration from:
handler: #escaping (_ imageURL: (ImageURL)) -> ()
to this:
handler: #escaping (ImageURL) -> Void
Using Void or () is matter of style since they are logically the same. I prefer the former ;)

Generic perform Request, using Generics

I would like to make a perform request function in swift using Generics. I want to make the call and switch on my enum Result based on what I get back. However, I don't understand the : 'cannot invoke performRequest with an argument list of type (NSURLRequest, (Result<__>) -> ())' Why can't I have an unnamed parameter here? I have also tried something like the following : r<MyStruct> --- but I then get an expected expression error. Any help explaining the above Result<_> error would be greatly appreciated. Thanks.
enum Result<A> {
case Value
case Error
}
func performRequest<A>(request:NSURLRequest, callback:(Result<A>) -> ()) {
let task = NSURLSession.sharedSession().dataTaskWithRequest(request) { (data, response, error) -> Void in
callback(parseResponse(data, response: response, error: error))
}
task.resume()
}
class SampleClass {
let request = NSURLRequest(URL: NSURL(string: "www.google.com")!)
init() {
performRequest(request) { r in -------- errors out
switch r {
case .Value:
case .Error:
}
}
}
The problem is that when you use performRequest, you have not given the compiler enough information about the generic parameter you intend to use. The critical part that is missing is that parseResponse needs to return a Result that is parameterised in the same way as the callback. However, in the snippet you provided, parseResponse is not generic.
I believe this will do what you intend. In this scenario, I've parameterised the Result with String, but you can substitute any other type.
// multi-purpose (generic) Result type
enum Result<A>
{
case Value(A) // because you parameterised the enum, you might as well take advantage of the type
case Error
}
// this is a custom parser, you may substitute your own that returns a different type
func parseString( data:NSData?, response:NSURLResponse?, error:NSError? ) -> Result<String> {
if let _ = error {
return Result.Error
}
return Result.Value("Success")
}
// this function is completely generic, but the parser and callback need to be compatible
func performRequest<A>( request:NSURLRequest,
parser:( NSData?, NSURLResponse?, NSError? ) -> Result<A>,
callback:( Result<A> ) -> Void ) {
let task = NSURLSession.sharedSession().dataTaskWithRequest(request) {
( data, response, error ) -> Void in
callback( parser( data, response, error ) )
}
task.resume()
}
let request = NSURLRequest(URL: NSURL(string: "www.google.com")!)
// actual invocation, now I need to pass in a concrete parser and callback with a specific type
performRequest( request, parser: parseString ) { // parseString returns a Result<String>
r in
switch r {
case .Value( let value ):
// because I passed in a parser that returns a Result<String>, I know that "value" is a String here
print( "Succeeded with value: \(value)" )
break;
case .Error:
print( "an error occurred" )
break;
}
}

Swift closure with Alamofire

I am making API calls to a server. I am leveraging Alamofire to handle this. I'm trying to create a function that uses Alamofire's GET function to return an object based on a custom class that holds the various outputs provided by this GET function.
It's not clear to me the way in which to do this.
Here's my custom class that will hold details about the response:
import Foundation
class ResponsePackage {
var success = false
var response: AnyObject? = nil
var error: NSError? = nil
}
In another class I have the following function:
func get(apiEndPoint: NSString) -> ResponsePackage {
let responsePackage = ResponsePackage()
Alamofire
.request(.GET, apiEndPoint)
.responseJSON {(request, response, JSON, error) in
responsePackage.response = JSON
responsePackage.success = true
responsePackage.error = error
}
return responsePackage
}
This returns nil as the call to the server is not complete before the return gets executed. I know that I should be able to do this with closures, but I am not sure how to construct this.
The code between the {} is the equivalent of block in objective-C : this is a chunk of code that gets executed asynchronously.
The error you made is where you put your return statement : when you launch your request, the code in {} is not executed until the framework received a response, so when the return statement is reached, chances are, there is still no response. You could simply move the line :
return responsePackage
inside the closure, so the func return only when it has received a response. This is a simple way, but it's not really optimal : your code will get stuck at waiting for the answers. The best way you can do this is by using closure, too. This would look something like :
func get(apiEndPoint: NSString, completion: (response: ResponsePackage) -> ()) -> Bool {
let responsePackage = ResponsePackage()
Alamofire
.request(.GET, apiEndPoint)
.responseJSON {(request, response, JSON, error) in
responsePackage.response = JSON
responsePackage.success = true
responsePackage.error = error
completion(response: responsePackage)
}
}
I make an example follow your question about responseJSON with closures:
Follow this little steps:
First of all you can create your custom types in a general class (for example a Constants.swift class):
typealias apiSuccess = (result: NSDictionary?) -> Void
typealias apiProgress = (result: NSDictionary?) -> Void // when you want to download or upload using Alamofire..
typealias apiFailure = (error: NSDictionary?) -> Void
Then in your class:
// Normal http request with JSON response..
func callJSONrequest(url:String, params:[String: AnyObject]?, success successBlock :apiSuccess,
failure failureBlock :apiFailure) {
Alamofire.request(.GET, url, parameters: params, encoding: ParameterEncoding.URL)
.responseJSON { response in
print("\(response.request?.URL)") // original URL request
//print(response.response) // URL response
//print(response.data) // server data
//print(response.result) // result of response serialization
if response.result.isSuccess {
let jsonDic = response.result.value as! NSDictionary
successBlock(result: jsonDic)
} else {
let httpError: NSError = response.result.error!
let statusCode = httpError.code
let error:NSDictionary = ["error" : httpError,"statusCode" : statusCode]
failureBlock(error: error)
}
}
}
}
func myCommonFunction() {
let myApiSuccess: apiSuccess = {(result: NSDictionary?) -> Void in
print ("Api Success : result is:\n \(result)")
// Here you can make whatever you want with result dictionary
}
let myApiFailure: apiFailure = {(error: NSDictionary?) -> Void in
print ("Api Failure : error is:\n \(error)")
// Here you can check the errors with error dictionary looking for http error type or http status code
}
var params :[String: AnyObject]?
let name : String! = "this is my name"
let id : String! = "000a"
params = ["name" : name, "id" : id]
let url : String! = "https://etc..."
callJSONrequest(url, params:params, success: myApiSuccess, failure: myApiFailure)
}