Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How do I call a Swift async method from within a synchronous method that has a return value and completion handlers can't be used?

I've been given an Objective-C framework that has the following API (simplified for this question):

@protocol SomeClassDelegate;

NS_REFINED_FOR_SWIFT
@interface SomeClass: NSObject

@property (weak) id<SomeClassDelegate> delegate;

- (instancetype)initWithDelegate:(id<SomeClassDelegate>)delegate NS_SWIFT_NAME(init(delegate:));

@end

NS_REFINED_FOR_SWIFT
@protocol SomeClassDelegate

- (BOOL)someClass:(SomeClass *)object shouldProcessData:(UUID *)recordID NS_SWIFT_NAME(someClass(_:shouldProcessData:));
- (nullable SomeHelper *)someClass:(SomeClass *)object helperFor:(UUID *)recordID NS_SWIFT_NAME(someClass(_:helerFor:));

@end

The Swift version of this API is being dictated as:

public class SomeClass {
    public weak var delegate: SomeClassDelegate

    init(delegate: SomeClassDelegate)
}

public protocol SomeClassDelegate : AnyObject {
    func shouldProcessData(recordID: UUID, someClass: SomeClass) async -> Bool
    func helperFor(recordID: UUID, someClass: SomeClass) async -> SomeHelper?
}

Given these APIs, I need to implement the Swift wrapper of the existing Objective-C framework. My biggest hurdle is that the Swift delegate methods are meant to be async but the Objective-C delegate methods are not asynchronous and do not have completion handlers.

Here's my first pass at the Swift wrapper which includes a delegate adapter.

public class SomeClass {
    public weak var delegate: SomeClassDelegate

    init(delegate: SomeClassDelegate) {
        wrapper = __SomeClass()
        delegateAdapter = SomeClassDelegateAdapter(self, delegate: delegate)
        wrapper.delegate = delegateAdapter
    }

    private wrapper: __SomeClass
    private delegateAdapter: SomeClassDelegateAdapter
}

fileprivate class SomeClassDelegateAdapter: NSObject, __SomeClassDelegate {
    private let delegate: SomeClassDelegate
    private let wrapper: SomeClass

    init(_ wrapper: SomeClass, delegate: SomeClassDelegate) {
        self.wrapper = wrapper
        self.delegate = delegate
    }

    func someClass(_ someClass: __SomeClass, shouldProcessData recordID: UUID) -> Bool {
        // The following gives "'async' call in a function that does not support concurrency"
        //return delegate.shouldProcessData(recordID, someClass: wrapper)
        // Adding await doesn't change anything
        //return await delegate.shouldProcessData(recordID, someClass: wrapper)
        // Wrapping in Task gives "Cannot convert return expression of type 'Task<Bool, Never>' to return type 'Bool'"
        Task {
            return await delegate.shouldProcessData(recordID, someClass: wrapper)
        }
        // Adding .value to the Task gets me back to the first error. Ugh!
    }

    func someClass(_ someClass: __SomeClass, helperFor recordID: UUID) -> __SomeHelper? {
        // Similar issues to above
    }
}

So how do I solve this? I've done a lot of searching and all solutions involve using completion handlers. But I can't do that since the Objective-C API is fixed and complete. I don't know why the Swift version of the delegate API is marked with async. That's out of my control. I need a solution that allows me to implement the given Swift API around the given Objective-C API.

Is there a valid way to get a return value from a Swift async method and return it from inside a non-async method? From my searching, the use of GCD or semaphores is a bad combination with Swift async/await. Do those issues apply here?

More fundamentally, is my overall approach of the delegate adapter the correct approach? If not, that completely changes my question.

like image 369
HangarRash Avatar asked Jan 22 '26 14:01

HangarRash


2 Answers

You asked:

Is there a valid way to get a return value from a Swift async method and return it from inside a non-async method?

No, there really isn’t. Apple is very specific that blocking should be avoided and that, where absolutely needed, things like locks must be isolated to a “tight, well-known critical section”. (The example would be a lock used for a very quick synchronization within a task, but not across separate tasks or continuations.) For more information, see Swift concurrency: Behind the scenes. But we have a contract with Swift concurrency to never “impede forward progress”. It's the whole idea behind a “concurrency system” (as in contrast to “parallelism”).

You raised the example of a UITableViewDataSource. The table view is requesting data and expects/requires the data source to return the results immediately and synchronously. You cannot return the result asynchronously. And you would never block to fetch the results with some anti-pattern (like semaphores, locks, etc.).

What you can do is initiate the asynchronous process and return nothing for now. Then, when the async fetch is done, tell the table view to reloadData, which will initiate a new query of the UITableViewDataSource. So keep the data source synchronous, but go ahead and use Swift concurrency to launch a task and later initiate a reloadData.

You also mention the CKSyncEngineDelegate example. That is a curious implementation, where the Swift rendition is async, but the Objective-C rendition is not (it has no completion handler). So, clearly the framework has been implemented with an asynchronous delegate for Swift developers, but they simply do not offer asynchronous support for Objective-C clients. (I can only guess that they did not want to encumber Objective-C developers with the burden of calling a completion handler, while offering Swift developers a very natural asynchronous interface, where you might have a suspension point, but you might not. That results in an unusual asymmetry between the two programming languages with this framework.) Re the standard interface with async interfaces between Swift and Objective-C, see Calling Objective-C APIs Asynchronously).

like image 70
Rob Avatar answered Jan 24 '26 07:01

Rob


I would organize delegate methods loosely into two general categories:

  1. Methods which just inform you of certain events, giving you the opportunity to react, but not necessarily immediately.
  2. Methods which are called to request your code to achieve some immediate effect, such as returning some kind of data as an immediate return value of the method call.

Group 1 methods are easy, you can usually just kick off your work in a Task (or dispatch async to another queue, etc.), and have your effect take place after the method's completion.

Group 2 methods are the trickier ones, because you need the result synchronously, but there are some tricks:

  1. As Rob pointed out, in some cases like UITableViewDataSource.tableView(_:numberOfRowsInSection:), you can return 0 for the item count at first (while you start off your load in the background), and then reload shortly after, to give the "real answer".
  2. In some other cases, you can return a "half baked" object synchronously, and flesh out the details asynchronously. For example, for UITableViewDataSource.tableView(_:cellForRowAt:) you might immediately return an empty cell, but kick off some background task that will asynchronously fill it in.
  3. In the worst case, you have methods where a result is immediately required, with its full details. You basically have no recourse here, you need to block in some way (ideally off the main thread), to do the work and have the result ready by the time the method returns.
like image 39
Alexander Avatar answered Jan 24 '26 07:01

Alexander



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!