I know we can call FlutterResult to send data from native plugin to dart like this:
// swift func
public func handle(_ call: FlutterMethodCall, result: #escaping FlutterResult) {
result("send something to dart")
}
But I run into a more tricky situcation. I'm calling NSFileCoordinator.coordinate, I only have file access in callback function:
NSFileCoordinator().coordinate(readingItemAt: url, error: &error) { (url) in
// you have access
}
I'm hoping I can call dart code synchronously to read the file so that I wouldn't lose file access.
NSFileCoordinator().coordinate(readingItemAt: url, error: &error) { (url) in
// Call dart code to read the file and block the current thread!!!
}
Seems not possible.
Related
I want to use Vapor(a Web framework made with Swift) and JavaScriptCore(Apple's JS engine) to process some requests. Vapor supports async/await, which makes handling HTTP requests very easy, like this:
func routes(_ app: Application) throws {
app.get("myrequestpath") { req async throws -> String in
let result = await myAsyncFunction()
return result
}
}
myAsyncFunction() handles the processing to return the desired result and some of the processing I do is in JavaScript, executed in JavaScriptCore. I have no problem with interacting with JavaScriptCore, I can call the JS functions and JS can call the Swift functions I give access to.
I want to be able to initiate a JS function from Swift, wait for the async code in JS to finish its job and then pass the result to Swift async code for further processing.
The problem is, JavaScript itself has async/await that works slightly differently than the one in Swift.
In JavaScript, async functions return a Promise object that later resolves once the code block inside it calls the resolve or reject functions.
As a result, I can't simply await a JS function because it will return a Promise object right away and Swift will continue execution:
func myAsyncFunction() async -> Bool {
let jsContext = JSEngine().evaluateScript(myJSCode)
let result = await jsContext.objectForKeyedSubscript("myAsyncJSFunction").call(withArguments: [data])
return result
}
I would love to do that but I can't, the result will be a Promise object that doesn't mean anything in Swift(right).
As a result, what I need is, to write an async Swift function that pauses until the JS function calls back the resolve or reject function.
Something like this maybe?:
func myAsyncFunction() async -> Bool {
let jsContext = JSEngine().evaluateScript(myJSCode)
let result = await {
/*
A function that I can pass to JS and the JS can call it when it's ready, return the result to Swift and continue execution
*/
}
return result
}
Any ideas how to achieve that?
Okay, as it turns out, Swift async/await concurrency does have a support for continuation. It's not mentioned on the main article on Swift documentation and most 3rd party articles bill this feature as a way to integrate the old callback centric concurrency with the new async/await API, however this is much more useful than simply integrating the old code with the new one.
Continuation provides you with an asynchronous function that can be called from within your async function to pause execution with await until you explicitly resume it. That means, you can await input from the user or another library that uses callback to deliver the results. That also means, you are entirely responsible to correctly resume the function.
In my case, I'm providing the JavaScriptCore library with a callback that resumes execution from Swift, which is called by an async JavaScript code upon completion.
Let me show you. This is a JS code that asynchronously processes a request:
async function processRequest(data, callback){
try {
let result = await myAsyncJSProcessing(data)
callback(result, null)
} catch(err) {
callback(null, err)
}
}
To be able to pause the Swift execution until the JS code finishes processing and continue once the JS calls the callback with the data, I can use continuation like this:
//proccessRequestSync is a synchronous Swift function that provides the callback that the JS will call.
// This is also the function with a callback that Swift will wait until the callback is called
// self.engine is the initiated JavaScriptCore context where the JS runs.
// We create the callback that we will pass to the JS engine as JSValue, then pass it to the JS function as a parameter. Once the JS is done, calls this function.
func proccessRequestSync(_ completion : #escaping (Result<JSValue, Error>) -> Void){
let callback : #convention(block) (JSValue, JSValue) -> Void = { success, failure in
completion(.success(success))
}
let jsCallback = JSValue(object: callback, in: self.engine)
self.engine?.objectForKeyedSubscript("processRequest").call(withArguments: ["some_data", jsCallback])
}
// Then we create the async function that will be paused using continuation.
// We can integrate the function into the rest of our async Swift.
func proccessRequestAsync() async throws -> JSValue {
//Continuation happens right here.
return try await withCheckedThrowingContinuation({ continuation in
proccessRequestSync { result in
// This is the closure that will be called by the JS once processing is finished
// We will explicitly resume the execution of the Swift code by calling continuation.resume()
switch result {
case .success(let val):
continuation.resume(returning: val)
case .failure(let error):
continuation.resume(throwing: error)
}
}
})
}
if let result = try? await proccessRequestAsync()
I'm a beginner in Swift and I'm trying to create a function which returns an Animated Image thanks to SDWebImage.
The path of the Image is known and stored in Firestore in an array.
So the function input is the path of the image (or name), then it downloads the URL and put into an AnimatedImage.
The issue is there is an error in XCODE "Unexpected non-void return value in void function"
There is my function :
func getURL(path: String) -> AnimatedImage {
let storage = Storage.storage()
storage.reference().child(path).downloadURL(completion: { url, error in
guard let url = url, error == nil else {
return
}
let animatedImage = AnimatedImage(url: url)
return animatedImage
})
}
Does anyone know why i get this error ?
Thanks in advance for help
Jean
So the issue here is that while your outer method getURL(path: String) -> AnimatedImage returns an image, the method downloadURL() is actually called asynchronously and doesn't return anything – so returning animatedImage here won't work.
Indeed, your method shouldn't return anything, because there won't be anything to return. Instead, your method should use a completion closure to provide the image:
func getURL(path: String, completion: ((AnimatedImage? -> Void))) {
// Get your image...
completion(animatedImage) // Do this instead of returning
}
Then the caller gets access to the returned image via the completion closure provided:
getURL(path: imagePath, completion: { image in
// `image` may (or may not be) your animated image
})
The issue relates to asynchronous (takes a while to complete, some code runs later with the results) and synchronous functions (control flow returns immediately). To illustrate this:
func getURL(path: String) -> AnimatedImage {
let storage = Storage.storage()
// A - we arrive here first and we just
// call downloadURL, passing a block of code called a "completion handler" as an argument representing what to do later when the data comes back
//
storage.reference().child(path).downloadURL(completion: { url, error in
/// C - Third: this block is (possibly) called, much later, when download is done
// any return statement here is returning *from the completion block*
})
// B - we hit this line second
// a return statement here or a closing } will return from the getURL function
// so we're about to return from the getURL function now...
}
One of the things that's throwing me in Swift is the never-ending chain of completionBlocks in my program; and I'm not sure how to get Swift to say "Ok, the completion block is now done -- come back to the main program."
In my project I am writing a simple board/card game which loads its data from a very small plist.
I wrote a fairly simple Plist loader, it loads the plist and kicks back Data via a completionBlock. It doesn't do any parsing; it doesn't care about how you want to parse it, it simply returns NSData (Data) or errors.
I have a Parser which fires the Plist loader, gets the Data and then parses it using the new Swift Codable protocol.
// A static function to try and find the plist file on bundle, load it and pass back data
static func loadBoard(completionHandler: #escaping (Board?, Error?) -> Void) {
PListFileLoader.getDataFrom(filename: "data.plist") { (data, error) in
if (error != nil) {
print ("errors found")
completionHandler(nil, error)
}
else {
guard let hasData = data else {
print ("no data found")
completionHandler(nil, error)
return
}
do {
print ("found board")
let board = try decodeBoard(from: hasData) // Call a function that will use the decoder protocol
completionHandler(board, nil)
} catch {
print ("some other board error occured")
completionHandler(nil, error)
}
}
}
}
and then returns this parsed data to the main program, or whatever called it -- for example; an XCTest
My XCTest:
func testBoardDidLoad() -> Board? { // The return bit will show an error; but its fine without the return part
BoardParsePlist.loadBoard { (board, error) in
XCTAssertNotNil(board, "Board is nil")
XCTAssertNotNil(error, error.debugDescription)
// How do I now escape this and return flow to the normal application?
// Can I wrap this in a try-catch?
}
}
From a hierarchical view it sort of looks like this.
XCTest
... Calls the parser (completionBlock ...)
.... Parser calls the PListLoader (completionHandler: ...)
Now it feels like I'm stuck in the block for the rest of the app
BoardParsePlist.loadBoard { (board, error) in
// ... rest of the app now lives here?
})
It seems I'm in a never ending loop of completionBlocks.
How do you "escape" or break out of the completion block and return flow back to the main app?
I'm not sure if I explained it correctly, but would appreciate any assistance on this.
Thank you for your time.
not sure how to get Swift to say "Ok, the completion block is now done -- come back to the main program."
There's no way and no need to say that - it happens automatically.
static func loadBoard(completionHandler: #escaping (Board?, Error?) -> Void) {
PListFileLoader.getDataFrom(filename: "data.plist") { (data, error) in
// completion handler code here
}
// "back to main"
}
In your example, execution will get "back to main" in one of two ways, depending on whether PListFileLoader.getDataFrom is running asynchronously or not.
If it's running synchronously, the execution order will be:
your app calls loadBoard
this calls getDataFrom
which in turn calls its completion handler parameter
the completion handler runs
"back to main" runs
OTOH, if getDataFrom is asynchronous (e.g. because it does a network request), the order will be:
your app calls loadBoard
this calls getDataFrom
which starts its asynchronous work and returns
"back to main" runs
at some later point in time, the work started by getDataFrom is done, and it calls the completion handler parameter
the completion handler runs
Either way, you get back to main without special effort.
This question already has answers here:
Returning data from async call in Swift function
(13 answers)
Closed 3 years ago.
I’m using the FileProvider library to look for files on my FTP server and Swift 5’s basic functionality to look for files in the “Documents“ folder of the device:
func lookForFiles() { //runs on a background thread
var ftpExists = false
var localExists = false
let ftpFile:FileObject = lookForFTPFile()
let localFile:FileObject = lookForLocalFile()
//Compare,... files
}
func lookForFTPFile() -> FileObject? {
var found:FileObject?
ftpProvider?.contentsOfDirectory(path: mypath, completionHandler: { (contents, error) in
//Look for ftp file
}) //This is run in an async task according to documentation
return found
}
This of course always returns "nil" because of the task in "contentsOfDirectory" (I also can't return the file from within).
Question: How do I wait for lookForFTPFile to finish before returning the result (which might be nil because it simply didn't find anything) - without just setting up a timer?
I'd prefer to not mess with how the library sets up its asynchronous work.
Something like
var waitingbool = false
var found:FileObject?
func lookForFiles() { //runs on a background thread
//Rest of code
lookForFTPFile()
while !waitingbool {}
//Use "found"
}
func lookForFTPFile() {
ftpProvider?.contentsOfDirectory(path: mypath, completionHandler: { (contents, error) in
//Look for ftp file and save result in "found"
self.waitingbool = true
})
}
looks like might work but at the same time it seems to be breaking a lot of unwritten rules.
Everyone who hasn't done async in Swift runs into the same problem. If you return a value from a method without a closure (as you are doing it), it must return sync. Since your completion handler runs async as you have noticed, we have a problem. You shouldreturn a value from a async method with a completion handler block.
I would have rewritten your method as follows:
func find(content: #escaping (FileObject?) -> ()) {
var found: FileObject?
// init the found variabel somewhere
ftpProvider?.contentsOfDirectory(path: mypath, completionHandler: { (contents, error) in
// You are in a closure completion block here also!
// send a callback to our waiting function...
content(contents)
})
// If ftpProvider is nil, please call the content completion handler block with nil also!
}
Calling side:
find { contents in // Capture self as unowned/weak maybe?
// Use contents.
}
When calling getItems function, XCode auto completes to:
getItems(success: (([File]) -> ())?, fail: ((Error) -> ())?)
I just need some assistance understanding what I need to input in order to get the list of items.
I've created simple callbacks and read up on Apple's closure documentation to better understand what's going on but am still a little confused by this in particular.
You'll have to put closures as parameters like below to use this function:
getItems(success: { (files) in
// files: [File]
for file in files {
print(file)
}
}, fail: { (error) in
// error: Error
print(error)
})