How To Make URLSession Wait For Connectivity

If you use URLSession to start a task with iOS 10 and the network is not available the connection fails at once with an error. There was a small change in iOS 11 that means you can now tell your URLSession to wait until network connectivity is available before trying the connection.

Configuring The Session

To configure a session to wait for connectivity set the waitsForConnectivity boolean to true when creating the session configuration. For example, starting with a default session configuration:

let configuration = URLSessionConfiguration.default
configuration.timeoutIntervalForResource = 300
if #available(iOS 11, *) {
  configuration.waitsForConnectivity = true
}
let session = URLSession(configuration: configuration, delegate: self, delegateQueue: nil)

Notes:

  • The default resource timeout interval is 7 days which might be longer than you want to wait. I changed it to five minutes here.
  • As with all session configuration settings you must set the waitsForConnectivity flag before creating the session. The session takes a copy of the configuration so changing it afterwards has no effect on the created session.
  • This API is new in iOS 11 so wrap waitsForConnectivity in an #available if supporting iOS 10 or earlier.
  • The default for waitsForConnectivity is false but note that background sessions always wait for connectivity.
  • Set the delegate when creating the session if you want to know when the session waits for connectivity.

Also remember that URLSession keeps a strong reference to its delegate. If the session owner is also the delegate you can end up with a retain cycle unless you call invalidateAndCancel on the session at some point to release the delegate and break the cycle.

session.invalidateAndCancel() // Don't use session after this

Waiting For Connectivity

Using iOS 10 or earlier if you start a data task and there is no connectivity it fails at once. You get back an error either via the completion handler or the delegate method urlSession(_:task:didCompleteWithError:) depending on which you are using.

In iOS 11 if you set waitsForConnectivity to true the session waits (it will eventually timeout) and only starts the task if the network connection becomes available. You also get a callback to the URLSessionTaskDelegate method urlSession(_:taskIsWaitingForConnectivity:) so you can take some action:

func urlSession(_ session: URLSession, taskIsWaitingForConnectivity task: URLSessionTask) {
    // waiting for connectivity, update UI, etc.
}

Notes:

  • The session only waits when trying the initial connection. If the network drops after making the connection you get back an error via the completion handler or session delegate as with iOS 10.
  • How long the session waits for connectivity depends on the session resource timeout timeoutIntervalForResource. This defaults to 7 days so you may want to change it to some shorter value when creating the session configuration.
Advertisements
How To Make URLSession Wait For Connectivity

Write Mockup Classes For Unit Testing Of UserDefaults, Core Data & UrlSessions In Swift 3 & XCode 8

URLSession

Source Code

public protocol CLSession {
func dataTask(with url: URL, completionHandler: @escaping (Data?, URLResponse?, Error?) -> Void) -> URLSessionDataTask
func dataTask(with request: URLRequest, completionHandler: @escaping (Data?, URLResponse?, Error?) -> Void) -> URLSessionDataTask
}

extension URLSession: CLSession { }

public final class URLSessionMock: CLSession {

var url: URL?
var request: URLRequest?
private let dataTaskMock: URLSessionDataTaskMock

public convenience init?(jsonDict: [String: Any], response: URLResponse? = nil, error: Error? = nil) {
guard let data = try? JSONSerialization.data(withJSONObject: jsonDict, options: []) else { return nil }
self.init(data: data, response: response, error: error)
}

public init(data: Data? = nil, response: URLResponse? = nil, error: Error? = nil) {
dataTaskMock = URLSessionDataTaskMock()
dataTaskMock.taskResponse = (data, response, error)
}

public func dataTask(with url: URL, completionHandler: @escaping (Data?, URLResponse?, Error?) -> Void) -> URLSessionDataTask {
self.url = url
self.dataTaskMock.completionHandler = completionHandler
return self.dataTaskMock
}

public func dataTask(with request: URLRequest, completionHandler: @escaping (Data?, URLResponse?, Error?) -> Void) -> URLSessionDataTask {
self.request = request
self.dataTaskMock.completionHandler = completionHandler
return self.dataTaskMock
}

final private class URLSessionDataTaskMock : URLSessionDataTask {

typealias CompletionHandler = (Data?, URLResponse?, Error?) -> Void
var completionHandler: CompletionHandler?
var taskResponse: (Data?, URLResponse?, Error?)?

override func resume() {
DispatchQueue.main.async {
self.completionHandler?(self.taskResponse?.0, self.taskResponse?.1, self.taskResponse?.2)
}
}
}

}

Images
Use

UserDefaults

Source Code

class MockUserDefaults: UserDefaults {
var loggedInUser = 0
override func set(_ value: Any?, forKey defaultName: String) {
if defaultName == “loggedInUser” {
loggedInUser += 1
}
}
}

Images

Use

CoreData

Source Code

// MARK: – Variables
//Core Data variables
var storeCoordinator: NSPersistentStoreCoordinator!
var managedObjectContext: NSManagedObjectContext!
var managedObjectModel: NSManagedObjectModel!
var store: NSPersistentStore!

//Managers
var apiManager: APIManager!
var dataManager: DataManager!

// MARK: – XCTest Methods

override func setUp() {
super.setUp()
// Put setup code here. This method is called before the invocation of each test method in the class.
//Set Managers
apiManager = APIManager.shared
dataManager = DataManager.shared

/* Core Data Mock Object Configuration */
// ——– Start ——-
managedObjectModel = NSManagedObjectModel.mergedModel(from: nil)
storeCoordinator = NSPersistentStoreCoordinator(managedObjectModel: managedObjectModel)

do {
store = try storeCoordinator.addPersistentStore(ofType: NSInMemoryStoreType, configurationName: nil, at: nil, options: nil)
} catch {
print(error.localizedDescription)
}

managedObjectContext = NSManagedObjectContext(concurrencyType: .mainQueueConcurrencyType)
managedObjectContext.persistentStoreCoordinator = storeCoordinator
apiManager.context = managedObjectContext
dataManager.context = managedObjectContext
(UIApplication.shared.delegate as! AppDelegate).mockContext = managedObjectContext
// ——– End ——-
}

override func tearDown() {
// Put teardown code here. This method is called after the invocation of each test method in the class.
managedObjectContext = nil
apiManager = nil
dataManager = nil
(UIApplication.shared.delegate as! AppDelegate).mockContext = nil

//Check Store Removal
do {
try storeCoordinator.remove(store)
} catch {
XCTFail(“couldn’t remove persistant store: \(error)”)
}

super.tearDown()
}

func testThatStoreIsSetUp() {
XCTAssertNotNil(store, “no persitant store”)
}

Images

Use

Write Mockup Classes For Unit Testing Of UserDefaults, Core Data & UrlSessions In Swift 3 & XCode 8