188

I have a protocol:

enum DataFetchResult {
    case success(data: Data)
    case failure
}

protocol DataServiceType {
    func fetchData(location: String, completion: (DataFetchResult) -> (Void))
    func cachedData(location: String) -> Data?
}

With an example implementation:

    /// An implementation of DataServiceType protocol returning predefined results using arbitrary queue for asynchronyous mechanisms.
    /// Dedicated to be used in various tests (Unit Tests).
    class DataMockService: DataServiceType {

        var result      : DataFetchResult
        var async       : Bool = true
        var queue       : DispatchQueue = DispatchQueue.global(qos: .background)
        var cachedData  : Data? = nil

        init(result : DataFetchResult) {
            self.result = result
        }

        func cachedData(location: String) -> Data? {
            switch self.result {
            case .success(let data):
                return data
            default:
                return nil
            }
        }

        func fetchData(location: String, completion: (DataFetchResult) -> (Void)) {

            // Returning result on arbitrary queue should be tested,
            // so we can check if client can work with any (even worse) implementation:

            if async == true {
                queue.async { [weak self ] in
                    guard let weakSelf = self else { return }

                    // This line produces compiler error: 
                    // "Closure use of non-escaping parameter 'completion' may allow it to escape"
                    completion(weakSelf.result)
                }
            } else {
               completion(self.result)
            }
        }
    }

The code above compiled and worked in Swift3 (Xcode8-beta5) but does not work with beta 6 anymore. Can you point me to the underlying cause?

Jonas Deichelmann
  • 3,513
  • 1
  • 30
  • 45
Lukasz
  • 19,816
  • 17
  • 83
  • 139
  • 5
    This is a very [great article](https://cocoacasts.com/what-do-escaping-and-noescaping-mean-in-swift-3/) on why it's done that way in Swift 3 – mfaani Jan 26 '17 at 19:07
  • 1
    It makes no sense that we have to do this. No other language requires it. – Andrew Koster Oct 02 '19 at 01:44

3 Answers3

322

This is due to a change in the default behaviour for parameters of function type. Prior to Swift 3 (specifically the build that ships with Xcode 8 beta 6), they would default to being escaping – you would have to mark them @noescape in order to prevent them from being stored or captured, which guarantees they won't outlive the duration of the function call.

However, now @noescape is the default for function-typed parameters. If you want to store or capture such functions, you now need to mark them @escaping:

protocol DataServiceType {
  func fetchData(location: String, completion: @escaping (DataFetchResult) -> Void)
  func cachedData(location: String) -> Data?
}

func fetchData(location: String, completion: @escaping (DataFetchResult) -> Void) {
  // ...
}

See the Swift Evolution proposal for more info about this change.

Hamish
  • 78,605
  • 19
  • 187
  • 280
  • 2
    But, how do you use a closure so it does not allow to escape? – Eneko Alonso Sep 08 '16 at 02:07
  • 6
    @EnekoAlonso Not entirely sure what you're asking – you can either call a non-escaping function parameter directly in the function itself, or you can call it when captured in a non-escaping closure. In this case, as we're dealing with asynchronous code, there is no guarantee that the `async` function parameter (and therefore the `completion` function) will be called before `fetchData` exits – and therefore must be `@escaping`. – Hamish Sep 08 '16 at 10:53
  • Feels ugly that we have to specify @escaping as method signature for protocols... is that what we should do? The proposal does not say! :S – Sajjon Sep 19 '16 at 14:09
  • 1
    @Sajjon Currently, you do need to match an `@escaping` parameter in a protocol requirement with an `@escaping` parameter in the implementation of that requirement (and vice versa for non-escaping parameters). It was the same in Swift 2 for `@noescape`. – Hamish Sep 19 '16 at 14:18
  • @EnekoAlonso See https://developer.apple.com/documentation/swift/2827967-withoutactuallyescaping – Peter Schorn May 31 '20 at 09:36
46

Since @noescape is the default, there 2 options to fix the error:

1) as @Hamish pointed out in his answer, just mark the completion as @escaping if you do care about the result and really want it to escape (that probably is the case in @Lukasz's question with Unit Tests as example and possibility of async completion)

func fetchData(location: String, completion: @escaping (DataFetchResult) -> Void)

OR

2) keep the default @noescape behavior by making the completion optional discarding the results altogether in cases when you don't care about the result. For example when user has already "walked away" and the calling view controller doesn't have to hang in memory just because there was some careless network call. Just as it was in my case when I came here looking for answer and the sample code wasn't very relevant for me, so marking @noescape was not the best option, event though it sounded as the only one from the first glance.

func fetchData(location: String, completion: ((DataFetchResult) -> Void)?) {
   ...
   completion?(self.result)
}
Vitalii
  • 4,267
  • 1
  • 40
  • 45
0

Making completion block asoptional variable helped me to solve the issue in Swift 5.

 private func updateBreakTime(for id: String, to time: Time, onSucess: EmptyAction?) {
    dataRepository.updateBreak(
        id: id,
        to: time.seconds,
        onSuccess: { _ in
            onSucess?()
        },
        onError: { [weak self] error in
            self?.screen.showError(error)
        }
    )
}

Where the onSuccess of dataRepository was @escaping

GDeep
  • 39
  • 7