2

I'm building a simple webapp with SwiftUI that simply load my website into the webview. From my website, it has to show the Javascript confirm for users to able to logout from the app and my function inside of runJavaScriptConfirmPanelWithMessage crashes the app when Javascript confirm is triggered.

Here is the entire code of my webapp.

import SwiftUI
import WebKit

struct Webview : UIViewRepresentable {
    let request: URLRequest
    var webview: WKWebView?

    init(web: WKWebView?, req: URLRequest) {
        self.webview = WKWebView()
        self.request = req
    }

    class Coordinator: NSObject, WKUIDelegate {
        var parent: Webview

        init(_ parent: Webview) {
            self.parent = parent
        }

        // Delegate methods go here

        func webView(_ webView: WKWebView,
        runJavaScriptAlertPanelWithMessage message: String,
             initiatedByFrame frame: WKFrameInfo,
             completionHandler: @escaping () -> Void) {

            // alert functionality goes here
            let alertController = UIAlertController(title: nil, message: message, preferredStyle: .alert)

            alertController.addAction(UIAlertAction(title: "OK", style: .default, handler: nil))

            alertController.present(alertController, animated: true)

            completionHandler()

        }

        func webView(_ webView: WKWebView,
        runJavaScriptConfirmPanelWithMessage message: String,
             initiatedByFrame frame: WKFrameInfo,
             completionHandler: @escaping (Bool) -> Void) {

            // confirm functionality goes here. THIS CRASHES THE APP
            let alertController = UIAlertController(title: nil, message: message, preferredStyle: .alert)

            alertController.addAction(UIAlertAction(title: "OK", style: .default, handler: { (action) in
                completionHandler(true)
            }))

            alertController.addAction(UIAlertAction(title: "Cancel", style: .default, handler: { (action) in
                completionHandler(false)
            }))

            alertController.present(alertController, animated: true, completion: nil)
        }

        func webView(_ webView: WKWebView,
        runJavaScriptTextInputPanelWithPrompt prompt: String,
                  defaultText: String?,
             initiatedByFrame frame: WKFrameInfo,
             completionHandler: @escaping (String?) -> Void) {

            // prompt functionality goes here

        }
    }

    func makeCoordinator() -> Coordinator {
        Coordinator(self)
    }

    func makeUIView(context: Context) -> WKWebView  {
        return webview!
    }

    func updateUIView(_ uiView: WKWebView, context: Context) {
        uiView.uiDelegate = context.coordinator
        uiView.load(request)
    }

    func goBack(){
        webview?.goBack()
    }

    func goForward(){
        webview?.goForward()
    }

    func reload(){
        webview?.reload()
    }
}

struct ContentView: View {

    let webview = Webview(web: nil, req: URLRequest(url: URL(string: "https://google.com")!))

    var body: some View {
        VStack {
            webview
            HStack() {
                Button(action: {
                    self.webview.goBack()
                }){
                    Image(systemName: "chevron.left")
                }.padding(32)

                Button(action: {
                    self.webview.reload()
                }){
                    Image(systemName: "arrow.clockwise")
                }.padding(32)

                Button(action: {
                    self.webview.goForward()
                }){
                    Image(systemName: "chevron.right")
                }.padding(32)
            }.frame(height: 40)
        }
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

I'm not sure what I did wrong in this section for runJavaScriptConfirmPanelWithMessage that crashes the app.

Here is the part that I'm having a trouble with.

        func webView(_ webView: WKWebView,
        runJavaScriptConfirmPanelWithMessage message: String,
             initiatedByFrame frame: WKFrameInfo,
             completionHandler: @escaping (Bool) -> Void) {

            // confirm functionality goes here. THIS CRASHES THE APP
            let alertController = UIAlertController(title: nil, message: message, preferredStyle: .alert)

            alertController.addAction(UIAlertAction(title: "OK", style: .default, handler: { (action) in
                completionHandler(true)
            }))

            alertController.addAction(UIAlertAction(title: "Cancel", style: .default, handler: { (action) in
                completionHandler(false)
            }))

            alertController.present(alertController, animated: true, completion: nil)
        }

Can anyone tell me what I'm doing wrong here?

Skate to Eat
  • 2,554
  • 5
  • 21
  • 53
  • You have to use SwiftUI `Alert` in this workflow. I would do it via additional wrapper around `Webview` with binding to present alert and, probably additional view model to transfer JS alert parameters. – Asperi Dec 19 '19 at 09:15
  • @Asperi Thank you for the comment. You meant I need to map Javascript Alert to SwiftUI alert(https://developer.apple.com/documentation/swiftui/alert) to make this work? Same thing goes for Confirm too then? – Skate to Eat Dec 19 '19 at 13:17
  • Yes, and these alerts should be presented via SwiftUI `View.alert` function – Asperi Dec 19 '19 at 13:29

2 Answers2

2

You need a View Controller to present alert. Try this:

func webView(_ webView: WKWebView, runJavaScriptConfirmPanelWithMessage message: String, initiatedByFrame frame: WKFrameInfo, completionHandler: @escaping (Bool) -> Void) {
    
    let alertController = UIAlertController(title: nil, message: message, preferredStyle: .actionSheet)
    alertController.addAction(
        UIAlertAction(title: "OK", style: .default, handler: { (action) in completionHandler(true) })
    )
    alertController.addAction(
        UIAlertAction(title: "Cancel", style: .default, handler: { (action) in completionHandler(false) })
    )
    
    if let controller = topMostViewController() {
        controller.present(alertController, animated: true, completion: nil)
    }
    
}

private func topMostViewController() -> UIViewController? {
    guard let rootController = keyWindow()?.rootViewController else {
        return nil
    }
    return topMostViewController(for: rootController)
}

private func keyWindow() -> UIWindow? {
    return UIApplication.shared.connectedScenes
        .filter {$0.activationState == .foregroundActive}
        .compactMap {$0 as? UIWindowScene}
        .first?.windows.filter {$0.isKeyWindow}.first
}

private func topMostViewController(for controller: UIViewController) -> UIViewController {
    if let presentedController = controller.presentedViewController {
        return topMostViewController(for: presentedController)
    } else if let navigationController = controller as? UINavigationController {
        guard let topController = navigationController.topViewController else {
            return navigationController
        }
        return topMostViewController(for: topController)
    } else if let tabController = controller as? UITabBarController {
        guard let topController = tabController.selectedViewController else {
            return tabController
        }
        return topMostViewController(for: topController)
    }
    return controller
}

Thanks to this answer https://stackoverflow.com/a/57877120/11212894

Artem Kovalev
  • 21
  • 1
  • 4
0

Please note that you're trying to present alertController from alertController and that's a crash reason.

lembrykos
  • 21
  • 3