0

What I am dealing with: web page + backend (php), swift app (wkwebview + some native components).

What I am trying to achieve: I need to let the web page know my app is changing it's state, by executing some java scripts (one script for "enter background mode", another one for "enter foreground", and so on).

What is my issue: I can't handle the case of app termination. Whenever the user is killing the app by home-double-click/swipe up, ApplicationWillTerminate method from AppDelegate is being called, but it fails to execute webView.evaluateJavaScript from here, as far as I was able to understand - due to the closure / completion handler is has, for it is async by design.

All other cases are covered, works fine, and with the last one for termination I am stuck.

I am playing around with the following code:

func applicationWillTerminate(_ application: UIApplication) {
        if let callback = unloadListenerCallback {
            if callback == "" {
                debugPrint("got null callback - stop listening")
                unloadListenerCallback = nil
            }
            else {
                jsCallbackInjection(s: callback) //executing JS callback injection into WebView

            }
        }
    }

unloadListenerCallback is the string? variable, works as a charm in other cases (just to mention it for clarity). jsCallbackInjection is the function, also works perfectly elsewhere:

func jsCallbackInjection(s: String) {
        let jscallbackname = s
        HomeController.instance.webView?.evaluateJavaScript(jscallbackname) { (result, error) in
             if error == nil {
                 print(result as Any)
                 print("Executed js callback: \(jscallbackname)")
             } else {
                 print("Error is \(String(describing: error)), js callback is: \(jscallbackname)")
             }
         }
    }

How do I made this code working, if possible? Any ideas, suggestions, tips or tricks? If not possible, any ideas on "how do I let my web counterpart know my app was terminated"? What do I do from here?

This is a cross-post from Apple Dev forum, where it is sitting for >1 week unattended.

halfer
  • 19,824
  • 17
  • 99
  • 186
  • try running it in a background thread – udi Sep 28 '22 at 13:36
  • Thanks Udi! I'll try, but it seems a bit wrong for me - as soon as webview is a part of UI and I will likely have issues trying to operate it from any thread other than main. Or am I making any mistake here? – Andrey Lesnykh Sep 28 '22 at 13:54
  • ...just checked with background, seems that doesn't work either :( else { DispatchQueue.global(qos: .background).async { self.jsCallbackInjection(s: callback) //executing JS callback injection into WebView } } – Andrey Lesnykh Sep 28 '22 at 14:23
  • "I can't handle the case of app termination" - indeed you can't, and you shouldn't. Your web app needs to handle such case on its own, just like it has to handle the case when user closes / kills the browser. User is in control of app/browser termination, not your app or your web app. – timbre timbre Sep 28 '22 at 14:43
  • Well, maybe I can comment a bit on this. The app is used (among other purposes) for video calls, which are 100% handled on php side (e.g. app is receiving voip push, handling it and reacting properly by opening the call page). The trick here is to let the web part know, for ex, if the app was killed by user - to handle the call state properly. You can say it is a bit strange way to deal with voip calls, and I will agree, but still - that's the way that it is now. In the end, I am ok with any possible way of sending "app terminated" flag to server, but still JS is preferred. – Andrey Lesnykh Sep 28 '22 at 15:47
  • I think your best option is a "I'm still alive" polling loop. When your server doesn't get an "I'm still alive" message after a timeout period, the server can clean up the session automatically. – Jroonk Sep 29 '22 at 05:34
  • I tried to wrap my call with DispatchQueue.global().async an semaphore, so that I can lock main, call my JS and signal in the completion closure. Looks promising (yes, I know it's a bad idea to lock the main thread, but it should not be a big deal as soon as the app was just intentionally killed by user). One thing though is that I can't even test the JS execution, because doing so in DispatchQueue.global() makes my call illegal (UI elements such as webview are to be in main thread). DispatchQueue.main will obviously deadlock, for I am locking it with my own hands by calling semaphore.wait. – Andrey Lesnykh Oct 02 '22 at 09:39
  • What comes to my mind so far: get current session & context info, somehow (?) to execute the JS, avoiding evaluateJavaScript, and thus to solve my issue. If it does not work, I'll probably contact my web app team to change it from JS into something like post request, which is (at least) OK to be executed in non-main thread and should be fine with this semaphore workaround. I am now playing with the solution provided by G Wesley in comments to https://stackoverflow.com/questions/55234914/can-i-make-an-api-call-when-the-user-terminates-the-app – Andrey Lesnykh Oct 02 '22 at 09:45

1 Answers1

0

Answering my own question:

I was not able to find a way of running JS from ApplicationWillTerminate.

However, I found a way of solving my issue, which is - instead of running JS, I am posting to my web service like that:

    func applicationWillTerminate(_ application: UIApplication) {
    let semaphore = DispatchSemaphore(value: 0)
    //setup your request here - Alamofire in my case
            
            DispatchQueue.global(qos: .background).async {
               //make your request here
                                onComplete: { (response: Update) in
                    //handle response if needed
                    semaphore.signal()
                },
                                onFail: {
                    //handle failure if needed
                    semaphore.signal()
                })
            }
            semaphore.wait(timeout: .distantFuture)
    }

This way, I am able to consistently report my app termination to the web page. I was lucky enough to have ajax already set up on the other end of a pipe, which I am just POSTing into with the simple AF request, so I don't need to struggle with JS anymore.

Basing on what I was able to find, there is NO suitable way of managing JS execution, due to

  • with semaphores, as soon as webview and it's methods are to be handled in main thread, you'll deadlock by using semaphore.wait()
  • no way I was able to find to run evaluateJavaScript synchronously
  • no way I was able to find to run the JS itself from JavaScriptCore, or some other way, within the same session and context

However, if someone still can contribute and provide solution, I'll be happy to accept it!