How to know when my WKWebview derived subview is loaded in Swift - ios

I have a view, which loads a subview after certain actions have happened, within the viewDidLoad() method:
override func viewDidLoad() {
super.viewDidLoad()
//OTHER STUFF...
let config = WKWebViewConfiguration()
config.userContentController = contentController
self.myWebView = WKWebView(
frame: self.containerView.bounds,
configuration: config
)
self.myWebView.navigationDelegate = self
self.view.addSubview(self.myWebView)
}
I need to do some checks once the web view has loaded. How can I do something like:
webSubviewDidLoad() {
//do stuff
}

there is a specific delegate didFinishNavigation
didFinishNavigation documentation
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let configuration = WKWebViewConfiguration()
let webView = WKWebView(frame: .zero, configuration: configuration)
webView.translatesAutoresizingMaskIntoConstraints = false
webView.navigationDelegate = self
view.addSubview(webView)
/* add layout constraints that make the webview fitting in your view */
if let url = URL(string: "https://google.com") {
webView.load(URLRequest(url: url))
}
}
}
extension ViewController: WKNavigationDelegate {
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
print("Finished navigating to url \(webView.url)")
}
}
or you can check the estimatedProgress property for the progress
estimatedProgress documentation

Step 1. Add an observer to be notified when your web view finishes loading like below. This code should be placed within viewDidLoad method of your view controller class.
myWebView.addObserver(self, forKeyPath: #keyPath(WKWebView.isLoading), options: .new, context: nil)
Step 2. Implement the observeValue(forKeyPath:) method in your view controller, doing whatever actions you need to do like below.
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
if keyPath == "isLoading" && !myWebView.isLoading {
// Do your needed action here
}
}

Implement your own WebView:
import WebKit
protocol WebViewDelegate {
func didLayoutSubviews()
}
class WebView : WKWebView {
weak var delegate: WebViewDelegate?
override func layoutSubviews() {
super.layoutSubviews()
self.delegate?.didLayoutSubviews()
}
}

Related

Making certain links in WKWebView open in Safari, not the Webview

I have a WebView app which contains external links that I wish to have users open within Safari as opposed to the webview itself, on tap. I believe it has something to do with a Navigation Delegate but I am new to iOS Dev and have no idea where to start! Below is my code as it is today. If you can tell me specifically what changes to make and where to put in any code, that would make my life so much easier. Thanks everyone in advance for any help! I think there's a way along the lines of setting a Navigation delegate such that all URL's that start with https://example-root.com/ open normal, in the webview since they are native nav buttons but all other URL's I want to open in safari on tap.
import UIKit
import WebKit
class ViewController: UIViewController {
let webView: WKWebView = {
let prefs = WKWebpagePreferences()
prefs.allowsContentJavaScript = true
let configuration = WKWebViewConfiguration()
configuration.defaultWebpagePreferences = prefs
let webView = WKWebView(frame: .zero, configuration: configuration)
return webView
}()
override func viewDidLoad() {
super.viewDidLoad()
view.addSubview(webView)
// Do any additional setup after loading the view.
guard let url = URL(string: "https://example-root.com/") else {
return
}
webView.load(URLRequest(url: url))
DispatchQueue.main.asyncAfter(deadline: .now()+5) {
self.webView.evaluateJavaScript("document.body.innerHTML") { result, error in guard lethtml = result as? String, error == nil else {
return
}
}
}
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
webView.frame = view.bounds
}
}
You're right that you'll need to use the NavigationDelegate to intercept the navigation action. Make your ViewController conform to WKNavigationDelegate and implement the webView(_:decidePolicyFor:decisionHandler:) method:
extension ViewController: WKNavigationDelegate {
func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: #escaping (WKNavigationActionPolicy) -> Void) {
if let url = navigationAction.request.url, !url.host.contains("example-root.com") {
UIApplication.shared.open(url)
decisionHandler(.cancel)
return
}
decisionHandler(.allow)
}
}
Don't forget to set the navigationDelegate property of your WKWebView, you can do this in viewDidLoad with the following:
webView.navigationDelegate = self

Cannot convert value type to expected argument type error?

working on my swift app here.
Below is the code I have,
struct WebView: UIViewRepresentable {
#State var allowsInLineMediaPlayback = true
let request: URLRequest//pass the website to webkit
func makeUIView(context: Context) -> WKWebView {
let configuration = WKWebViewConfiguration()
configuration.allowsInlineMediaPlayback = true
configuration.mediaTypesRequiringUserActionForPlayback = []
let webView = WKWebView(frame: .zero, configuration: configuration)
return webView
}
func updateUIView(_ uiView: WKWebView, context: Context) {
uiView.load(request)
}
}
class ViewController: UIViewController {
var webView = WKWebView()
override func viewDidLoad() {
super.viewDidLoad()
NotificationCenter.default.addObserver(self, selector: #selector(didBecomeActive),
name: UIApplication.didBecomeActiveNotification, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(willResignActive),
name: UIApplication.willResignActiveNotification, object: nil)
}
#objc func willResignActive(){
disableIgnoreSilentSwitch(webView)
}
#objc func didBecomeActive() {
//Always creates new js Audio object to ensure the audio session behaves correctly
forceIgnoreSilentHardwareSwitch(webView, initialSetup: false)
}
and I am getting an error with this part with webView,
disableIgnoreSilentSwitch(webView)
The error states
Cannot convert value of type '(WKWebView, WKNavigation?) -> ()' to expected argument type 'WKWebView'
Thanks in advance.
If it helps, I am using the code from here as reference. How to force WKWebView to ignore hardware silent switch on iOS?

Load WKWebView in background/off screen

Is there a way to load a WKWebView in the background/off-screen so I can hide the loading behind another view?
You can add the WKWebView to the view hierarchy but its x is your current width, so it lays out of the screen but within the rendering hierarchy.
Add WKNavigationDelegate to your class and add it to the webView like
webView.navigationDelegate = self
Then implement the following function:
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation)
This function and some others are called when the webview finished loading the content, BUT this does not include the 100% finished rendering. After that there is still some time required for rendering, the time consumption for this depends on the content that was loaded.
There currently is no callback for the 100% finished loading and rendering, so if you know the files, you can calculate or use a fix delay before moving the webView into the visible rect.
OR
If you feel fine with that, you observe private values in the webview and move your webview after those value changes to your preferred state. This looks for example like that:
class MyCustomWKWebView: WKWebView {
func setupWebView(link: String) {
let url = NSURL(string: link)
let request = NSURLRequest(url: url! as URL)
load(request as URLRequest)
addObserver(self, forKeyPath: "loading", options: .new, context: nil)
}
override public func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
guard let _ = object as? WKWebView else { return }
guard let keyPath = keyPath else { return }
guard let change = change else { return }
switch keyPath {
case "loading":
if let val = change[NSKeyValueChangeKey.newKey] as? Bool {
//do something!
}
default:
break
}
}
deinit {
removeObserver(self, forKeyPath: "loading")
}
}

How do I dismiss a progressView when it's fully loaded?

I have a progress view that tracks the status of a Youtube video loading via WKWebView. When it's loaded, I want to dismiss the progressView because when I click back onto the main page of my app, the status bar is still there (but it doesn't display anything and takes up space, blocking other elements of my app). How do I, when the progressView is completely loaded, dismiss the progressView?
This is my class that deals with the WKWebView.
import UIKit
import WebKit
class VideoPlayViewController: UIViewController, WKNavigationDelegate {
var webView: WKWebView!
var progressView: UIProgressView!
var videoURL: String = ""
override func loadView() {
webView = WKWebView()
webView.navigationDelegate = self
view = webView
}
override func viewDidLoad() {
super.viewDidLoad()
let url = URL(string: videoURL)!
webView.load(URLRequest(url: url))
webView.allowsBackForwardNavigationGestures = true
//Progress View/ Refresh Button
progressView = UIProgressView(progressViewStyle: .default)
progressView.sizeToFit()
let progressButton = UIBarButtonItem(customView: progressView)
progressView.progressTintColor = UIColor(red: 254, green: 53, blue: 98)
let spacer = UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: nil, action: nil)
let refresh = UIBarButtonItem(barButtonSystemItem: .refresh, target: webView, action: #selector(webView.reload))
refresh.tintColor = UIColor(red: 254, green: 53, blue: 98)
toolbarItems = [progressButton, spacer, refresh]
navigationController?.isToolbarHidden = false
// Lets the progress bar change according to what the Observer sends back (# from 0-1)
webView.addObserver(self, forKeyPath: #keyPath(WKWebView.estimatedProgress), options: .new, context: nil)
}
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
if keyPath == "estimatedProgress" {
progressView.progress = Float(webView.estimatedProgress)
}
}
}
If you need more info / more code, let me know.
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
guard let keyPath = keyPath else { return }
switch keyPath {
case "estimatedProgress":
// If you are using a `UIProgressView`, this is how you update the progress
progressView.isHidden = webView.estimatedProgress == 1
progressView.progress = Float(webView.estimatedProgress)
default:
break
}
}
and don't forgot to deinitialize the observers
deinit {
//remove all observers
webView.removeObserver(self, forKeyPath: "estimatedProgress")
}
Check webView(_:didFinish:) method of WKNavigationDelegate
You have to set typeable:
public typealias isCompletion = (_ isCompleted: Bool?) -> Void
And add variables like this in class:
var completion: isCompletion?
Now you have to create a method :
func webViewLoad(_ iscompleted:#escaping isCompletion){
completion = iscompleted
let url = URL(string: videoURL)!
webView.load(URLRequest(url: url))
webView.allowsBackForwardNavigationGestures = true
}
You have to set completion in the delegate method
func webView(webView: WKWebView, didFinishNavigation navigation: WKNavigation!) {
print("finish to load")
completion(true)
}
You can call this method from viewdidload like this:
// show loading from here
webViewLoad { (flag) in
//hide loading here
}

WKWebView on link click listener?

Does there exist something like a onLinkClickListener in the WKWebView class? I tried googling it but found nothing, I also found some unanswered questions on stackoverflow of simillar type.
The reason I need a linkClickListener is that, when I click on a link and the page did not load yet, it does not load the website. I also could create a fancy loading screen, when the page is loading with the listener.
You can do it like this
add WKNavigationDelegate to your class
class ViewController: UIViewController, WKNavigationDelegate
set a navigation delegate
yourWKWebview.navigationDelegate = self
after that you will be able to use decidePolicyFor navigationAction function
func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: #escaping (WKNavigationActionPolicy) -> Void) {
if navigationAction.navigationType == WKNavigationType.linkActivated {
print("link")
decisionHandler(WKNavigationActionPolicy.cancel)
return
}
print("no link")
decisionHandler(WKNavigationActionPolicy.allow)
}
Here is the solution you were looking for
Original answer from Bala: https://stackoverflow.com/a/44408807/8661382
Create WkNavigationDelegate to your class:
class ViewController: UIViewController, WKNavigationDelegate {
}
Override the method loadView and add an observer like this:
override func loadView() {
webView = WKWebView()
webView.navigationDelegate = self
webView.addObserver(self, forKeyPath: "URL", options: [.new, .old], context: nil)
view = webView
}
In viewDidLoad add an url to your webView.:
override func viewDidLoad() {
super.viewDidLoad()
self.navigationController?.setNavigationBarHidden(false, animated: true)
let url = URL(string: "https://www.hackingwithswift.com")!
webView.load(URLRequest(url: url))
webView.allowsBackForwardNavigationGestures = true
}
Finally, most importantly override observerValue method like this:
override func observeValue(forKeyPath keyPath: String?, of object: Any?,
change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
if let newValue = change?[.newKey] as? Int, let oldValue = change?
[.oldKey] as? Int, newValue != oldValue {
//Value Changed
print(change?[.newKey] as Any)
}else{
//Value not Changed
print(change?[.oldKey] as Any)
}
}
This will print the link you click on webView before loading the link.

Resources