— 1 min read
Here's a charade: I have two functions. Can you tell which one below is synchronous and which one is asynchronous?
1func doThis(_ closure: () -> Void) {2 // ...3}4
5func doThat(_ closure: @escaping () -> Void) {6 // ...7}
That's a silly question, I know. The answer is - there's no way to know by this code. The implementation is hidden.
But they are slightly different, aren't they? Both accept closures as the argument, but one closure is constrained with the @escaping annotation.
An escaping closure just means that that block can escape its context, or simply it may not be consumed within the function's body.
The common use for escaping closures is to perform asynchronous operations. In Swift (before the advance of async/await
), the way you generally tell the compile to execute a computational intensive task without blocking control is by dispatching it to a background thread and giving it a completion callback for the time it was finished.
But you still can have synchronous tasks running in it. It all depends of your implementation.
This is evident when you're working with Dependency Injection and you have an implementation for you app, but use mocks in your tests. Just because you have a escaping closure, it doesn't mean you need to wait expectations to assert what happens within it.
Below we have an example to demonstrate how this happens.
1// Api Declaration2protocol Api: AnyClass {3 func callEndpoing(performTaskWhenApiResponds: @escaping (MyApiResult) -> Void)4}5
6// Default Async Api used in the App7final class DefaultAsyncApi: Api {8 func callEndpoing(performTaskWhenApiResponds: @escaping (MyApiResult) -> Void) {9 // Dispatching to another queue, asynchronously10 DispatchQueue.global(qos: .background).async { [self] in11 // ... calls network12 }13 }14}15
16// Our Awesome Api Service17final class MyAwesomeApiService {18 private let api: Api19 init(api: Api) {20 self.api = api21 }22
23 func callEndpoint(_ closure: @escaping (MyApiResult) -> Void) {24 api.callEndpoing(performTaskWhenApiResponds: closure)25 }26}27
28// ..... TESTING TIME .....29
30// Mock Api31final class MockApi: Api {32 var callEndpointIsCalled = false33 func callEndpoing(performTaskWhenApiResponds: @escaping (MyApiResult) -> Void) {34 callEndpointIsCalled = true35 performTaskWhenApiResponds(/* Api result here */)36 }37}38
39// ... somewhere in (probably) MyAwesomeApiServiceTests.swift40
41// Testing it with a asynchronous implementation (bad code, don't do that)42func test_callEndpoint_runsApiCallEndpoint_async() {43 // Expectation44 let expects = expectation("Expects callEndpoint(_:) closure to run") // this test NEEDS an expectation, as it runs an asynchronous task. BAD PRACTICE WARNING! Don't use me.45 // Given46 let api = DefaultAsyncApi() // using the same object that the app uses47 let apiService = MyAwesomeApiService(api: api)48 // When49 apiService.callEndpoint { result in50 // Then51 XCTAssertEqual(result, /* expected result here */) // This line will run asynchronously52 expects.fulfill()53 }54 // Wait55 wait(for: [expects], time: 0.1)56}57
58// Testing it with a synchronous implementation59func test_callEndpoint_runsApiCallEndpoint_sync() {60 // Given61 let mockApi = MockApi() // we have total control over this object62 let apiService = MyAwesomeApiService(api: mockApi)63 // When64 apiService.callEndpoint { result in65 // Then66 XCTAssertEqual(result, /* expected result here */) // This line will run synchronously67 }68}
MyAwesomeApiService
takes an object of type Api
and consumes it as a dependency, to forward the call we've asked it to perform to the network. The asynchronous operation lives in the DefaultAsyncApi
implementation, but not in MyAwesomeApiService
. That means, unless demmanded, callEndpoint(_:)
will run synchronously.
That's it for today. Thanks for stopping by once more. See you next time.