Load WKWebView in background/off screen - ios

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")
}
}

Related

How to detect AVplayer and get url of current video from WKWebView?

I'm using below code to extract url from UIWebView: it is working fine but, this same code using for WKWebView it's not working anymore. Can anyone help me? The video playing in WKWebView is Inlineplacyback not in fullscreen.
My code is :
NotificationCenter.default.addObserver(self, selector: #selector(self.playerItemBecameCurrent(_:)), name: NSNotification.Name("AVPlayerItemBecameCurrentNotification"), object: nil)
#objc func playerItemBecameCurrent(_ sender : NSNotification){
let playerItem: AVPlayerItem? = sender.object as? AVPlayerItem
if playerItem == nil {
print("player item nil")
return
}
// Break down the AVPlayerItem to get to the path
let asset = playerItem?.asset as? AVURLAsset
let url: URL? = asset?.url
let path = url?.absoluteString
print(path!,"video url")
}
Response URL :
https://r2---sn-po4g5uxa-5hql.googlevideo.com/videoplayback?txp=5531432&sparams=clen%2Cdur%2Cei%2Cgir%2Cid%2Cinitcwndbps%2Cip%2Cipbits%2Citag%2Clmt%2Cmime%2Cmm%2Cmn%2Cms%2Cmv%2Cpcm2%2Cpl%2Cratebypass%2Crequiressl%2Csource%2Cexpire&ip=103.37.181.55&ratebypass=yes&id=o-AM9UWIaxopyYZX4gikGuswG8EMi3dhH_PPBMIqY5cbXj&expire=1554400796&c=MWEB&fvip=4&initcwndbps=481250&ipbits=0&mime=video%2Fmp4&dur=60.093&lmt=1554142002789460&key=yt6&mt=1554379078&itag=18&source=youtube&gir=yes&requiressl=yes&signature=6C68366FC249958BB8E95A5D88074FF8BCB99745.DA113E66DD0B46863BAE52DAA3CAB31FD141F0E5&clen=2708520&mm=31%2C29&mn=sn-po4g5uxa-5hql%2Csn-cvh7knek&ei=vPGlXPOWHIWD8QOO1KBo&ms=au%2Crdu&pcm2=no&pl=24&mv=m&cpn=I9d32bNmeq3kf0jn&cver=2.20190403&ptk=youtube_none&pltype=contentugc
It's video URL not Webpage URL so, please help me how to get this.
Thanks.
This is kind of a hack, but the only way I found to accomplish this.
First set yourself as WKWebView navigation delegate:
self.webView?.navigationDelegate = self
Now listen to all navigation changes, and save the requested url:
func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: #escaping (WKNavigationActionPolicy) -> Void) {
if let urlStr = navigationAction.request.url?.absoluteString {
//Save presented URL
//Full path can be accessed via self.webview.url
}
decisionHandler(.allow)
}
Now you only need to know when does the new screen become visible, and use the URL you saved (To know the video URL of the new visible screen).
You can do this via listening to UIWindowDidBecomeVisibleNotification notification:
NotificationCenter.default.addObserver(self, selector: #selector(windowDidBecomeVisibleNotification(notif:)), name: NSNotification.Name("UIWindowDidBecomeVisibleNotification"), object: nil)
Then check if the navigation window is not your window, and that means a new screen did open:
#objc func windowDidBecomeVisibleNotification(notif: Notification) {
if let isWindow = notif.object as? UIWindow {
if (isWindow !== self.view.window) {
print("New window did open, check what is the currect URL")
}
}
}
You can try to inject JS in your WKWebView like shown here: https://paulofierro.com/blog/2015/10/12/listening-for-video-playback-within-a-wkwebview
retrieves the full URL from the request property of the navigation action in the webView(_:decidePolicyFor:decisionHandler:) method of WKNavigationDelegate.
func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: #escaping (WKNavigationActionPolicy) -> Void) {
if let urlStr = navigationAction.request.url?.absoluteString {
//urlStr is your URL
}
decisionHandler(.allow)
}
also don't forgot to conform protocol
webView.navigationDelegate = self
Using Swift
You can get the html content from url of webview using code below
let docString = webView.stringByEvaluatingJavaScriptFromString("document.documentElement.outerHTML")
This case you'll get the whole html content,
Then look for href links inside the html string
let regex = try! NSRegularExpression(pattern: "<a[^>]+href=\"(.*?)\"[^>]*>")
let range = NSMakeRange(0, docString.characters.count)
let matches = regex.matches(in: docString, range: range)
for match in matches {
let htmlLessString = (docString as NSString).substring(with: match.rangeAt(1))
print(htmlLessString)
}
Check if it is youtube url using
Regular expression: "#https?://(www.)?youtube.com/.[^\s.,"\']+#i"
Another way to achive this
Think outside the box!
You can make an api call to get the url. That seems pretty easy using the web languages like php, .net etc.
The code for getting all urls inside a webpage in PHP (Use whatever language that is okey for you)
$url="http://wwww.somewhere.com";
$data=file_get_contents($url);
$data = strip_tags($data,"<a>");
$d = preg_split("/<\/a>/",$data);
foreach ( $d as $k=>$u ){
if( strpos($u, "<a href=") !== FALSE ){
$u = preg_replace("/.*<a\s+href=\"/sm","",$u);
$u = preg_replace("/\".*/","",$u);
print $u."\n";
}
}
To check one by one if it is youtube url.
$sText = "Check out my latest video here http://www.youtube.com/?123";
preg_match_all('#https?://(www\.)?youtube.com/.[^\s.,"\']+#i', $sText, $aMatches);
var_dump($aMatches);
If you wanted to check if the sample apps are using the same method, get a web debugging proxy and dig on it
Many of the above explanations are taken from other sites.
I Hope it sum up to your need!
Happy coding!
Try this in your ViewController, to add an URL Observer on WKWebView:
override func loadView() {
let webConfig = WKWebViewConfiguration()
webView = WKWebView(frame: .zero, configuration: webConfig)
webView.addObserver(self, forKeyPath: "URL", options: .new, context: nil)
view = webView
}
Overriding the observeValue to get url request:
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
if keyPath == #keyPath(WKWebView.url) {
let urlRequest:String = webView.url?.absoluteString ?? ""
print(urlRequest)
}
}
Finally... deinit the Observer:
deinit { webView.removeObserver(self, forKeyPath: "URL") }
In WKWebView we need add the configuration for Inlineplacyback is true in WKWebViewConfiguration. If the configuration is set in WKWebView its automatically takes to full screen view.
Below code for reference:
class WebViewController: UIViewController {
lazy var webView: WKWebView! = {
let webView = WKWebView(frame: .zero, configuration: configuration)
webView.translatesAutoresizingMaskIntoConstraints = false
webView.uiDelegate = self
webView.navigationDelegate = self
let request = URLRequest(url: .init(string: "https://developer.apple.com/videos/play/wwdc2020/10188/")!)
webView.load(request)
return webView
}()
lazy var configuration: WKWebViewConfiguration! = {
let configuration = WKWebViewConfiguration()
configuration.allowsInlineMediaPlayback = true
configuration.mediaTypesRequiringUserActionForPlayback = .audio
configuration.allowsPictureInPictureMediaPlayback = true
return configuration
}()
override func loadView() {
super.loadView()
self.view.backgroundColor = .white
self.view.addSubview(self.webView)
// Constraint
NSLayoutConstraint.activate([
self.webView.topAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.topAnchor),
self.webView.bottomAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.bottomAnchor),
self.webView.leadingAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.leadingAnchor),
self.webView.trailingAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.trailingAnchor),
])
}
}
extension WebViewController: WKUIDelegate{
}
extension WebViewController: WKNavigationDelegate{
func webView(_ webView: WKWebView, decidePolicyFor navigationResponse: WKNavigationResponse) async -> WKNavigationResponsePolicy {
debugPrint("---------------------------- decidePolicyFor navigationResponse")
return .allow
}
func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, preferences: WKWebpagePreferences, decisionHandler: #escaping (WKNavigationActionPolicy, WKWebpagePreferences) -> Void) {
debugPrint("---------------------------- decidePolicyFor navigationAction")
decisionHandler(.allow, .init())
}
}
Click here to see sample output video

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

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()
}
}

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.

NSBundleResourceRequest No Progress Update

When I try to observe the progress of a NSBundleResourceRequest, observeValue(forKeyPath: object: change: context:) is not called for the .new observing option. Therefore, the NSProgressIndicator isn't updated. Here is the setup and code:
Setup:
Xcode 8.3.1
Deployment Target iOS 10.3
Device: iPad 4
Resource tags (368 KB) are located located in Download Only On Demand and consist of thumbnails displayed in a UICollectionView. Thumbnail images are located in MyCollection.xcassets. All IBOutlets are connected.
Images are correctly displayed in the collection view, but progress bar remains at zero.
Code:
final class MyCollectionVC: UIViewController, UICollectionViewDataSource, UICollectionViewDelegate {
var myCollectionVCResourceRequest: NSBundleResourceRequest!
var myCollectionVCResourceRequestLoaded = false
static var myCollectionProgressObservingContext = UUID().uuidString
private let designThumbnailTags: Set<String> = ["Resource1", "Resource2", "Resource3"]
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
loadOnDemandResources()
}
func loadOnDemandResources() {
myCollectionVCResourceRequest = NSBundleResourceRequest(tags: designThumbnailTags)
myCollectionVCResourceRequest.progress.addObserver(self, forKeyPath: "fractionCompleted", options: [.new, .initial], context: &MyCollectionVC.myCollectionProgressObservingContext)
myCollectionVCResourceRequest.beginAccessingResources(completionHandler: { (error) in
print("Complete: \(self.myCollectionVCResourceRequest.progress.fractionCompleted)") // Prints Complete: 0.0
self.myCollectionVCResourceRequest.progress.removeObserver(self, forKeyPath: "fractionCompleted", context: &MyCollectionVC.myCollectionProgressObservingContext)
OperationQueue.main.addOperation({
guard error == nil else { self.handleOnDemandResourceError(error! as NSError); return }
self.myCollectionVCResourceRequestLoaded = true
self.updateViewsForOnDemandResourceAvailability()
self.fetchDesignThumbnailsWithOnDemandResourceTags(self.myCollectionVCResourceRequest) // Correctly creates Core Data Instances
})
})
}
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
if context == &MyCollectionVC.myCollectionProgressObservingContext {
OperationQueue.main.addOperation({
let progressObject = object as! Progress
self.progressView.progress = Float(progressObject.fractionCompleted)
print(Float(progressObject.fractionCompleted)) // Prints 0.0 as a result of including the .initial option
self.progressDetailLabel.text = progressObject.localizedDescription
})
}
else {
super.observeValue(forKeyPath: keyPath, of: object, change: change, context: context)
}
}
}

Resources