I'm wondering if there is a way to implement reconnection mechanism with new Apple framework Combine and use of URLSession publisher
tried to find some examples in WWDC 2019
tried to play with waitsForConnectivity with no luck (it even not calling delegate on custom session)
tried URLSession.background but it crashed during publishing.
I'm also not understanding how do we track progress in this way
Does anyone already tried to do smth like this?
upd:
It seems like waitsForConnectivity is not working in Xcode 11 Beta
upd2:
Xcode 11 GM - waitsForConnectivity is working but ONLY on device. Use default session, set the flag and implement session delegate. Method task is waiting for connectivity will be invoked no matter if u r using init task with callback or without.
public class DriverService: NSObject, ObservableObject {
public var decoder = JSONDecoder()
public private(set) var isOnline = CurrentValueSubject<Bool, Never>(true)
private var subs = Set<AnyCancellable>()
private var base: URLComponents
private lazy var session: URLSession = {
let config = URLSessionConfiguration.default
config.waitsForConnectivity = true
return URLSession(configuration: config, delegate: self, delegateQueue: nil)
}()
public init(host: String, port: Int) {
base = URLComponents()
base.scheme = "http"
base.host = host
base.port = port
super.init()
// Simulate online/offline state
//
// let pub = Timer.publish(every: 3.0, on: .current, in: .default)
// pub.sink { _ in
// let rnd = Int.random(in: 0...1)
// self.isOnline.send(rnd == 1)
// }.store(in: &subs)
// pub.connect()
}
public func publisher<T>(for driverRequest: Request<T>) -> AnyPublisher<T, Error> {
var components = base
components.path = driverRequest.path
var request = URLRequest(url: components.url!)
request.httpMethod = driverRequest.method
return Future<(data: Data, response: URLResponse), Error> { (complete) in
let task = self.session.dataTask(with: request) { (data, response, error) in
if let err = error {
complete(.failure(err))
} else {
complete(.success((data!, response!)))
}
self.isOnline.send(true)
}
task.resume()
}
.map({ $0.data })
.decode(type: T.self, decoder: decoder)
.eraseToAnyPublisher()
}
}
extension DriverService: URLSessionTaskDelegate {
public func urlSession(_ session: URLSession, taskIsWaitingForConnectivity task: URLSessionTask) {
self.isOnline.send(false)
}
}
Have you tried retry(_:) yet? It’s available on Publishers and reruns the request upon failure.
If you don’t want the request to immediately rerun for all failures then you can use catch(_:) and decide which failures warrant a rerun.
Here's some code to achieve getting the progress.
enum Either<Left, Right> {
case left(Left)
case right(Right)
var left: Left? {
switch self {
case let .left(value):
return value
case .right:
return nil
}
}
var right: Right? {
switch self {
case let .right(value):
return value
case .left:
return nil
}
}
}
extension URLSession {
func dataTaskPublisherWithProgress(for url: URL) -> AnyPublisher<Either<Progress, (data: Data, response: URLResponse)>, URLError> {
typealias TaskEither = Either<Progress, (data: Data, response: URLResponse)>
let completion = PassthroughSubject<(data: Data, response: URLResponse), URLError>()
let task = dataTask(with: url) { data, response, error in
if let data = data, let response = response {
completion.send((data, response))
completion.send(completion: .finished)
} else if let error = error as? URLError {
completion.send(completion: .failure(error))
} else {
fatalError("This should be unreachable, something is clearly wrong.")
}
}
task.resume()
return task.publisher(for: \.progress.completedUnitCount)
.compactMap { [weak task] _ in task?.progress }
.setFailureType(to: URLError.self)
.map(TaskEither.left)
.merge(with: completion.map(TaskEither.right))
.eraseToAnyPublisher()
}
}
I read your question title several times. If you mean reconnect the URLSession's publisher. Due to the URLSession.DataTaskPublisher has two results. Success output or Failure (a.k.a URLError). It's not possible to make it reconnect after the output produced.
You can declare one subject. e.g
let output = CurrentValueSubject<Result<T?, Error>, Never>(.success(nil))
And add a trigger when network connection active then request resources and send the new Result to the output. Subscribe output in the other place. So that you can get new value when network back-online.
Related
I'm trying to write some unit tests for my API using URLSession.DataTaskPublisher. I've found an already existing question on Stackoverflow for the same but I'm struggling to implement a working class using the proposed solution.
Here's the existing question: How to mock URLSession.DataTaskPublisher
protocol APIDataTaskPublisher {
func dataTaskPublisher(for request: URLRequest) -> URLSession.DataTaskPublisher
}
class APISessionDataTaskPublisher: APIDataTaskPublisher {
func dataTaskPublisher(for request: URLRequest) -> URLSession.DataTaskPublisher {
return session.dataTaskPublisher(for: request)
}
var session: URLSession
init(session: URLSession = URLSession.shared) {
self.session = session
}
}
class URLSessionMock: APIDataTaskPublisher {
func dataTaskPublisher(for request: URLRequest) -> URLSession.DataTaskPublisher {
// How can I return a mocked URLSession.DataTaskPublisher here?
}
}
My API then uses the above like this:
class MyAPI {
/// Shared URL session
private let urlSession: APIDataTaskPublisher
init(urlSession: APIDataTaskPublisher = APISessionDataTaskPublisher(session: URLSession.shared)) {
self.urlSession = urlSession
}
}
What I don't know is how to implement URLSessionMock.dataTaskPublisher().
It would probably be simpler not to mock DataTaskPublisher. Do you really care if the publisher is a DataTaskPublisher? Probably not. What you probably care about is getting the same Output and Failure types as DataTaskPublisher. So change your API to only specify that:
protocol APIProvider {
typealias APIResponse = URLSession.DataTaskPublisher.Output
func apiResponse(for request: URLRequest) -> AnyPublisher<APIResponse, URLError>
}
Conform URLSession to it for production use:
extension URLSession: APIProvider {
func apiResponse(for request: URLRequest) -> AnyPublisher<APIResponse, URLError> {
return dataTaskPublisher(for: request).eraseToAnyPublisher()
}
}
And then your mock can create the publisher in any way that's convenient. For example:
struct MockAPIProvider: APIProvider {
func apiResponse(for request: URLRequest) -> AnyPublisher<APIResponse, URLError> {
let response = HTTPURLResponse(url: request.url!, statusCode: 200, httpVersion: "HTTP/1.1", headerFields: nil)!
let data = "Hello, world!".data(using: .utf8)!
return Just((data: data, response: response))
.setFailureType(to: URLError.self)
.eraseToAnyPublisher()
}
}
If you store in UT bundle stub JSON (XML, or something) for every API call that you want to test then the simplest mocking code might look as following
class URLSessionMock: APIDataTaskPublisher {
func dataTaskPublisher(for request: URLRequest) -> URLSession.DataTaskPublisher {
// here might be created a map of API URLs to cached stub replies
let stubReply = request.url?.lastPathComponent ?? "stub_error"
return URLSession.shared.dataTaskPublisher(for: Bundle(for: type(of: self)).url(forResource: stubReply, withExtension: "json")!)
}
}
so instead call to network server your publisher is created with URL of locally stored resource with known data, so you can verify all your workflow.
I will develop the step from having a simple Get request, to mocking .dataTaskPublisher for Combine and for the last part, testing the call. It is a ready to use code, for everyone in case someone else would need it.
Follow the comment to add your model or anything that depends on your project data.
So this is the protocol that give the rules to my NetworkRequest class:
protocol NetworkRequestProtocol {
associatedtype Resource
var resourceURL: NetworkEndpoint { get set }
var resourceSession: URLSession { get set }
func download() -> AnyPublisher<Resource, NetworkError>
}
There is some custom class, NetworkEndpoint and NetworkError, you can add your own here if you want or use URL and URLError instead:
enum NetworkEndpoint {
static let baseURL = URL(string: "API_BASE_URL")! // Add your api base url here
case live
var url: URL {
switch self {
case .live:
return NetworkEndpoint.baseURL!.appendingPathComponent("END_OR_YOUR_API_URL") // Add the end of your API url here
}
}
}
enum NetworkError: LocalizedError {
case addressUnreachable(URL)
case invalidResponse
var errorDescription: String? {
switch self {
case .invalidResponse:
return "The server response is invalid."
case .addressUnreachable(let url):
return "\(url.absoluteString) is unreachable."
}
}
}
Now, I am creating the NetworkRequest class to handle the API call. RessourceSession initializer is used for the UnitTest part only:
final class NetworkRequest<Resource> where Resource: Codable {
var resourceURL: NetworkEndpoint
var resourceSession: URLSession
init(_ resourceURL: NetworkEndpoint,
resourceSession: URLSession = URLSession(configuration: .default)) {
self.resourceURL = resourceURL
self.resourceSession = resourceSession
}
// MARK: - Dispatch Queues
let downloadQueue = DispatchQueue(
label: "downloadQueue", qos: .userInitiated,
attributes: .concurrent, autoreleaseFrequency: .inherit, target: .main)
}
// MARK: - Network Requests
extension NetworkRequest: NetworkRequestProtocol {
func download() -> AnyPublisher<Resource, NetworkError> {
resourceSession
.dataTaskPublisher(for: resourceURL.url)
.receive(on: downloadQueue)
.map(\.data)
.decode(type: Resource.self, decoder: JSONDecoder())
.mapError { error -> NetworkError in
switch error {
case is URLError:
return .addressUnreachable(self.resourceURL.url)
default:
return .invalidResponse }}
.eraseToAnyPublisher()
}
}
For the production code, this is an example of use of the NetworkRequest class, and of course, your model must be Codable:
var subscriptions = Set<AnyCancellable>()
func downloadData() {
NetworkRequest<YOUR_MODEL_NAME>(.live).download() // Add your model name inside the brackets
.sink(
receiveCompletion: { completion in
switch completion {
case .failure(let error):
print(error)
case .finished:
break }},
receiveValue: { data in
print(data) })
.store(in: &subscriptions)
}
So now that all the code is setup in the project, we can pass to the UnitTest part and start mocking URLSession:
class MockURLSession: URLSession {
var data: Data?
var response: URLResponse?
var error: Error?
init(data: Data?, response: URLResponse?, error: Error?) {
self.data = data
self.response = response
self.error = error
}
override func dataTask(with request: URLRequest,
completionHandler: #escaping (Data?, URLResponse?, Error?) -> Void) -> URLSessionDataTask {
let data = self.data
let response = self.response
let error = self.error
return MockURLSessionDataTask {
completionHandler(data, response, error)
}
}
}
Now, we mock URLSessionDataTask that we return when overriding dataTask in MockURLSession, and it will work for .dataTaskPublisher:
class MockURLSessionDataTask: URLSessionDataTask {
private let closure: () -> Void
init(closure: #escaping () -> Void) {
self.closure = closure
}
override func resume() {
closure()
}
}
We create fake response data to pass into our tests, but you must create a .json file with your data in it to fetch them in the tests:
class FakeResponseData {
static let response200OK = HTTPURLResponse(url: URL(string: "https://test.com")!,
statusCode: 200,
httpVersion: nil,
headerFields: nil)!
static let responseKO = HTTPURLResponse(url: URL(string: "https://test.com")!,
statusCode: 500,
httpVersion: nil,
headerFields: nil)!
class RessourceError: Error {}
static let error = RessourceError()
static var correctData: Data {
let bundle = Bundle(for: FakeResponseData.self)
let fakeJsonURL = bundle.url(forResource: "FAKE_JSON_FILE_NAME", withExtension: "json") // Add your fake json file name in here
let fakeJsonData = try! Data(contentsOf: fakeJsonURL!)
return fakeJsonData
}
static let incorrectData = "error".data(using: .utf8)!
}
And to finish, this is the part where you test your NetworkRequest, with the fake data coming from the .json file, or the error. You use resourceSession initializer to add your MockURLSession here and avoid making real network call:
class NetworkRequestTests: XCTestCase {
var expectation: XCTestExpectation!
var subscriptions: Set<AnyCancellable>!
override func setUpWithError() throws {
try super.setUpWithError()
expectation = XCTestExpectation(description: "wait for queue change")
subscriptions = Set<AnyCancellable>()
}
override func tearDownWithError() throws {
subscriptions = nil
expectation = nil
try super.tearDownWithError()
}
func testNetworkRequest_mockURLSessionAddCorrectDataResponse_returnRatesDataModelValues() throws {
let expectedTestValue = "test" // This value is set in your .json fake data for testing
// This is where you use resourceSession to pass your fake data
let networkRequest = NetworkRequest<RatesData>(.live, resourceSession:
MockURLSession(data: FakeResponseData.correctData,
response: FakeResponseData.response200OK,
error: nil))
networkRequest.download()
.sink(
receiveCompletion: { completion in
self.expectation.fulfill() },
receiveValue: { value in
XCTAssertEqual(expectedTimestamp, value.InFakeJson) // Compare with your fake json file
})
.store(in: &subscriptions)
wait(for: [expectation], timeout: 0.1)
}
func testNetworkRequest_mockURLSessionAddServerErrorAsResponse_returnNetworkErrorInvalidResponse() throws {
let expectedNetworkError = NetworkError.invalidResponse.localizedDescription
// This is where you use resourceSession to pass your fake data
let networkRequest = NetworkRequest<RatesData>(.live, resourceSession:
MockURLSession(data: nil,
response: FakeResponseData.responseKO,
error: nil))
networkRequest.download()
.sink(
receiveCompletion: { completion in
switch completion {
case .failure(let error):
XCTAssertEqual(expectedNetworkError, error.localizedDescription)
case .finished:
break
}
self.expectation.fulfill() },
receiveValue: { value in
XCTAssertNil(value)
})
.store(in: &subscriptions)
wait(for: [expectation], timeout: 0.1)
}
}
Answered on original question, but will repost here:
Since DataTaskPublisher uses the URLSession it is created from, you can just mock that. I ended up creating a URLSession subclass, overriding dataTask(...) to return a URLSessionDataTask subclass, which I fed with the data/response/error I needed...
class URLSessionDataTaskMock: URLSessionDataTask {
private let closure: () -> Void
init(closure: #escaping () -> Void) {
self.closure = closure
}
override func resume() {
closure()
}
}
class URLSessionMock: URLSession {
var data: Data?
var response: URLResponse?
var error: Error?
override func dataTask(with request: URLRequest, completionHandler: #escaping (Data?, URLResponse?, Error?) -> Void) -> URLSessionDataTask {
let data = self.data
let response = self.response
let error = self.error
return URLSessionDataTaskMock {
completionHandler(data, response, error)
}
}
}
Then obviously you just want your networking layer using this URLSession, I went with a factory to do this:
protocol DataTaskPublisherFactory {
func make(for request: URLRequest) -> URLSession.DataTaskPublisher
}
Then in your network layer:
func performRequest<ResponseType>(_ request: URLRequest) -> AnyPublisher<ResponseType, APIError> where ResponseType : Decodable {
Just(request)
.flatMap {
self.dataTaskPublisherFactory.make(for: $0)
.mapError { APIError.urlError($0)} } }
.eraseToAnyPublisher()
}
Now you can just pass a mock factory in the test using the URLSession subclass (this one asserts URLErrors are mapped to a custom error, but you could also assert some other condition given data/response):
func test_performRequest_URLSessionDataTaskThrowsError_throwsAPIError() {
let session = URLSessionMock()
session.error = TestError.test
let dataTaskPublisherFactory = mock(DataTaskPublisherFactory.self)
given(dataTaskPublisherFactory.make(for: any())) ~> {
session.dataTaskPublisher(for: $0)
}
let api = API(dataTaskPublisherFactory: dataTaskPublisherFactory)
let publisher: AnyPublisher<TestCodable, APIError> =
api.performRequest(URLRequest(url: URL(string: "www.someURL.com")!))
let _ = publisher.sink(receiveCompletion: {
switch $0 {
case .failure(let error):
XCTAssertEqual(error, APIError.urlError(URLError(_nsError: NSError(domain: "NSURLErrorDomain", code: -1, userInfo: nil))))
case .finished:
XCTFail()
}
}) { _ in }
}
The one issue with this is that URLSession init() is deprecated from iOS 13, so you have to live with a warning in your test. If anyone can see a way around that I'd greatly appreciate it.
(Note: I'm using Mockingbird for mocks).
Using DispatchGroup I am trying to run 2 network requests against my client, returning the results when both have completed.
I am having an issue in that sometimes the completion handler for one of DispatchGroup requests is called twice and the other is not called at all.
An example would be -
func fetchProfileWithRelatedArticle(onSuccess: #escaping (User, [RelatedArticle]) -> Void, onError: #escaping (Error) -> Void) {
let dispatchGroup = DispatchGroup()
var user: User?
var articles: [RelatedArticle] = []
var errors: [Error] = []
dispatchGroup.enter()
fetchProfileForUser(onSuccess: {
user = $0
print("fetchProfile:",$0)
print("123")
dispatchGroup.leave()
}, onError: { error in
errors.append(error)
dispatchGroup.leave()
})
dispatchGroup.enter()
getArticlesForUser(onSuccess: {
articles = $0
print("getArticlesForUser:",$0)
print("456")
dispatchGroup.leave()
}, onError: { error in
errors.append(error)
dispatchGroup.leave()
})
dispatchGroup.notify(queue: .main) {
guard let user = user, errors.isEmpty else { return }
onSuccess(user, articles)
}
}
Here I fetch a user profile and also fetch a list of articles they have written. These are returned via a completion handler and presented elsewhere.
Most of the time this works, however it appears on occasion either one of those requests will call its own completion handler twice and the other request wont.
I suspect this may be down to when my access token expires as it occurs if I leave the app for a short time. My access token has a life of 2 minutes.
Should a request receive a 401 response, I have the following method in my network client that requests a new token, then invokes the call again. I believe this may not be working as I'd like.
if response.statusIs401() {
self?.refreshHandler { success in
guard success else { completion(.failure(TokenError.refused)); return }
self?.request(resource, completion)
}
return
}
I suspect calling the method again after the update is doing something to the requests my dispatch group is returning.
Is it possible to chain requests in this fashion?
struct NoContent: Codable { }
typealias RefreshHandler = (#escaping (Bool) -> Void) -> ()
typealias TokenGetter = () -> [String: String]
protocol ClientType: class {
associatedtype Route: RouterType
func request<T: Codable>(_ resource: Route, _ completion: #escaping (Result<T>)-> Void)
}
class Client<Route: RouterType>: ClientType {
enum APIError: Error {
case unknown, badResponse, jsonDecoder, other
}
enum TokenError: String, Error {
case expired = "Access Token Expired"
case refused = "Refresh Token Failed"
}
private(set) var session: SessionType
private(set) var tokenGetter: TokenGetter
private(set) var refreshHandler: RefreshHandler
private lazy var decoder: JSONDecoder = {
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .iso8601withFractionalSeconds
return decoder
}()
init(session: SessionType, tokenGetter: #escaping TokenGetter, refreshHandler: #escaping RefreshHandler) {
self.session = session
self.tokenGetter = tokenGetter
self.refreshHandler = refreshHandler
}
func request<T: Codable>(_ resource: Route, _ completion: #escaping (Result<T>)-> Void) {
let request = URLRequest(
resource: resource,
headers: tokenGetter()
)
URLSession.shared.dataTask(with: request) { [weak self] data, response, error in
guard error == nil else { completion(.failure(APIError.unknown)); return }
guard let response = response as? HTTPURLResponse else { completion(.failure(APIError.badResponse)); return }
if response.statusIs401() {
self?.refreshHandler { success in
guard success else { completion(.failure(TokenError.refused)); return }
self?.request(resource, completion)
}
return
}
if response.statusIsSuccess() {
guard let self = self, let data = self.deserializeNoContentResponse(data: data) else { completion(.failure(APIError.badResponse)); return }
do {
let value = try self.decoder.decode(T.self, from: data)
DispatchQueue.main.async {
completion(.success(value))
}
} catch let error {
print(error)
}
return
}
completion(.failure(APIError.other))
}.resume()
}
// some calls return a 200/201 with no data
private func deserializeNoContentResponse(data: Data?) -> Data? {
if data?.count == 0 {
return "{ }".data(using: .utf8)
}
return data
}
}
Sounds like you would need to something like the following in your networking client:
func makeTheRequest(_ completion: CompletionHandler) {
URLSession.shared.dataTask(with: someURL) { data, response, error in
guard let httpResponse = response as? HTTPURLResponse else {
return
}
if httpResponse.statusCode == 401 {
self.refreshToken { success in
if success { self.makeTheRequest(completion) }
}
}
// handle response
completion(whateverDataYouNeedToPass)
}
}
That would make the call, check the response code, refresh the token is needed and if that succeeds it calls the method that should make the response again with the completion handler that was passed to in the first place without calling it first. So the completion handler wouldn't be called until after the API call is made the second time.
Of course, adopt this for your own code, shouldn't be too hard to do
I'm trying to do the unit tests for my app.
I've this function preparing the request
func getWeatherDataAtLocation() {
let WEATHER_URL = "http://api.openweathermap.org/data/2.5/weather"
let weatherAPI = valueForAPIKey(named:"weatherAPI")
let lat = String(locationService.latitude)
let lon = String(locationService.longitude)
do {
try networkService.networking(url: "\(WEATHER_URL)?APPID=\(weatherAPI)&lon=\(lon)&lat=\(lat)", requestType: "weather")
} catch let error {
print(error)
}
}
I've a service class networkservice processing the network request :
class NetworkService {
var weatherDataDelegate: WeatherData?
var session: URLSession
init(session: URLSession = URLSession(configuration: .default)) {
self.session = session
}
func networking(url: String, requestType: String) {
var request = URLRequest(url: requestUrl)
request.httpMethod = "GET"
var task: URLSessionDataTask
task = session.dataTask(with: request) { (data, response, error) in
switch requestType {
case "weather":
do {
let weatherJSON = try JSONDecoder().decode(WeatherJSON.self, from: data)
self.weatherDataDelegate?.receiveWeatherData(weatherJSON)
} catch let jsonErr {
print(jsonErr)
}
case // Other cases
default:
print("error")
}
}
task.resume()
}
}
Then i've the delegate running this function to update the JSON received
func receiveWeatherData(_ data: WeatherJSON) {
self.dataWeather = data
do {
try updateWeatherDataOnScreen()
} catch let error {
print(error)
}
}
The issue is I've no idea how I can write some code to test this and all the ressources I find is to test with a callback, any idea?
So there are mutliple steps in this.
1: Create a mocked version of the response of exactly this request. And save it in a json file. Named like weather.json
2: Once you have done that you want to add an #ifdef testSchemeName when executing request. And tell it to tell your function called networking() to read from a file named "\(requestType).json" instead of making the request.
Optional, more advanced way:
This actually intercepts your request and send you the file data instead. A bit more advanced, but your testing gets 1 level deeper.
I want to make use of the RequestAdapter and RequestRetrier protocols, so I created my own so called AuthenticationHandler class which implements both protocols. I do this because the refresh token may be expired so this mechanism comes in handy.
The RequestAdapter protocol method adapt does get called, but the should RequestRetrier protocol method does not. I have a separate class that does the actual request:
class TestRequest {
var authHandler: AuthenticationHandler?
func executeRequest() {
// For testing purposes a false access token is passed
self.authHandler = AuthenticationHandler(accessToken: "some_default_token")
let sessionManager = SessionManager()
sessionManager.adapter = authHandler
sessionManager.retrier = authHandler
var loginModel : LoginMessage = LoginMessage.init()
loginModel.username = "someUserName"
loginModel.password = "WrongPassword"
do {
let binaryData = try loginModel.serializedData()
// Create a file with this binary data in order to use it as part of the multipart formdata
guard let fileURL = createFileFrom(data: binaryData) else {
print("Error creating file")
return
}
// Note: custom headers have been set in the AuthenticationHandler
sessionManager.upload(multipartFormData: { multipartFormData in
multipartFormData.append(fileURL, withName: "content")
},
to: K.endpointLogin) { (encodingResult) in
switch encodingResult{
case .success(let upload, _, _):
upload.responseJSON { response in
print("Encoding result success...")
print("Statuscode: \(response.response?.statusCode)")
print(response)
}
case .failure(let encodingError):
print("Failure: \(encodingError)")
}
}
} catch {
print(error)
}
}
I have followed the example in the documentation here
I have read several previous posts saying it has to do with retaining the sessionManager. But I think that is also covered. My authentication handler looks like this:
class AuthenticationHandler: RequestAdapter, RequestRetrier {
private typealias RefreshCompletion = (_ succeeded: Bool, _ accessToken: String?) -> Void
private let sessionManager: SessionManager = {
let configuration = URLSessionConfiguration.default
configuration.httpAdditionalHeaders = SessionManager.defaultHTTPHeaders
return SessionManager(configuration: configuration)
}()
private let lock = NSLock()
private var accessToken: String
private var isRefreshing = false
private var requestsToRetry: [RequestRetryCompletion] = []
init(accessToken: String) {
self.accessToken = accessToken
}
// MARK: - RequestAdapter protocol method
func adapt(_ urlRequest: URLRequest) throws -> URLRequest {
var urlRequest = urlRequest
if let urlString = urlRequest.url?.absoluteString, urlString.hasPrefix(K.SERVER_URL) {
urlRequest.setValue("application/json", forHTTPHeaderField: "Accept")
urlRequest.setValue("multipart/form-data", forHTTPHeaderField: "Content-Type")
urlRequest.setValue("Bearer " + accessToken, forHTTPHeaderField: "Authorization")
}
return urlRequest
}
// MARK: - RequestRetrier protocol method
func should(_ manager: SessionManager, retry request: Request, with error: Error, completion: #escaping RequestRetryCompletion) {
lock.lock() ; defer { lock.unlock() }
}
}
My config is as follows:
Alamofire version: 4.7.2
Xcode version: 9.4.1
Swift version: 4
What am I doing wrong here? Why is the request cancelled and is the should method not called?
Any help is greatly appreciated.
Your core issue is that your SessionManager instance is being deinited, which cancels any ongoing tasks. You should keep it around in a singleton or something similar, which will fix the other issue, that of using a new SessionManager for each request, which is an anti pattern.
I am currently working my way through he Treehouse IOS Swift course, and we are building a weather app. I've gotten to a point where I keep getting an error that my class isn't conforming to my protocol, but I can't figure out why.
Here is my protocol declaration:
public protocol APIClient {
var configuration: URLSessionConfiguration { get }
var session: URLSession { get }
func JSONTaskWithRequest(request: URLRequest, completion: JSONTaskCompletion) -> JSONTask
func fetch<T: JSONDecodable>(request: URLRequest, parse: (JSON) -> T?, completion: (APIResult<T>) -> Void)
}
And then I have a protocol extension where I have default implementations of my two methods declared in the protocol, and one of the methods is calling the other method, as you can see below.
public extension APIClient {
func JSONTaskWithRequest(request: URLRequest, completion: #escaping JSONTaskCompletion) -> JSONTask {
let task = session.dataTask(with: request) { data, response, error in
guard let HTTPResponse = response as? HTTPURLResponse else {
let userInfo = [
NSLocalizedDescriptionKey: NSLocalizedString("Missing HTTP Response", comment: "")
]
let error = NSError(domain: BPSnetworkingErrorDomain, code: MissingHTTPReponseError, userInfo: userInfo)
completion(nil, response as! HTTPURLResponse, error)
return
}
if data == nil {
if let error = error {
completion(nil, response as! HTTPURLResponse, error as NSError?)
}
} else {
switch HTTPResponse.statusCode {
case 200:
do {
let JSON = try JSONSerialization.jsonObject(with: data!, options: []) as? [String: AnyObject]
completion(JSON, HTTPResponse, nil)
} catch let error as NSError {
completion(nil, HTTPResponse, error)
}
default: print("Received HTTP Response \(HTTPResponse.statusCode) - not handled")
}
}
}
return task
}
public func fetch<T>(request: URLRequest, parse: #escaping (JSON) -> T?, completion: #escaping (APIResult<T>) -> Void) {
let task = JSONTaskWithRequest(request: request) { json, response, error in
DispatchQueue.main.async {
guard let json = json else {
if let error = error {
completion(.Failure(error))
} else {
let error = "Something is really wrong. There was no JSON object created, but there was no error either."
completion(.Failure(error as! Error))
}
return
}
if let value = parse(json) {
completion(.Success(value))
} else {
let error = NSError(domain: BPSnetworkingErrorDomain, code: unexpectedResponseError, userInfo: nil)
completion(.Failure(error))
}
}
}
task.resume()
}
}
Then I have my class declaration where I am getting my non conformity error.
final class ForecastAPIClient: APIClient {
let configuration: URLSessionConfiguration
lazy var session: URLSession = {
return URLSession(configuration: self.configuration)
}()
private let token: String
init(config: URLSessionConfiguration, APIKey: String) {
self.configuration = config
self.token = APIKey
}
convenience init(APIKey: String) {
self.init(config: URLSessionConfiguration.default, APIKey: APIKey)
}
func fetchCurrentWeather(coordinate: Coordinate, completion: #escaping (APIResult<CurrentWeather>) -> Void) {
let request = Forecast.Current(token: self.token, coordinate: coordinate).request
fetch(request: request, parse: { (JSON) -> CurrentWeather? in
if let currentWeatherDictionary = JSON["currently"] as? [String: AnyObject] {
return CurrentWeather(JSON: currentWeatherDictionary)
} else {
return nil
}
}, completion: completion)
}
}
I've done a lot of reading around for several hours trying to figure out what is going on here. From what I understand, I shouldn't need to define those two methods in my class since they have default implementations in the protocol extension. I came across the issue of public/internal types and things like that, that someone else was having here on StackExchange with their extensions, (as you an see by my labeling things public and what not), but that didn't seem to help in my case. The only way I've been able to get the error to go away, is by commenting out those method declarations in the original protocol declaration. Which seems to indicate to me that either the class, or the protocol, or something isn't seeing the extension for some reason, however, if I command click on the fetch method call in the class declaration, it takes me to the definition of it in the extension. I haven't been able to find a solution, or even someone who is doing this similar thing, there are several people on Treehouse that seem to be having this same issue as well.
Also, I download the teachers code, and converted it to Swift 3, and it was getting the same error as well, so maybe it's an issues with having a different version of Xcode the what he used when he made the video?
I feel like I'm kind of grasping at straws a little bit, but I really am eager to get this figured out, so any possible help would be so much appreciated.
Thank you!
I used Xcode's playground to test and play around with your code. I took your code (protocol declaration, protocol extension, and class declaration) and heavily simplified the JSONTaskWithRequest() and fetch() functions. The code compiled with no "non conformity error." Here is the code I used:
//: Playground :
import UIKit
// protocol declaration
public protocol APIClient {
var configuration: URLSessionConfiguration { get }
var session: URLSession { get }
func JSONTaskWithRequest()
func fetch()
}
// protocol extension
public extension APIClient {
func JSONTaskWithRequest() {
print("JSONTaskWithRequest here")
}
func fetch() {
print("fetch here")
}
}
// class declaration
final class ForecastAPIClient: APIClient {
let configuration: URLSessionConfiguration
lazy var session: URLSession = {
return URLSession(configuration: self.configuration)
}()
private let token: String
init(config: URLSessionConfiguration, APIKey: String) {
self.configuration = config
self.token = APIKey
}
}
I suspect that there is a bug in JSONTaskWithRequest and/or fetch. I suggest you isolate either function to figure out which one is giving you the error. Then debug from there.
Also, just another suspicion. In the extension's JSONTaskWithRequest function implementation, you have:
let task = session.dataTask(with: request) {...}
return task
JSONTaskWithRequest is required to return a JSONTask. Maybe you need to downcast task:
return task as! JSONTask
I couldn't use your provided code because things like JSONTaskCompletion and JSONDecodable aren't recognized by Swift. Are you using a third party JSON swift library?