Correct memory management in closures - ios

I have method with closure.
It works like this
func ping() -> pingProtocol {
let factory = pingFactory()
let ping = factory.getPing() // returns pingProtocol conforming instance, for example "PingService"
return ping
}
func performPing(success: #escaping () -> (), fail: #escaping () -> ()) {
ping().status { (ready, response) in
if ready {
success()
} else {
fail()
}
}
}
Realisation of ping is abstraction over Alamofire.
class PingService {
func status(completion: #escaping (Bool, ServiceResponse) -> Void) {
doSimpleRequest(endPoint: "/api/get_state/", param: [:], completion: completion)
}
func doSimpleRequest(endPoint: String, param: [String: Any], completion: #escaping (Bool, ServiceResponse) -> Void) {
APIManager.shared.postRequest(mode: APIMode.Service, endPoint: endPoint, parameters: param) { [weak self] (response, error) in
guard let strongSelf = self else {
return
}
guard let response = response as? [String: Any] else { return }
let serviceResponse = ServiceResponse(dict: response)
if error != nil {
completion(false, serviceResponse)
return
}
completion(true, serviceResponse)
}
}
}
The problem is that with weak self guard in self is always triggers. If I use unowned, I get crash. If I dont use weak or unowned it works fine.
The ping method itself lives in viewmodel of controller, and controller stays on screen when completion block is needed to be executed, so the problem is in deallocation of ping instance, not in viewmodel thats holds it.
If I use capture list I dont get desired behavior, but If I do I suspect that I get memory leak.
What is best practice here ?

Related

How can I chain completion handlers if one method also has a return value?

I have 2 methods I need to call, the second method must be executed using the result of the first method and the second method also returns a value.
I have put together a simple playground that demonstrates a simple version of the flow
import UIKit
protocol TokenLoader {
func load(_ key: String, completion: #escaping (String?) -> Void)
}
protocol Client {
func dispatch(_ request: URLRequest, completion: #escaping (Result<(Data, HTTPURLResponse), Error>) -> Void) -> URLSessionTask
}
class AuthTokenLoader: TokenLoader {
func load(_ key: String, completion: #escaping (String?) -> Void) {
print("was called")
completion("some.access.token")
}
}
class Networking: Client {
private let loader: TokenLoader
init(loader: TokenLoader) {
self.loader = loader
}
func dispatch(_ request: URLRequest, completion: #escaping (Result<(Data, HTTPURLResponse), Error>) -> Void) -> URLSessionTask {
let task = URLSession.shared.dataTask(with: request, completionHandler: { data, response, error in
if let error = error {
completion(.failure(error))
} else if let data = data, let response = response as? HTTPURLResponse {
completion(.success((data, response)))
}
})
task.resume()
return task
}
}
let loader = AuthTokenLoader()
let client = Networking(loader: loader)
let request = URL(string: "https://jsonplaceholder.typicode.com/todos/1")!
client.dispatch(.init(url: request), completion: { print($0) })
I need to use the token returned by AuthTokenLoader as a header on the request sent by dispatch method in my Networking class.
Networking also returns a task so this request can be cancelled.
As I cannot return from inside the completion block of the AuthTokenLoader load completion, I unsure how to achieve this.
You can create a wrapper for your task and return that instead.
protocol Task {
func cancel()
}
class URLSessionTaskWrapper: Task {
private var completion: ((Result<(Data, HTTPURLResponse), Error>) -> Void)?
var wrapped: URLSessionTask?
init(_ completion: #escaping (Result<(Data, HTTPURLResponse), Error>) -> Void) {
self.completion = completion
}
func complete(with result: Result<(Data, HTTPURLResponse), Error>) {
completion?(result)
}
func cancel() {
preventFurtherCompletions()
wrapped?.cancel()
}
private func preventFurtherCompletions() {
completion = nil
}
}
Your entire playground would become
protocol TokenLoader {
func load(_ key: String, completion: #escaping (String?) -> Void)
}
protocol Client {
func dispatch(_ request: URLRequest, completion: #escaping (Result<(Data, HTTPURLResponse), Error>) -> Void) -> Task
}
class AuthTokenLoader: TokenLoader {
func load(_ key: String, completion: #escaping (String?) -> Void) {
print("was called")
DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
completion("some.access.token")
}
}
}
protocol Task {
func cancel()
}
class URLSessionTaskWrapper: Task {
private var completion: ((Result<(Data, HTTPURLResponse), Error>) -> Void)?
var wrapped: URLSessionTask?
init(_ completion: #escaping (Result<(Data, HTTPURLResponse), Error>) -> Void) {
self.completion = completion
}
func complete(with result: Result<(Data, HTTPURLResponse), Error>) {
completion?(result)
}
func cancel() {
preventFurtherCompletions()
wrapped?.cancel()
}
private func preventFurtherCompletions() {
completion = nil
}
}
class Networking: Client {
private let loader: TokenLoader
init(loader: TokenLoader) {
self.loader = loader
}
func dispatch(_ request: URLRequest, completion: #escaping (Result<(Data, HTTPURLResponse), Error>) -> Void) -> Task {
let task = URLSessionTaskWrapper(completion)
loader.load("token") { token in
task.wrapped = URLSession.shared.dataTask(with: request, completionHandler: { data, response, error in
if let error = error {
task.complete(with: .failure(error))
} else if let data = data, let response = response as? HTTPURLResponse {
task.complete(with: .success((data, response)))
}
})
task.wrapped?.resume()
}
return task
}
}
let loader = AuthTokenLoader()
let client = Networking(loader: loader)
let request = URL(string: "https://jsonplaceholder.typicode.com/todos/1")!
client.dispatch(.init(url: request), completion: { print($0) })
Turns out it was harder to do in Combine than I thought. Like most people, I'm still quite new to this. Would happily accept edits from people who know better :)
The general principle is that, instead of taking a completion block, your functions should return a Publisher, that you can then choose to do things with, and chain together.
So your token loader can look like this:
protocol TokenLoader {
func load(_ key: String) -> AnyPublisher<String, Error>
}
Instead of taking a completion block, you now return a publisher which will send you a string on success, or an error if something goes wrong.
And your implementation, well I wasn't sure what you were planning on doing in there but here's an example of sorts:
class AuthTokenLoader: TokenLoader {
func load(_ key: String) -> AnyPublisher<String, Error> {
print("was called")
// Do async stuff to create your token, ending in a result
let result = Result<String, Error>.success("some.access.token")
return result.publisher.eraseToAnyPublisher()
}
}
Your client can look like this:
protocol Client {
func dispatch(_ request: URLRequest) -> AnyPublisher<Data, Error>
}
Now this is the complicated bit. What you want to do is take the publisher from your token loader, and when it gives a result, make your URL request, and then make another publisher from that URL request. URLSession can give you a publisher for a data task, and there is a flatMap operator which is supposed to allow you to turn the results of one publisher into a new publisher, but you get stuck in the weeds of the type system so the code is uglier than it ought to be:
func dispatch(_ request: URLRequest) -> AnyPublisher<Data, Error> {
return loader.load("someKey")
.flatMap {
token -> AnyPublisher<Data, Error> in
var finalRequest = request
finalRequest.setValue(token, forHTTPHeaderField: "x-token")
return URLSession.shared.dataTaskPublisher(for: finalRequest)
.map { $0.data }
.mapError { $0 as Error }
.eraseToAnyPublisher()
}.eraseToAnyPublisher()
}
You'd use this code like so:
let request = URLRequest(url: URL(string: "https://jsonplaceholder.typicode.com/todos/1")!)
let sub = client.dispatch(request)
.sink(receiveCompletion: {
switch $0 {
case .finished: print("Done")
case .failure(let error): print("Error :\(error)")
}
}, receiveValue: { data in
print("Data: \(data)")
})
sub is an AnyCancellable, so if it is deallocated or you call cancel on it, this passes back up the chain and it will cancel the URL task for you.
If you want to do things with the data then there are operators for mapping or decoding or whatever which makes the whole thing very nice to work with.

Alamofire 5.0.0-rc.3 RequestInterceptor Adapt method not being called of Alamofire although retry gets called when there is any error in response

Alamofire 5.0.0-rc.3 RequestInterceptor Adapt method not being called of Alamofire although retry gets called when there is any error in response.
Method:
func adapt(_ urlRequest: URLRequest, for session: Session, completion: #escaping (AFResult<URLRequest>) -> Void) {
}
class Interceptor: RequestInterceptor {
func adapt(_ urlRequest: URLRequest, for session: Session, completion: #escaping (AFResult<URLRequest>) -> Void) {
print("ADAPT :=")
completion(.success(urlRequest))
}
func retry(_ request: Request, for session: Session, dueTo error: Error, completion: #escaping (RetryResult) -> Void) {
print("RETRY :=")
completion(.doNotRetry)
}
}
Network Request method which is in different class:
public func request<T: Codable> (_ urlConvertible: URLRequestConvertible) -> Observable<T> {
return Observable<T>.create { observer in
// 1
print("Url := \(urlConvertible.urlRequest!.url!)")
// 2
let request = AF.request(urlConvertible, interceptor: Interceptor()).responseDecodable { (response: AFDataResponse<T>) in
if let data = response.data{
print("Response := \(String(decoding: data, as: UTF8.self))")
}
else{
print("data is nil")
}
switch response.result {
case .success(let value):
print("value :-> \(value)")
observer.onNext(value)
observer.onCompleted()
case .failure(let error):
observer.onError(error)
}
}
//Finally, we return a disposable to stop the request
return Disposables.create {
request.cancel()
}
}
}
See here :
https://github.com/Alamofire/Alamofire/issues/2998
The function is not called because there is an ambiguity...
In the Interceptor :
Add this :
typealias AdapterResult = Swift.Result<URLRequest, Error>
And replace in the "adapt" method, the #escaping parameter by this :
#escaping (RetryResult)
It should works.

How to mock URL struct?

I am writing some code to be able to make testing easier. After researching, I found a good way of making URLSession testable is to make it conform to a protocol and use that protocol in the class I need to test. I applied the same method to URL. However, I now need to downcast the url parameter as a URL type. This seems a bit "dangerous" to me. What is the proper way to make URL testable? How can I also mock the URLSessionDataTask type returned by dataTask?
import Foundation
protocol URLSessionForApiRequest {
func dataTask(with url: URL, completionHandler: #escaping (Data?, URLResponse?, Error?) -> Void) -> URLSessionDataTask
}
protocol URLForApiRequest {
init?(string: String)
}
extension URLSession: URLSessionForApiRequest {}
extension URL: URLForApiRequest {}
class ApiRequest {
class func makeRequest(url: URLForApiRequest, urlSession: URLSessionForApiRequest) {
guard let url = url as? URL else { return }
let task = urlSession.dataTask(with: url) { _, _, _ in print("DONE") }
task.resume()
}
}
You want to make a protocol that wraps the function that you are going to call, then make a concrete implementation and mock implementation that returns whatever you initialize it with. Here is an example:
import UIKit
import PlaygroundSupport
protocol RequestProvider {
func request(from: URL, completion: #escaping (Data?, URLResponse?, Error?) -> Void)
}
class ApiRequest: RequestProvider {
func request(from url: URL, completion: #escaping (Data?, URLResponse?, Error?) -> Void) {
URLSession.shared.dataTask(with: url, completionHandler: completion).resume
}
}
class MockApiRequest: RequestProvider {
enum Response {
case value(Data, URLResponse), error(Error)
}
private let response: Response
init(response: Response) {
self.response = response
}
func request(from url: URL, completion: #escaping (Data?, URLResponse?, Error?) -> Void) {
switch response {
case .value(let data, let response):
completion(data, response, nil)
case .error(let error):
completion(nil, nil, error)
}
}
}
class SomeClassThatMakesAnAPIRequest {
private let requestProvider: RequestProvider
init(requestProvider: RequestProvider) {
self.requestProvider = requestProvider
}
//Use the requestProvider here and it uses either the Mock or the "real" API provider based don what you injected
}

Why am I getting this error: Ambiguous reference to member 'fetch(with:parse:completion:)'

I am using Xcode 8.3.3. I am getting a Swift Compiler Error "Ambiguous reference to member". I have gone over the code and can't seem to figure it out.
protocol APIClient {
var session: URLSession { get }
func fetch<T: JSONDecodable>(with request: URLRequest, parse: #escaping (JSON) -> T?, completion: #escaping (Result<T, APIError>) -> Void)
func fetch<T: JSONDecodable>(with request: URLRequest, parse: #escaping (JSON) -> [T], completion: #escaping (Result<[T], APIError>) -> Void)
}
fetch(with: request, parse: { json -> [YelpBusiness] in
guard let businesses = json["businesses"] as? [[String: Any]] else { return [] }
return businesses.flatMap { YelpBusiness(json: $0) }
}, completion: completion)
https://github.com/jripke74/RestaurantReviews.git
I checked your code. You have created a protocol which confirms the class YelpClient. What's wrong?
fetch(with: request, parse: { json -> [YelpBusiness] in
guard let businesses = json["businesses"] as? [[String: Any]] else { return [] }
return businesses.flatMap { YelpBusiness(json: $0) }
}, completion: completion)
Using above code you are directly calling the protocol without any delegate. The problem is you need to inherit the protocol in the YelpClient class like below.
func fetch<T>(with request: URLRequest, parse: #escaping (APIClient.JSON) -> T?, completion: #escaping (Result<T, APIError>) -> Void) where T : JSONDecodable {
//Code
}
func fetch<T>(with request: URLRequest, parse: #escaping (APIClient.JSON) -> [T], completion: #escaping (Result<[T], APIError>) -> Void) where T : JSONDecodable {
//Code
}
For more details refer to apple documentation
Updated:
Check the below screenshot for better idea

DispatchGroup returning multiple times

Below code which I am using to make concurrent API call. somehow this method returning multiple times. I have tested without DispatchGroup, It is working as expected. Help me to find why it is calling multiple times.
My Code Snippet :
func makeConcurrentCallForUpdating(_ parent: Parent,
completionBlock: #escaping (_ success: Bool, _ error: DescriptiveErrorType?) -> Void)
let fetchGroup = DispatchGroup()
let queue = DispatchQueue.global(qos: .default)
let endPoints = [.email, .others ]
DispatchQueue.concurrentPerform(iterations: endPoints.count) { (index) in
let enumType = endPoints[index]
switch enumType {
case .email:
updateEmail(parent, fetchGroup: fetchGroup, completionBlock: completionBlock)
case .others:
update(parent, fetchGroup: fetchGroup, completionBlock: completionBlock)
default:
break
}
}
fetchGroup.notify(queue: queue) {
if self.endPoints.count > 0 {
completionBlock(false, error)
} else {
self.saveUpdated(parent, completionBlock: completionBlock)
}
}
}
#MARK: EMAIL CALL
fileprivate func updateEmail(_ parent: Parent,
fetchGroup: DispatchGroup,
completionBlock: #escaping (_ success: Bool, _ error: DescriptiveErrorType?) -> Void) {
fetchGroup.enter()
updateEmail(parent: parent) { (success, error) in
fetchGroup.leave()
}
}
You need to enter() all dispatched tasks before any task in the group will leave().
Move your fetchGroup.enter() before DispatchQueue.concurrentPerform(...
let endPoints = [.email, .others ]
endPoints.forEach {_ in fetchGroup.enter()} //<- add this line
DispatchQueue.concurrentPerform(iterations: endPoints.count) { (index) in
And remove fetchGroup.enter() in each task:
fileprivate func updateEmail(_ parent: Parent,
fetchGroup: DispatchGroup,
completionBlock: #escaping (_ success: Bool, _ error: DescriptiveErrorType?) -> Void) {
//fetchGroup.enter() //<- remove this line, in your `update(...)` as well
updateEmail(parent: parent) { (success, error) in
fetchGroup.leave()
}
}
Please try.

Resources