How to capture taps ignored by WKWebView
Turns out the solution to this is to turn the problem inside out. I put a transparent UIView
over the WKWebView
and attached several UIGestureRecognizer
s to my transparent view. I use JavaScript calls to react to pan gestures. When a tap is recognized on my transparent view, I query the DOM to see if it is a link and if not, I can do whatever I want with it. Another benefit is that I get to handle long press, which is something I wanted to do instead of letting WKWebView do its baked-in, unchangeable behavior. This works really well.
How to add gesture to WKWebView
try this
class SwipeObserveExperimentController: UIViewController,UIGestureRecognizerDelegate
let tapGesture = UITapGestureRecognizer(target: self, action: #selector(viewTap) )
tapGesture.delegate = self
webView.addGestureRecognizer(tapGesture)
and call the following method
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
return true
}
and call the function as
func viewTap() {
print("View Tap")
}
How to disable popup when user taps phone number in WKWebView on iPad?
I could imagine several approaches, though I don't have the opportunity to try them right now.
WKWebViewConfiguration
Set properties like
dataDetectorTypes
to.none
, although that should be the default, anyway.Use
setURLSchemeHandler(_:forURLScheme:)
to ignore thetel://
scheme
HTML/JS
- Remove the offending link tags before displaying the HTML through scraping
- Apply some Javascript that catches the event and use
preventDefault()
or something.
Detecting taps inside a UIWebView
Most of the approaches deal with a complicated pair of UIView
and UIWebView
subclasses and overrode -touchesBegan:withEvent:
etc. methods.
This JavaScript-based approach intercepts touches on the web DOM itself, and it seems like a clever way to sidestep the more complex process. I haven't tried it myself, but I'm curious to know the results, if you give it a shot.
Disable magnification gesture in WKWebView
You can prevent your users from zooming by setting the delegate of your WKWebKit's UIScrollView and implementing viewForZooming(in:)
as in the following:
class MyClass {
let webView = WKWebView()
init() {
super.init()
webView.scrollView.delegate = self
}
deinit() {
// Without this, it'll crash when your MyClass instance is deinit'd
webView.scrollView.delegate = nil
}
}
extension MyClass: UIScrollViewDelegate {
func viewForZooming(in scrollView: UIScrollView) -> UIView? {
return nil
}
}
How to force WKWebView to ignore hardware silent switch on iOS?
Update: Added new hack working also for iOS 14(and 15)! (reflected in code, see bottom for extra details).
Since I have a solution to this nontrivial problem, I'd like to share it:
override func viewDidLoad() {
super.viewDidLoad()
NotificationCenter.default.addObserver(self, selector: #selector(didBecomeActive),
name: NSNotification.Name.UIApplicationDidBecomeActive, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(willResignActive),
name: NSNotification.Name.UIApplicationWillResignActive, object: nil)
let configuration = WKWebViewConfiguration()
configuration.allowsInlineMediaPlayback = true
configuration.mediaTypesRequiringUserActionForPlayback = []
wkWebView = WKWebView(frame: .zero, configuration: configuration)
}
@objc func willResignActive() {
disableIgnoreSilentSwitch(wkWebView)
}
@objc func didBecomeActive() {
//Always creates new js Audio object to ensure the audio session behaves correctly
forceIgnoreSilentHardwareSwitch(wkWebView, initialSetup: false)
}
And most importantly in WKNavigationDelegate
:
private func disableIgnoreSilentSwitch(_ webView: WKWebView) {
//Nullifying the js Audio object src is critical to restore the audio sound session to consistent state for app background/foreground cycle
let jsInject = "document.getElementById('wkwebviewAudio').muted=true;"
webView.evaluateJavaScript(jsInject, completionHandler: nil)
}
private func forceIgnoreSilentHardwareSwitch(_ webView: WKWebView, initialSetup: Bool) {
//after some trial and error this seems to be minimal silence sound that still plays
let silenceMono56kbps100msBase64Mp3 = "data:audio/mp3;base64,//tAxAAAAAAAAAAAAAAAAAAAAAAASW5mbwAAAA8AAAAFAAAESAAzMzMzMzMzMzMzMzMzMzMzMzMzZmZmZmZmZmZmZmZmZmZmZmZmZmaZmZmZmZmZmZmZmZmZmZmZmZmZmczMzMzMzMzMzMzMzMzMzMzMzMzM//////////////////////////8AAAA5TEFNRTMuMTAwAZYAAAAAAAAAABQ4JAMGQgAAOAAABEhNIZS0AAAAAAD/+0DEAAPH3Yz0AAR8CPqyIEABp6AxjG/4x/XiInE4lfQDFwIIRE+uBgZoW4RL0OLMDFn6E5v+/u5ehf76bu7/6bu5+gAiIQGAABQIUJ0QolFghEn/9PhZQpcUTpXMjo0OGzRCZXyKxoIQzB2KhCtGobpT9TRVj/3Pmfp+f8X7Pu1B04sTnc3s0XhOlXoGVCMNo9X//9/r6a10TZEY5DsxqvO7mO5qFvpFCmKIjhpSItGsUYcRO//7QsQRgEiljQIAgLFJAbIhNBCa+JmorCbOi5q9nVd2dKnusTMQg4MFUlD6DQ4OFijwGAijRMfLbHG4nLVTjydyPlJTj8pfPflf9/5GD950A5e+jsrmNZSjSirjs1R7hnkia8vr//l/7Nb+crvr9Ok5ZJOylUKRxf/P9Zn0j2P4pJYXyKkeuy5wUYtdmOu6uobEtFqhIJViLEKIjGxchGev/L3Y0O3bwrIOszTBAZ7Ih28EUaSOZf/7QsQfg8fpjQIADN0JHbGgQBAZ8T//y//t/7d/2+f5m7MdCeo/9tdkMtGLbt1tqnabRroO1Qfvh20yEbei8nfDXP7btW7f9/uO9tbe5IvHQbLlxpf3DkAk0ojYcv///5/u3/7PTfGjPEPUvt5D6f+/3Lea4lz4tc4TnM/mFPrmalWbboeNiNyeyr+vufttZuvrVrt/WYv3T74JFo8qEDiJqJrmDTs///v99xDku2xG02jjunrICP/7QsQtA8kpkQAAgNMA/7FgQAGnobgfghgqA+uXwWQ3XFmGimSbe2X3ksY//KzK1a2k6cnNWOPJnPWUsYbKqkh8RJzrVf///P///////4vyhLKHLrCb5nIrYIUss4cthigL1lQ1wwNAc6C1pf1TIKRSkt+a//z+yLVcwlXKSqeSuCVQFLng2h4AFAFgTkH+Z/8jTX/zr//zsJV/5f//5UX/0ZNCNCCaf5lTCTRkaEdhNP//n/KUjf/7QsQ5AEhdiwAAjN7I6jGddBCO+WGTQ1mXrYatSAgaykxBTUUzLjEwMKqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqg=="
//Plays 100ms silence once the web page has loaded through HTML5 Audio element (through Javascript)
//which as a side effect will switch WKWebView AudioSession to AVAudioSessionCategoryPlayback
var jsInject: String
if initialSetup {
jsInject =
"var s=new Audio('\(silenceMono56kbps100msBase64Mp3)');" +
"s.id='wkwebviewAudio';" +
"s.play();" +
"s.loop=true;" +
"document.body.appendChild(s);"
} else {
//Restore sound hack
jsInject = "document.getElementById('wkwebviewAudio').muted=false;"
}
webView.evaluateJavaScript(jsInject, completionHandler: nil)
}
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
//As a result the WKWebView ignores the silent switch
forceIgnoreSilentHardwareSwitch(webView, initialSetup: true)
}
Interestingly a related Safari problem is mentioned here: IOS WebAudio only works on headphones where @Spencer Evans workaround looks very similar to mine.
However when I tried to apply his shorter base64 silence sound it didn't work for WKWebView
, so I'm providing my own minimal silence sound tested on iOS12.
Why it works?
Playing an <audio>
or <video>
element (which in the workaround happens to be non audible silence) changes WKWebView
audio session category from AVAudioSessionCategoryAmbient
to AVAudioSessionCategoryPlayback
. This will be valid until next load request resets it.
It's all great till the app is backgrounded. But upon subsequent foregrounding things will break in 2 possible ways:
- user needs to tap for the sounds to reappear
- rarely no user input will help and the WKWebView lands in semi frozen state
To counter that^ the hack is reverted with disableIgnoreSilentSwitch(wkWebView)
and later reenabled with forceIgnoreSilentHardwareSwitch(wkWebView, initialSetup: false)
Since WKWebView
core runs in an external process it cannot be accessed the way UIWebView
shared (with our app) AVAudioSession
can be.
Verified for:
iOS 11.4
iOS 12.4.1
iOS 13.3
iOS 14.1
iOS 14.5.1
iOS 14.8
iOS 15.0
iOS 14 update
Situation got pretty bad in iOS 14 where obsolete audio tag .src=null
trick stopped working. Technically .src=null
does work for a very short window of time (one can revert the hack using .src
during initial setup). However once the silence loop is playing it becomes useless.
The new trick relies on .mute
which miraculously works across all iOS versions including iOS14 (but only when accessing documentById directly not a var). No mediacenter when locking the screen neither. It took a lot of research, but we got it.
Related Topics
How to Get Current Location Using Cllocationmanager in iOS
Phimageresultisdegradedkey/Phimagefileurlkey Is Not Found
Best Method to Store Data for an iOS App
iOS Web App: Showing Content Only If the Application Is Standalone
How to Encrypt and Decrypt a String(Plain Text) with Rsa Public Key in iOS, Swift
How Is Filters Uiscrollview/Uicollectionview in Apple's Photos App Implemented That It Opens So Fast
Taking Screenshots in the Background (Ios) - Improving Performance
Grouping Coredata by Date() in Swiftui List as Sections
Physicsbody Doesn't Adhere to Node's Anchor Point
Declaring Global Variables in Swift
Add a Navigation Bar to a View Without a Navigation Controller
App Crashes in Background While Changing Permission - Swift
How to Code the Launchscreen Programmatically
How to Unit Test an App Extension on Xcode 6