1

I'm developing an iOS app in Swift4 with an Object-C framework called 'YapDatabase'. There is an Object-C function with a block like this in class 'YapDatabaseConnection':

- (void)readWithBlock:(void (^)(YapDatabaseReadTransaction *transaction))block;

I use the function in this way:

static func readNovelIds() -> [String]? {        
    let account = XFAccountManager.share().account
    var events: [XFNovelClickEvent]?
    OTRDatabaseManager.shared.readOnlyDatabaseConnection?.read({ (transaction) in
        events = XFNovelClickEvent.allNovelClickEvents(accountId: account.uniqueId, transaction: transaction)
    })
    guard let clickEvents = events else {
        return nil
    }
    let readNovelsIds = clickEvents.map {
        $0.bookId ?? ""
    }
    return readNovelsIds
}

I thought the closure will be executed immediately after the 'events' parameter declared. In fact, the closure doesn't be executed before result returns. To search the reason, I open the file named 'YapDatabaseConnection.h(Interface)' generated by Xcode (with cmd+shift+o), found the function has been translate to Swift in this way:

open func read(_ block: @escaping (YapDatabaseReadTransaction) -> Void)

So, how do I use this function in a @noescap way?

Itachi
  • 5,777
  • 2
  • 37
  • 69
Rabbit Zhou
  • 123
  • 1
  • 8

3 Answers3

0

As the caller, you can't change when the closure is executed. That's up to the read() function. If you control that function, you'll need to modify it to call the closure immediately. If you don't control it, then you can't modify how it behaves.

You can convert an asynchronous call into a synchronous call using a DispatchGroup as described in Waiting until the task finishes. However, you can't make a database call on the main queue; you risk crashing the app. As a general rule, you should just use async calls in this case (i.e. make readNovelIds also be asynchronous and take a completion handler).

Rob Napier
  • 286,113
  • 34
  • 456
  • 610
0

The reason why Xcode bridged the objective-c block as @escaping is because the block may be executed after the function return.

Since you don’t own YapDatabase, you couldn’t modify the source code to make it non-escaped, so you may wanna make your readNovelIds function takes a closure as parameter and pass the return value through closure.

static func readNovelIds(resultHandler: @escaping ([String]?) -> ()) {
    let account = XFAccountManager.share().account
    var events: [XFNovelClickEvent]?
    OTRDatabaseManager.shared.readOnlyDatabaseConnection?.read({ (transaction) in
        events = XFNovelClickEvent.allNovelClickEvents(accountId: account.uniqueId, transaction: transaction)
        if let clickEvents = events {
            let readNovelsIds = clickEvents.map {
                $0.bookId ?? ""
            }
            resultHandler(readNovelsIds)
        }
        resultHandler(nil)
    })
}
kubrick G
  • 856
  • 6
  • 10
0

If the method is in fact synchronous (i.e. it will not allow the block to escape its context), the Objective C header method should be decorated with NS_NOESCAPE. Looking at the documentation (which does say it is synchronous), and the implementation, it should be annotated that way.

- (void)readWithBlock:(void (NS_NOESCAPE ^)(YapDatabaseReadTransaction *transaction))block;

That, I believe, should allow the Swift interface importer to add the @noescaping declaration. You should probably file a bug request on the YapDatabase project; they can change it there.

Carl Lindberg
  • 2,902
  • 18
  • 22