※ 今回紹介するやり方は、実現できたことの紹介 なだけなので、その辺りはご了承ください。
やりたいこと(JavaScript, css 禁止)
ピンチインピンチアウトができるのはそのままに
A. ズームしていない状態でダブルタップしてもズームさせない
B. ズームしている状態でダブルタップしてもズームとスクロールさせない
というのがやりたいことです。
A の現象について
WKWebView はダブルタップするとズームする機能があります。
下記の例は初期表示状態(zoomScale: 1.00)の状態で、ダブルタップすると zoomScale が 1.60 になります。
※ 下記の例は背景が黄色で、青い四角い領域(div)があるだけの html を表示したものです
初期表示
ダブルタップ後
B の現象について
また、WKWebView はズームされている状態でダブルタップすると縮小?しつつスクロールします。
下記の例では zoomScale: 1.60 に縮小し(戻り)ました。
※ なぜ 1.60 かはわかりません。たまたま A の現象のズーム率と同じでした。段階があるのかもしれません。
適当にピンチした後
適当にピンチした後の状態でダブルタップした後
A の防ぎ方(比較的簡単)
ダブルタップを検知するための UITapGestureRecognizer を削除する。
// WKWebView の didFinish くらいのタイミングで実行する
// ※ viewDidLoad などのインスタンスが生成された直後くらいだと消せない
self.removeDoubleTapGestureRecognizer(view: webView)
// ビューを渡すと再帰的に UITapGestureRecognizer を削除する
private func removeDoubleTapGestureRecognizer(view: UIView) {
if let gestureRecognizers = view.gestureRecognizers {
for gestureRecognizer in gestureRecognizers {
if let tapGestureRecognizer = gestureRecognizer as? UITapGestureRecognizer {
if tapGestureRecognizer.numberOfTapsRequired == 2 {
view.removeGestureRecognizer(tapGestureRecognizer)
}
}
}
}
for subview in view.subviews {
self.removeDoubleTapGestureRecognizer(view: subview)
}
}
B の防ぎ方1
B の場合も ダブルタップ なので A と同様に【ダブルタップを検知するための UITapGestureRecognizer を削除】すればいいのですが、このダブルタップでズームする UITapGestureRecognizer はピンチインピンチアウトを行うと "復活" します。
なので、ピンチイン/ピンチアウトが終わったときに毎回削除を試みるようにします。
具体的には UIScrollView の delegate である scrollViewDidEndZooming で UITapGestureRecognizer の削除を試みます。
なお、下記のコードを実装した状態で、ピンチインピンチアウトを繰り返していると、何度も【削除しました。】のログが出ます。
何度も削除されるということが復活するということの証拠になります。
// ピンチインピンチアウトで拡大/縮小が完了したときに、ダブルタップ用の UITapGestureRecognizer の削除を試みる
func scrollViewDidEndZooming(_ scrollView: UIScrollView, with view: UIView?, atScale scale: CGFloat) {
self.removeDoubleTapGestureRecognizer(view: self.wkWebView.scrollView)
}
// 削除対象があり、削除された場合は print 文で削除したことをログに出力する
private func removeDoubleTapGestureRecognizer(view: UIView) {
if let gestureRecognizers = view.gestureRecognizers {
for gestureRecognizer in gestureRecognizers {
if let tapGestureRecognizer = gestureRecognizer as? UITapGestureRecognizer {
if tapGestureRecognizer.numberOfTapsRequired == 2 {
view.removeGestureRecognizer(tapGestureRecognizer)
print("【delete】doubleTapGestureRecognizer を削除しました。")
}
}
}
}
for subview in view.subviews {
self.removeDoubleTapGestureRecognizer(view: subview)
}
}
B の防ぎ方2
ピンチイン/ピンチアウトの操作をするたびに UITapGestureRecognizer が生成されるので、これを止める方法はないか調べました。
結論から言うと止めることはできましたが、いわゆる黒魔術なので、防ぎ方1の方がマシだと思います。
UITapGestureRecognizer を生成しているのは、WKWebView の内部実装にある、
WKContentView クラス
の
_createAndConfigureDoubleTapGestureRecognizer
というメソッドなので、このメソッドを何もしないメソッドに書き換えてしまう、というのが防ぎ方2です。
書き換えは、Method Swizzling と呼ばれる手法で行います。
method_exchangeImplementations(method_A, method_B)
この関数を使うと、method_A が method_B に置き換わります。
なので、イメージとしては、
method_exchangeImplementations(_createAndConfigureDoubleTapGestureRecognizer, myDummyFunction)
func myDummyFunction() {
print("何もしないダミーの関数です。")
}
のように、_createAndConfigureDoubleTapGestureRecognizer を何もしない関数に置き換えてしまう、ということをします。
// NSObject の extension の必要はないですが、今回はこういうふうに作りました、という程度です。
extension NSObject {
class func swizzleMethod(originClassName: String, originFunctionName: String, newClassName: String, newFunctionName: String) -> Void {
guard let originClass = objc_getClass(originClassName) as? AnyClass else {
print("objc_getClass(\"\(originClassName)\") could not get class.")
return
}
guard let newClass = objc_getClass(newClassName) as? AnyClass else {
print("objc_getClass(\"\(newClassName)\") could not get class.")
return
}
guard let originMethod = class_getInstanceMethod(originClass, Selector(originFunctionName)) else {
print("could not find origin method. originClassName: \(originClassName) originFunctionName: \(originFunctionName)")
return
}
guard let newMethod = class_getInstanceMethod(newClass, Selector(newFunctionName)) else {
print("could not find new method. newClassName: \(newClassName) newFunctionName: \(newFunctionName)")
return
}
method_exchangeImplementations(originMethod, newMethod)
}
}
// 置き換える予定のメソッド
// 実質何もしないメソッド
@objc func dummy() {
print("dummy")
}
// メソッドの置き換え
NSObject.swizzleMethod(
originClassName: "WKContentView",
originFunctionName: "_createAndConfigureDoubleTapGestureRecognizer",
newClassName: NSStringFromClass(type(of: self)),
newFunctionName: "dummy"
)
この Method Swizzling でメソッドの置換を行った後、ピンチイン/ピンチアウトをしてみてください。
大量の【dummy】というログが出力されます。
なので、UITapGestureRecognizer がなければ作る、ということを何度も試みているということになります。
恐ろしいですね。
【dummy】というログが出ていれば置き換えができているということなので、ズーム中のダブルタップが効かなくなっていると思います。
まとめ
- A の防ぎ方 + B の防ぎ方1
- A の防ぎ方 + B の防ぎ方2
のどちらかの組み合わせで今回のやりたいことは実現できました。
コードの重要なポイントだけ掲載しておきます。
class ViewController: UIViewController {
private var wkWebView: WKWebView!
override func viewDidLoad() {
super.viewDidLoad()
// WKWebView のインスタンス生成や AutoLayout の設定など
self.wkWebView.navigationDelegate = self
self.wkWebView.scrollView.delegate = self
// 【 B の防ぎ方2】ズーム時のダブルタップジェスチャーが作られないようにする
NSObject.swizzleMethod(
originClassName: "WKContentView",
originFunctionName: "_createAndConfigureDoubleTapGestureRecognizer",
newClassName: NSStringFromClass(type(of: self)),
newFunctionName: "dummy"
)
}
@objc func dummy() {
print("dummy")
}
private func removeDoubleTapGestureRecognizer(view: UIView) {
if let gestureRecognizers = view.gestureRecognizers {
for gestureRecognizer in gestureRecognizers {
if let tapGestureRecognizer = gestureRecognizer as? UITapGestureRecognizer {
if tapGestureRecognizer.numberOfTapsRequired == 2 {
view.removeGestureRecognizer(tapGestureRecognizer)
print("【delete】doubleTapGestureRecognizer を削除しました。")
}
}
}
}
for subview in view.subviews {
self.removeDoubleTapGestureRecognizer(view: subview)
}
}
}
extension NSObject {
class func swizzleMethod(originClassName: String, originFunctionName: String, newClassName: String, newFunctionName: String) -> Void {
guard let originClass = objc_getClass(originClassName) as? AnyClass else {
print("objc_getClass(\"\(originClassName)\") could not get class.")
return
}
guard let newClass = objc_getClass(newClassName) as? AnyClass else {
print("objc_getClass(\"\(newClassName)\") could not get class.")
return
}
guard let originMethod = class_getInstanceMethod(originClass, Selector(originFunctionName)) else {
print("could not find origin method. originClassName: \(originClassName) originFunctionName: \(originFunctionName)")
return
}
guard let newMethod = class_getInstanceMethod(newClass, Selector(newFunctionName)) else {
print("could not find new method. newClassName: \(newClassName) newFunctionName: \(newFunctionName)")
return
}
method_exchangeImplementations(originMethod, newMethod)
}
}
extension ViewController: WKNavigationDelegate {
// 【 A の防ぎ方】
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
// zoomScale が 1.0 のときに使われるダブルタップジェスチャーを削除する
self.removeDoubleTapGestureRecognizer(view: webView)
}
}
extension ViewController: UIScrollViewDelegate {
// 【 B の防ぎ方1】
func scrollViewDidEndZooming(_ scrollView: UIScrollView, with view: UIView?, atScale scale: CGFloat) {
// ピンチイン/ピンチアウトが完了したときダブルタップジェスチャーを削除する
self.removeDoubleTapGestureRecognizer(view: self.wkWebView.scrollView)
}
}
今回のケースとしては、
wkWebView.pinch.isEnabled = false // ピンチイン/ピンチアウト禁止
wkWebView.doubleTapAndZoom.isEnabled = false // ダブルタップによるズームの禁止
のようなプロパティがあると嬉しかったですね。
直感的ということを売りにしている?Apple ですが、こういうのこそ直感的じゃないでしょうか?
なんでないんだろう?
(is○○Enabled プロパティがなかったときの絶望感・・・)
また、他にも UITouch でダブルタップを検知してイベントを伝播させるかさせないかをコントロールする、というやり方も試みたのですが、こちらは難しくてうまく実現できませんでした。
UIWindow を使ったやり方、touchesBegan などを使ったやり方をご存知の方、ぜひやり方を教えてください。
参考にしたもの
・ WKWebView のソースコード
https://opensource.apple.com/source/WebKit2/WebKit2-7610.4.3.0.3/UIProcess/API/ios/WKWebViewIOS.mm.auto.html
・Method Swizzling 関連
https://qiita.com/t_osawa_009/items/99ff5540255986a95c33
https://qiita.com/daisuke0131/items/9226a256de868b435050
https://abhimuralidharan.medium.com/method-swizzling-in-ios-swift-1f38edaf984f