How do I keep a WKWebView object from crashing? - ios

Scenario
I'm building an iOS application in Swift. One feature is to have a live video feed as the application background. The video feed is originating from a Raspberry Pi on a local network using sudo motion. Motion is successfully hosting the feed on default port 8081.
The Swift app has a WKWebView object with the source pointing to my Raspberry Pi's motion port.
Suspected Issue
The webpage at port 8081 is constantly refreshing to load the most recent frame from the camera.
Problem
When running the app, the feed connects successfully and loads the first frame, and occasionally a second but then cuts off.
On a few occasions I received the following error in the terminal: [ProcessSuspension] 0x282022a80 - ProcessAssertion() Unable to acquire assertion for process with PID 0 leading me to believe that it is a memory management issue related to the constantly refreshing nature of the webpage.
Current Configuration
Currently, my call to .load() the WKWebView object is in ViewController.swift > override func viewDidLoad().
Proposed Resolution
Do I need to build some form of loop structure where I load a frame, pause execution and then call the WKWebView to reload a new frame a few seconds later.
I'm very new to Swift so patience with my question format is highly appreciated.

The WkWebView and motion loading worked in Xcode 9 with iOS 11 versions but doesn't seem to work anymore with iOS 12. You are correct that the webkit is crashing on the second image.
Due to you being new to Swift I would advise reading this link on delegates because this solution I am providing will make more sense to you.
Swift Delegates
In summary, "Delegates are a design pattern that allows one object to send messages to another object when a specific event happens."
With this solution/hack we are going to use several of the WKNavigationDelegates to inform us when the WkWebView is doing specific tasks and inject our solution to the problem. You can find out all of the delegates the WKWebKit has here WKNavigationDelegates.
The following code can be used in a brand new iOS project and replace the code in the ViewController.swift. It requires no interface builder or IBOutlet connections. It will create a single web view on the view and point to the address 192.168.2.75:6789. I have included inline comments to attempt to explain what the code is doing.
We are loading the HTTP Response twice from motion in decidePolicyFor navigationResponse delegate and keeping track with a counter. I have left some print statements so you can see what the response is. The first is a header and the second is the image information.
When our counter gets to 3 items (ie the second image) we are forcing the wkWebView to cancel all navigation (ie stop loading) in the decidePolicyFor navigationResponse delegate. See the line with decisionHandler(.cancel). This is what is stopping the crash.
This leads us to receive the callback from the wkwebview delegate WebView didFail navigation. At this point we want load our motion/pi url again and start the loading process again.
We must then reset our counter so we can repeat this process until someone else comes up with a better solution.
import UIKit
import WebKit
class ViewController: UIViewController, WKNavigationDelegate {
// Memeber variables
var m_responseCount = 0; /* Counter to keep track of how many loads the webview has done.
this is a complete hack to get around the webkit crashing on
the second image load */
let m_urlRequest = URLRequest(url: URL(string: "http://192.168.2.75:6789")!) //Enter your pi ip:motionPort
var m_webView:WKWebView!
override func viewDidLoad() {
super.viewDidLoad()
m_webView = WKWebView(frame: self.view.frame) // Create our webview the same size as the viewcontroller
m_webView.navigationDelegate = self // Subscribe to the webview navigation delegate
}
override func viewDidAppear(_ animated: Bool) {
m_webView.load(m_urlRequest) // Load our first request
self.view.addSubview(m_webView) // Add our webview to the view controller view so we can see it
}
// MARK: - WKNavigation Delegates
func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: #escaping (WKNavigationActionPolicy) -> Void) {
print("decidePolicyFor navigationAction")
print(navigationAction.request) //This is the request to connect to the motion/pi server http::/192.168.2.75:6789
decisionHandler(.allow)
}
func webView(_ webView: WKWebView, decidePolicyFor navigationResponse: WKNavigationResponse, decisionHandler: #escaping (WKNavigationResponsePolicy) -> Void) {
print("decidePolicyFor navigationResponse")
print(navigationResponse.response) // This is HTML from the motion/rpi
/* We only want to load the html header and the first image
Loading the second image is causing the crash
m_responseCount = 0 - Header
m_responseCount = 1 - First Image
m_responseCount >= 2 - Second Image
*/
if(m_responseCount < 2)
{
decisionHandler(.allow)
}
else{
decisionHandler(.cancel) // This leads to webView::didFail Navigation Delegate to be called
}
m_responseCount += 1; // Incriment our counter
}
func webView(_ webView: WKWebView, didFail navigation: WKNavigation!, withError error: Error) {
/*
We have forced this failure in webView decidePolicyFor navigationResponse
by setting decisionHandler(.cancel)
*/
print("didFail navigation")
m_webView.load(m_urlRequest) //Lets load our webview again
m_responseCount = 0 /* We need to reset our counter so we can load the next header and image again
repeating the process forever
*/
}
func webViewWebContentProcessDidTerminate(_ webView: WKWebView) {
// If you find your wkwebview is still crashing break here for
// a stack trace
print("webViewWebContentProcessDidTerminate")
}
}
Note: You are also required to add the following to your info.plist file due to the motion/pi server response being http and not https
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsArbitraryLoadsInWebContent</key>
<true/>
<key>NSAllowsArbitraryLoads</key>
<true/>
</dict>
I encourage you to use this basic example and modify it to suit your application requirements. I also encourage you to post any findings of your own because I am having the exact same problem using the exact same hardware as you and this is a hack more than a solution.

Related

How to run Code after asynchronus code is finished, Swift

I have a asynchronus Funktion, loadHTMLString, that I call to load the Text out of an HTML File. The loading Process needs time, almost a second. The Problem is: I want to go on with my Code, if the File is loaded, but I dont know when the loading is finished. Is there a way to do that?
func generateAndLoadPDF() {
// Thats my HTML File
let html = HTML.get(from: "Vorlage.html")
// I load this HTML File in my WebView, that takes almost a second
wkWebView.loadHTMLString(html, baseURL: Bundle.main.bundleURL)
// I delayed the Following Code, so the HTML-String has time to load
// Actually I dont want to delay the Code, I want that the following Code runs after .loadHTMLString is finished.
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
// I generate the PDF
let wkPDFData = PDF.generate(using: self.wkWebViewPrintFormatter())
self.loadIntoWKWebView(wkPDFData)
}
}
Thanks,
Boothosh
Conform to WKNavigationDelegate
webView.navigationDelegate = self
extension ViewController: WKNavigationDelegate {
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
debugPrint("didFinish")
}
}

Opening external links in WKWebView (not externally)

My app contains HTML information pages opened in WKWebView, containing some hyperlinks. These used to work, but stopped working with recent iOS releases with error messages, containing words
Could not create sandbox extension
Connection to daemon was invalidated
When I apply WKNavigation delegate with the following code
extension InformationPageController: WKNavigationDelegate {
func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: ((WKNavigationActionPolicy) -> Void)) {
var policy = WKNavigationActionPolicy.allow
if navigationAction.navigationType == .linkActivated {
// Neither of commented lines below works!
// webView.load(navigationAction.request)
// webView.loadHTMLString(navigationAction.request.url!.absoluteString, baseURL: nil)
UIApplication.shared.openURL(navigationAction.request.url!)
policy = .cancel
}
decisionHandler(policy)
}
}
the pages are opened in an external browser, but not inside the app.
Is there a way to open the links as before, within same WKWebView?
It looks like only https protocol is currently accepted by WKWebView. If you have just http, WKWebView will complain about insecure protocol and won't open.
After I changed http-s to https-s, the pages opened, but not AppStore pages. This leads to another issue.
When URL address starts with https://apps.apple.com/..., iOS (but not macOS) redirects it to itms-appss://apps.apple.com/... for opening in AppStore app, causing WKWebView to come up with the error: Redirection to URL with a scheme that is not HTTP(S). Similar problem with mailto urls.
To bypass these issues I modified code to the following:
extension InformationPageController: WKNavigationDelegate {
static let disallowedUrlPrefixes = ["https://apps.apple.com", "mailto:"]
func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: ((WKNavigationActionPolicy) -> Void)) {
var policy = WKNavigationActionPolicy.allow
if navigationAction.navigationType == .linkActivated,
let url = navigationAction.request.url {
let urlString = url.absoluteString
if InformationPageController.disallowedUrlPrefixes.first(where:{urlString.starts(with: $0)}) != nil {
UIApplication.shared.openURL(url)
policy = .cancel
}
}
decisionHandler(policy)
}
}
Now the 'troublesome' pages are opened with an external browser, all others are opened within WKWebView. Unfortunately that's the best I can currently think of.
Perhaps it's because an iOS app by default doesn't allow http requests. Try to allow it by adding "App Transport Security Settings" and add a sub key "Allow Arbitrary Loads" and set it to true in the app's info.plist:
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsArbitraryLoads</key>
<true/>
</dict>

Is there a way to determined what element is at a point in a WKWebView or SFSafariViewController view?

I would like to know what element is "under" a particular point in a web view. For example, in the screenshot below, I would like to know what element is at point 550x, 275y. I'm hoping to get the result:
<img alt=​"Typhoon Mangkhut approaching the Philippines on September 14" src=​"/​/​upload.wikimedia.org/​wikipedia/​commons/​thumb/​3/​30/​Mangkhut_2018-09-14_0750Z.jpg/​140px-Mangkhut_2018-09-14_0750Z.jpg" width=​"140" height=​"111" srcset=​"/​/​upload.wikimedia.org/​wikipedia/​commons/​thumb/​3/​30/​Mangkhut_2018-09-14_0750Z.jpg/​210px-Mangkhut_2018-09-14_0750Z.jpg 1.5x, /​/​upload.wikimedia.org/​wikipedia/​commons/​thumb/​3/​30/​Mangkhut_2018-09-14_0750Z.jpg/​280px-Mangkhut_2018-09-14_0750Z.jpg 2x" data-file-width=​"1219" data-file-height=​"963">​
If the webview supports it, you could have javascript determine what's there, and report it to the webview. I apologize but I haven't run this code, I don't have the means to test it at the moment.
If you don't have the ability to add javascript to the page then you'll have to do it all in WKWebView .evaluateJavaScript
find the element value with:
var elem = document.elementFromPoint(550, 275);
Then tell your webview about it by creating a custom scheme request. you can do it this way:
var iframe = document.createElement("IFRAME");
iframe.setAttribute("src", "customScheme://" + JSON.stringify(elem);
document.documentElement.appendChild(iframe);
iframe.parentNode.removeChild(iframe);
iframe = null;
Intercept it in the webview delegate.
public func webView(_ webView: WKWebView,
decidePolicyFor navigationAction: WKNavigationAction,
decisionHandler: #escaping (WKNavigationActionPolicy) -> Void) {
let url = navigationAction.request.url
if(url.range(of:"customScheme://") != nil){
// this is your reported info
}
return true
}
This approach will only tell you the element as it relates to the web browser coordinates, not it's position on the device's screen. If you want to do it via device position you'll have to translate that and pass the coordinates over to the webview by invoking some javascript on the device.

swift WebView open unwanted browser after navigation buttons

My webview opens a webpage and works well except when it randomly opens the page in the browser after goBack or goForward methods.
I've searched for solutions but didn't find anything about preventing the browser from opening.
#IBAction func didClickNextBtn(_ sender: Any) {
if webView.canGoForward {
webView.goForward()
}
}
This issue happens when I push this buttons instantly (or the same happens when the internet connection off and trying to go back or forward )
I tried to disable the buttons while it's loading the page but that not helped and that way feel wrong
You could try to implement func webView(WKWebView, decidePolicyFor: WKNavigationAction, decisionHandler: (WKNavigationActionPolicy) -> Void) from WKNavigationDelegate, and print all navigationAction.
If some wired urls lead this issues, just call decisionHandler(.cancel) within this method to block them.

Changing the requests from HTTPS to HTTP from WKWebview?

My requirement is to redirecting the HTTPS request to HTTP from WKWebView.
But the request initiated inside the webview(Ajax call) are not being captured.
After that, the requests being made from that webpage are not captured.
Please share your ideas. Much appreciated.
You would need to implement a WebPolicyDelegate with a decidePolicyForNavigationAction and then load the page yourself without https. This should work, but I haven't tested it.
Have a look at this post for some insides, on UIWebView it would have worked with shouldStartLoadWithRequest:
Migrating from UIWebView to WKWebView
Use the following steps for UIWevView to load a remote address
Load the url address
let urlString = "http://www.example.com"
webView.loadRequest(NSURLRequest(url: NSURL(string:"url) as! URL) as URLRequest)
Conform UIWebViewDelegate in ViewController
After conforming UIWebViewDelegate, use the following delegates
/* Delegates for WebView */
fun webViewDidStartLoad(_ webView: UIWebView){
// Control loading the url
}
func webViewDidFinishLoad(_ webView: UIWebView){
// statements when loading finishes
}
fun webView(_ webView: UIWebView, didFailLoadWithError error: Error){
// Handle error here
}
If still does not work, add a row for "App Transport Security Settings" in your project Info.plist. Here is screenshot for it settings.

Resources