Ran across an interesting one today. Loading up a window controller that I pop up for some task-specific UI stuff. I was just working on the actual window layout dynamics, and wanted to suppress the loading of a configured view controller into one of the views - Just slapped a "return" in the load-up function, thinking that it would exit the function before the async call to load the VC.
func setContainer( container:ListContainer ){
self.container = container
return
DispatchQueue.main.async {
// return
let vs = ListsViewController()
vs.setLists(lists: self.container.itemLists)
vs.view.frame = CGRect(x: 0, y: 0, width: self.listsBox.frame.width, height: self.listsBox.frame.height)
self.listsBox.subviews.removeAll()
self.listsBox.addSubview(vs.view)
}
}
Interestingly, with that first return in there, the stuff inside the async call still executed, like the compiler is saying "that return doesn't apply to the async call because I said so."
The (currently commented out) return inside the async block does exactly the right thing, but I thought it odd that the async block would still run, with appearing after the return. Am I nuts?
macOS Catalina,
macOS application,
Xcode 12.0.1,
Swift 5 (presumably)
This is expected. Your function returns Void, a type that has a single value (). DispatchQueue.main.async also returns Void. With your return you are returning the result of DispatchQueue.main.async (which is again ()). As matt suggested in the comments add a ; after return.
Related
Recently, I ran into a problem where I got a warning for updating UI on background task.
func didInit() async {
listOfTodo = await interactor.getTodos()
}
I tried to wrap the function body inside DispatchQue.main.async {}, but I got an error.
I then found a solution which I have to put #MainActor on top of my function, but I feel like there are other solutions that would make more sense, or this is the only way to work with async/await on Main Thread?
#MainActor
func didInit() async {
listOfTodo = await interactor.getTodos()
}
You're approaching this backwards. If interactor.getTodos() must be run on the main actor, then it should be marked #MainActor, not the caller. But if didInit is logically "a UI-updating method," then it's fine to mark it #MainActor as well.
Or you can use MainActor.run {...} to manually move this one call to the main actor. It all comes down to what you mean to express.
A better approach would be to create a function which takes responsibility of updating UI, something like below
#MainActor
func updateUI() async {
// Code to update your UI
}
Call this function inside your task function like below
func didInit() {
Task.detached { // or specify a priority with Task.detached(priority: .background)
listOfTodo = interactor.getTodos()
await self.updateUI()
}
}
Note: Code is not tested on Xcode so may be require some changes
In this case the async function reads a file and returns the parsed contents.
In my view I want to load the contents off of the main thread, and then update the view once complete.
I've used this pattern in various places and noticed that in some cases the async call is on the main thread (by debugging) while in others it is on the Thread 4 Queue : com.apple.root.user-initiated-qos.cooperative (concurrent) thread
For example:
struct MyView: View {
#State var data = "some data"
var body: some View {
Button("refresh") {
// when the button is pressed refresh it
Task {
await refresh()
}
}.task {
// when the view appears
await refresh()
}
Text("the data is \(data)") // write the data which was refreshed async
}
}
func refresh() async {
do {
let res = try await anotherAyncFunction()
data = res // this is presumably wrong and off the main thread - obviously might not be correct but leave here for debug as well
} catch {
print("got error \(error)")
}
}
I created several different views using a similar pattern (.task block calling async functions)
In some cases the functions are long running (reading from disk) and that is happening on the main thread
Change Task { to Task.detached {.
From the Swift Language Guide:
To create an unstructured task that runs on the current actor, call the Task.init(priority:operation:) initializer. To create an unstructured task that’s not part of the current actor, known more specifically as a detached task, call the Task.detached(priority:operation:) class method.
When you call Task.init, the asynchronous code runs on the current actor, which, in this context is the main actor. This has the result of blocking the main thread.
By calling Task.detached, you allow the asynchronous work to happen off the main thread.
Using Swift structured concurrency, to run code on a background thread, use a detached task or (better) an actor.
I've made an aplication with vala where at some point I have to process a lot of files. I've created a window to choose a folder and then I get the paths of files and make some proces on them.
I've added a progress bar to this window to show how many files have been processed but for some reason it remains always empty.
Code about window:
this.files_window = new Gtk.Window();
this.files_window.window_position = Gtk.WindowPosition.CENTER;
this.files_window.destroy.connect (Gtk.main_quit);
// VBox:
Gtk.Box vbox = new Gtk.Box (Gtk.Orientation.VERTICAL, 5);
this.files_window.add (vbox);
// Buttons to open and close
Gtk.Button cancel = new Gtk.Button.with_label ("Cancel");
Gtk.Button select = new Gtk.Button.with_label ("Select");
vbox.add (select);
vbox.add (cancel);
// proogress bar
this.progress_bar = new Gtk.ProgressBar();
vbox.add(this.progress_bar);
// conect select to method do_stuff
select.clicked.connect (do_stuff);
this.files_window.show_all ();
As you can see, I connect the button "select" to the method "do_stuff" where I get the paths of selected files and make some process.
I update correctlly the fraction of the progres bar because I've added some prints to know if the value is correct and it is. It's just that the windows is not refreshing, possibly because all the work it is doing with the process of the files. Here is the code about do_stuff() method:
// some proces to get paths of files in the list sfiles
double fraction = 0.0;
this.progress_bar.set_fraction (fraction);
int processed_files = 0;
foreach (string sfile in sfiles) {
do_some_proces_to_file(sfile);
processed_files += 1;
fraction = (double)processed_files/(double)sfiles.length;
this.progress_bar.set_fraction (fraction);
stdout.printf("Real fraction: %f\n", this.progress_bar.get_fraction());
}
The printf shows that the value of the progres bar is being updated but in the window the bar is always empty.
Am I missing something? Is it the correct way to do the progres bar? Should I made another thread to do the stuff?
As #nemequ says, your code is blocking the main loop thread (which handles both user input and scheduling/drawing widget updates), hence it the progress bar is not updated until the method completes.
Using a thread is one way solve the problem, however using threads can lead to a lot of bugs however since it can be difficult to make even simple interactions between threads safe.
An async method avoids this by interleaving the code with the other work being done by the main loop. An async version of your do_stuff() would be pretty straight-forward to write, simply declare it async and put a yield in the for loop somewhere:
public async void do_stuff() {
...
foreach (string sfile in sfiles) {
// all of this is as before
do_some_proces_to_file(sfile);
processed_files += 1;
fraction = (double)processed_files/(double)sfiles.length;
this.progress_bar.set_fraction (fraction);
// Schedule the method to resume when idle, then
// yield control back to the caller
Idle.add(do_stuff.callback);
yield;
}
}
You can then kick it off from your click handler by calling: do_stuff.begin().
Unless there is some relevant code you're not showing, you're blocking the main loop. One option would be to do everything in a thread, and use an idle callback to update the UI. The basic idea is something like:
new GLib.Thread<void*>("file-processor", () => {
foreach (string sfile in sfiles) {
/* do stuff */
GLib.Idle.add(() => {
/* Update progress */
return false;
});
}
return null;
});
Depending on your application you may need to add a mutex to avoid race conditions. You may also need to add some logic for canceling the operation.
A better option might be to use a GLib.ThreadPool. You'd still want to update the UI from an idle callback, but this would allow each task to execute in parallel, which could provide a significant speed-up.
If I were you I'd probably wrap it all up in an async function to keep the API tidy, but you don't really have to.
I'm a long-time Objective-C user and slowly migrating towards Swift with new projects. I'm using CocoaPods for the bigger things and can't find a good library to cover this.
So I have this code inside my NSViewController viewDidLoad to start with:
_ = NSEvent.addGlobalMonitorForEventsMatchingMask(NSEventMask.KeyDownMask) {
(event) -> Void in
print("Event is \(event)")
}
let event = NSEvent.keyEventWithType(NSEventType.KeyDown,
location: CGPoint(x:0,y:0),
modifierFlags: NSEventModifierFlags(rawValue: 0),
timestamp: 0.0,
windowNumber: 0,
context: nil,
characters: "\n",
charactersIgnoringModifiers: "",
isARepeat: false,
keyCode: 0) //NSF7FunctionKey
NSApplication.sharedApplication().sendEvent(event!)
So the first event capturing works perfect after having my app checked in the System Preferences' Accessibility list. Anywhere in OS X it will capture key-presses. Now in the docs it says for Function-keys I should use keyEventWithType.
I found this gist and noticed that it addresses the same sharedApplication instance, yet I don't know how to catch the event. Do I delegate in a certain way? Also the F-key constant is int and the method says it only wants to receive Uint16. I can typecast it, but I guess I'm using it wrong.
Fixed it by using a CocoaPods pod that I found later. Works perfectly.
Is synchronous communication between JavaScript and Swift/Obj-C native code possible using the WKWebView?
These are the approaches I have tried and have failed.
Approach 1: Using script handlers
WKWebView's new way of receiving JS messages is by using the delegate method userContentController:didReceiveScriptMessage: which is invoked from JS by window.webkit.messageHandlers.myMsgHandler.postMessage('What's the meaning of life, native code?')
The problem with this approach is that during execution of the native delegate method, JS execution is not blocked, so we can't return a value by immediately invoking webView.evaluateJavaScript("something = 42", completionHandler: nil).
Example (JavaScript)
var something;
function getSomething() {
window.webkit.messageHandlers.myMsgHandler.postMessage("What's the meaning of life, native code?"); // Execution NOT blocking here :(
return something;
}
getSomething(); // Returns undefined
Example (Swift)
func userContentController(userContentController: WKUserContentController, didReceiveScriptMessage message: WKScriptMessage) {
webView.evaluateJavaScript("something = 42", completionHandler: nil)
}
Approach 2: Using a custom URL scheme
In JS, redirecting using window.location = "js://webView?hello=world" invokes the native WKNavigationDelegate methods, where the URL query parameters can be extracted. However, unlike the UIWebView, the delegate method is not blocking the JS execution, so immediately invoking evaluateJavaScript to pass a value back to the JS doesn't work here either.
Example (JavaScript)
var something;
function getSomething() {
window.location = "js://webView?question=meaning" // Execution NOT blocking here either :(
return something;
}
getSomething(); // Returns undefined
Example (Swift)
func webView(webView: WKWebView, decidePolicyForNavigationAction navigationAction: WKNavigationAction, decisionHandler decisionHandler: (WKNavigationActionPolicy) -> Void) {
webView.evaluateJavaScript("something = 42", completionHandler: nil)
decisionHandler(WKNavigationActionPolicy.Allow)
}
Approach 3: Using a custom URL scheme and an IFRAME
This approach only differs in the way that window.location is assigned. Instead of assigning it directly, the src attribute of an empty iframe is used.
Example (JavaScript)
var something;
function getSomething() {
var iframe = document.createElement("IFRAME");
iframe.setAttribute("src", "js://webView?hello=world");
document.documentElement.appendChild(iframe); // Execution NOT blocking here either :(
iframe.parentNode.removeChild(iframe);
iframe = null;
return something;
}
getSomething();
This nonetheless, is not a solution either, it invokes the same native method as Approach 2, which is not synchronous.
Appendix: How to achieve this with the old UIWebView
Example (JavaScript)
var something;
function getSomething() {
// window.location = "js://webView?question=meaning" // Execution is NOT blocking if you use this.
// Execution IS BLOCKING if you use this.
var iframe = document.createElement("IFRAME");
iframe.setAttribute("src", "js://webView?question=meaning");
document.documentElement.appendChild(iframe);
iframe.parentNode.removeChild(iframe);
iframe = null;
return something;
}
getSomething(); // Returns 42
Example (Swift)
func webView(webView: UIWebView, shouldStartLoadWithRequest request: NSURLRequest, navigationType: UIWebViewNavigationType) -> Bool {
webView.stringByEvaluatingJavaScriptFromString("something = 42")
}
No I don't believe it is possible due to the multi-process architecture of WKWebView. WKWebView runs in the same process as your application but it communicates with WebKit which runs in its own process (Introducing the Modern WebKit API). The JavaScript code will be running in the WebKit process. So essentially you are asking to have synchronous communication between two different processes which goes against their design.
I found a hack for doing synchronous communication but haven't tried it yet: https://stackoverflow.com/a/49474323/2870783
Edit: Basically you can use the the JS prompt() to carry your payload from the js side to the native side. In the native WKWebView will have to intercept the prompt call and decide if it is a normal call or if it is a jsbridge call. Then you can return your result as a callback to the prompt call. Because the prompt call is implemented in such a way that it waits for user input your javascript-native communication will be synchronous. The downside is that you can only communicate trough strings.
I also investigated this issue, and failed as you. To workaround, you have to pass a JavaScript function as a callback. Native function needs to evaluate the callback function to return result. Actually, this is the JavaScript way, because JavaScript never wait. Blocking JavaScript thread may cause ANR, it's very bad.
I have created a project named XWebView which can establish a bridge between native and JavaScript. It offers binding styled API for calling native from JavaScript and vice versa. There is a sample app.
It's possible to synchronously wait for the result of evaluateJavaScript by polling the current RunLoop's acceptInput method. What this does is allow your UI thread to respond to input while you wait for the the Javascript to finish.
Please read warnings before you blindly paste this into your code
//
// WKWebView.swift
//
// Created by Andrew Rondeau on 7/18/21.
//
import Cocoa
import WebKit
extension WKWebView {
func evaluateJavaScript(_ javaScriptString: String) throws -> Any? {
var result: Any? = nil
var error: Error? = nil
var waiting = true
self.evaluateJavaScript(javaScriptString) { (r, e) in
result = r
error = e
waiting = false
}
while waiting {
RunLoop.current.acceptInput(forMode: RunLoop.Mode.default, before: Date.distantFuture)
}
if let error = error {
throw error
}
return result
}
}
What happens is that, while the Javascript is executing, the thread calls RunLoop.current.acceptInput until waiting is false. This allows your application's UI to be responsive.
Some warnings:
Buttons, ect, on your UI will still respond. If you don't want someone to push a button while your Javascript is running, you should probably disable interacting with your UI. (This is especially the case if you're calling out to another server in Javascript.)
The multi-process nature of calling evaluateJavaScript may be slower than you expect. If you're calling code that is "instant," things may still slow down if you make repeated calls into Javascript in a tight loop.
I've only tested this on the Main UI thread. I don't know how this code will work on a background thread. If there are problems, investigate using a NSCondition.
I've only tested this on macOS. I do not know if this works on iOS.
I was facing a similar issue, i resolved it by storing promise callbacks.
The js that you load in your web view via WKUserContentController::addUserScript
var webClient = {
id: 1,
handlers: {},
};
webClient.onMessageReceive = (handle, error, data) => {
if (error && webClient.handlers[handle].reject) {
webClient.handlers[handle].reject(data);
} else if (webClient.handlers[handle].resolve){
webClient.handlers[handle].resolve(data);
}
delete webClient.handlers[handle];
};
webClient.sendMessage = (data) => {
return new Promise((resolve, reject) => {
const handle = 'm' + webClient.id++;
webClient.handlers[handle] = { resolve, reject };
window.webkit.messageHandlers.<message_handler_name>.postMessage({data: data, id: handle});
});
}
Perform Js Request like
webClient.sendMessage(<request_data>).then((response) => {
...
}).catch((reason) => {
...
});
Receive request in userContentController :didReceiveScriptMessage
Call evaluateJavaScript with webClient.onMessageReceive(handle, error, response_data).