How to detect a movie being played in a WKWebView?
Since the solution(s) to this question required a lot of research and different approaches, I'd like to document it here for others to follow my thoughts. If you're just interested in the final solution, look for some fancy headings.
The app I started with, was pretty simple. It's a Single-View Application that imports WebKit
and opens a WKWebView
with some NSURL
:
import UIKit
import WebKit
class ViewController: UIViewController {
var webView: WKWebView!
override func viewDidAppear(animated: Bool) {
webView = WKWebView()
view = webView
let request = NSURLRequest(URL: NSURL(string: "http://tinas-burger.tumblr.com/post/133991473113")!)
webView.loadRequest(request)
}
}
The URL includes a video that is (kind of) protected by JavaScript. I really haven't seen the video yet, it was just the first I discovered. Remember to add
NSAppTransportSecurity
andNSAllowsArbitraryLoads
to yourInfo.plist
or you will see a blank page.
WKNavigationDelegate
The WKNavigationDelegate
won't notify you about a video being played. So setting webView.navigationDelegate = self
and implementing the protocol won't bring you the desired results.
NSNotificationCenter
I assumed that there must be an event like SomeVideoPlayerDidOpen
. Unfortunately there wasn't any, but it might have a SomeViewDidOpen
event, so I started inspecting the view hierarchy:
UIWindow
UIWindow
WKWebView
WKScrollView
...
...
UIWindow
UIWindow
UIView
AVPlayerView
UITransitionView
UIView
UIView
UIView
...
UIView
...
AVTouchIgnoringView
...
As expected there will be an additional UIWindow
added which might have an event and hell yes it does have!
I extended viewDidAppear:
by adding a new observer:
NSNotificationCenter.defaultCenter().addObserver(self, selector: "windowDidBecomeVisible:", name: UIWindowDidBecomeVisibleNotification, object: nil)
And added the corresponding method:
func windowDidBecomeVisible(notification: NSNotification) {
for mainWindow in UIApplication.sharedApplication().windows {
for mainWindowSubview in mainWindow.subviews {
// this will print:
// 1: `WKWebView` + `[WKScrollView]`
// 2: `UIView` + `[]`
print("\(mainWindowSubview) \(mainWindowSubview.subviews)")
}
As expected it returns the view hierarchy as we inspected earlier. But unfortunately it seems like the AVPlayerView
will be created later.
If you trust your application that the only UIWindow
it'll open is the media player, you're finished at this point. But this solution wouldn't let me sleep at night, so let's go deeper...
Injecting An Event
We need to get notified about the AVPlayerView
being added to this nameless UIView
. It seems pretty obvious that AVPlayerView
must be a subclass of UIView
but since it's not officially documented by Apple I checked the iOS Runtime Headers for AVPlayerView
and it definitely is a UIView
.
Now that we know that AVPlayerView
is a subclass of UIView
it will probably added to the nameless UIView
by calling addSubview:
. So we'd have to get notified about a view that was added. Unfortunately UIView
doesn't provide an event for this to be observed. But it does call a method called didAddSubview:
which could be very handy.
So let's check wether a AVPlayerView
will be added somewhere in our application and send a notification:
let originalDidAddSubviewMethod = class_getInstanceMethod(UIView.self, "didAddSubview:")
let originalDidAddSubviewImplementation = method_getImplementation(originalDidAddSubviewMethod)
typealias DidAddSubviewCFunction = @convention(c) (AnyObject, Selector, UIView) -> Void
let castedOriginalDidAddSubviewImplementation = unsafeBitCast(originalDidAddSubviewImplementation, DidAddSubviewCFunction.self)
let newDidAddSubviewImplementationBlock: @convention(block) (AnyObject!, UIView) -> Void = { (view: AnyObject!, subview: UIView) -> Void in
castedOriginalDidAddSubviewImplementation(view, "didAddsubview:", subview)
if object_getClass(view).description() == "AVPlayerView" {
NSNotificationCenter.defaultCenter().postNotificationName("PlayerWillOpen", object: nil)
}
}
let newDidAddSubviewImplementation = imp_implementationWithBlock(unsafeBitCast(newDidAddSubviewImplementationBlock, AnyObject.self))
method_setImplementation(originalDidAddSubviewMethod, newDidAddSubviewImplementation)
Now we can observe the notification and receive the corresponding event:
NSNotificationCenter.defaultCenter().addObserver(self, selector: "playerWillOpen:", name: "PlayerWillOpen", object: nil)
func playerWillOpen(notification: NSNotification) {
print("A Player will be opened now")
}
Better notification injection
Since the AVPlayerView
won't get removed but only deallocated we'll have to rewrite our code a little bit and inject some notifications to the AVPlayerViewController
. That way we'll have as many notifications as we want, e.g.: PlayerWillAppear
and PlayerWillDisappear
:
let originalViewWillAppearMethod = class_getInstanceMethod(UIViewController.self, "viewWillAppear:")
let originalViewWillAppearImplementation = method_getImplementation(originalViewWillAppearMethod)
typealias ViewWillAppearCFunction = @convention(c) (UIViewController, Selector, Bool) -> Void
let castedOriginalViewWillAppearImplementation = unsafeBitCast(originalViewWillAppearImplementation, ViewWillAppearCFunction.self)
let newViewWillAppearImplementationBlock: @convention(block) (UIViewController!, Bool) -> Void = { (viewController: UIViewController!, animated: Bool) -> Void in
castedOriginalViewWillAppearImplementation(viewController, "viewWillAppear:", animated)
if viewController is AVPlayerViewController {
NSNotificationCenter.defaultCenter().postNotificationName("PlayerWillAppear", object: nil)
}
}
let newViewWillAppearImplementation = imp_implementationWithBlock(unsafeBitCast(newViewWillAppearImplementationBlock, AnyObject.self))
method_setImplementation(originalViewWillAppearMethod, newViewWillAppearImplementation)
let originalViewWillDisappearMethod = class_getInstanceMethod(UIViewController.self, "viewWillDisappear:")
let originalViewWillDisappearImplementation = method_getImplementation(originalViewWillDisappearMethod)
typealias ViewWillDisappearCFunction = @convention(c) (UIViewController, Selector, Bool) -> Void
let castedOriginalViewWillDisappearImplementation = unsafeBitCast(originalViewWillDisappearImplementation, ViewWillDisappearCFunction.self)
let newViewWillDisappearImplementationBlock: @convention(block) (UIViewController!, Bool) -> Void = { (viewController: UIViewController!, animated: Bool) -> Void in
castedOriginalViewWillDisappearImplementation(viewController, "viewWillDisappear:", animated)
if viewController is AVPlayerViewController {
NSNotificationCenter.defaultCenter().postNotificationName("PlayerWillDisappear", object: nil)
}
}
let newViewWillDisappearImplementation = imp_implementationWithBlock(unsafeBitCast(newViewWillDisappearImplementationBlock, AnyObject.self))
method_setImplementation(originalViewWillDisappearMethod, newViewWillDisappearImplementation)
Now we can observe these two notifications and are good to go:
NSNotificationCenter.defaultCenter().addObserver(self, selector: "playerWillAppear:", name: "PlayerWillAppear", object: nil)
NSNotificationCenter.defaultCenter().addObserver(self, selector: "playerWillDisappear:", name: "PlayerWillDisappear", object: nil)
func playerWillAppear(notification: NSNotification) {
print("A Player will be opened now")
}
func playerWillDisappear(notification: NSNotification) {
print("A Player will be closed now")
}
URL of the video
I spent a couple of hours digging some iOS Runtime Headers to guess where I could find the URL pointing to the video, but I couldn't manage to find it. When I was digging into some source files of WebKit itself, I had to give up and accept that there's no easy way to do it, although I believe it's somewhere hidden and can be reached, but most likely only with a lot of effort.
Detect if page is loaded inside WKWebView in JavaScript
You can check for the existence of window.webkit.messageHandlers
which WKWebKit uses to receive messages from JavaScript. If it exists, you're inside a WKWebView
.
That combined with a simple user agent check should do the trick:
var iOS = (navigator.userAgent.match(/(iPad|iPhone|iPod)/g) ? true : false);
var isWKWebView = false;
if (window.webkit && window.webkit.messageHandlers) {
isWKWebView = true;
}
WKWebView Media Player fullscreen detection
This workaround seems to work on iOS8 & iPhone 6
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
...
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(windowBecameHidden:) name:UIWindowDidBecomeHiddenNotification object:nil];
return TRUE;
}
- (void)windowBecameHidden:(NSNotification *)notification {
UIWindow *window = notification.object;
if (window != self.window) { // Not my own window: assuming the video window was hidden, maybe add some more checks here.
// Add code here
}
}
Hiding view when WKWebView has finished loading swiftUI
Your View
won't know to reload unless it is triggered with something like a @State
property or an ObservableObject
. You haven't really provided enough code from your SwiftUiWebView
to show everything, but here's the gist of what needs to happen:
struct ContentView: View {
@State var webViewFinishedLoading = false
var body: some View {
SwiftUiWebView(url: URL(string: "myUrl"), finishedLoading: $webViewFinishedLoading)
ZStack {
if (!webViewFinishedLoading) {
....
}
}
}
}
struct SwiftUiWebView : UIViewRepresentable {
var url: URL
var finishedLoading: Binding<Bool>
//...
}
Then, you will need to pass that finishedLoading
Binding to your web view delegate and set its .wrappedValue
to true
when you're done loading:
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
if (webView.isLoading) {
return
}
print("Done Loading")
finishedLoading.wrappedValue = true
}
Because it's tied to a @State
variable, your view will know to refresh.
Related Topics
How to Add a Watermark to an Image Using This Code
Swift 4 - Notification Center Addobserver Issue
Wait Until an Asynchronous API Call Is Completed - Swift/Ios
How to Properly Implement the Equatable Protocol in a Class Hierarchy
Macos/Swift Capture Audio with Avcapturesession
Deploy App with Pre-Populated Core Data
Is String Type a Class or a Struct? or Something Else
Generate an Rsa Public/Private Key Pair
Nsstatusitem in Nsstatusbar, Action Selector Method Not Responding
Render a 3D Model (Hair) with Semi-Transparent Texture in Scenekit
Watchos3 Complication That Launches App
Swift Error: Missing Argument Label 'Name:' in Call
Storyboard Tableview with Segues to Multiple Views
Target Parameter in Dispatchqueue
Hstack with Sf Symbols Image Not Aligned Centered
Xcode Incorrectly Reporting Swift Access Race Condition