web-dev-qa-db-ja.com

WKWebViewのコンテンツサイズを決定する方法は?

IOS 8以降で実行しているときに、UIWebViewの動的に割り当てられたインスタンスをWKWebViewインスタンスに置き換える実験を行っていますが、WKWebViewのコンテンツサイズを特定する方法が見つかりません。

私のWebビューは、より大きなUIScrollViewコンテナー内に埋め込まれているため、Webビューの理想的なサイズを決定する必要があります。これにより、Webビュー内でスクロールすることなく、すべてのHTMLコンテンツを表示するようにフレームを変更できます。また、スクロールビューコンテナーの正しい高さを設定できます(scrollview.contentSizeを設定することにより)。

私はsizeToFitとsizeThatFitsを試してみましたが成功しませんでした。 WKWebViewインスタンスを作成し、それをコンテナのスクロールビューに追加するコードを次に示します。

// self.view is a UIScrollView sized to something like 320.0 x 400.0.
CGRect wvFrame = CGRectMake(0, 0, self.view.frame.size.width, 100.0);
self.mWebView = [[[WKWebView alloc] initWithFrame:wvFrame] autorelease];
self.mWebView.navigationDelegate = self;
self.mWebView.scrollView.bounces = NO;
self.mWebView.scrollView.scrollEnabled = NO;

NSString *s = ... // Load s from a Core Data field.
[self.mWebView loadHTMLString:s baseURL:nil];

[self.view addSubview:self.mWebView];

以下は、実験的なdidFinishNavigationメソッドです。

- (void)webView:(WKWebView *)aWebView
                             didFinishNavigation:(WKNavigation *)aNavigation
{
    CGRect wvFrame = aWebView.frame;
    NSLog(@"original wvFrame: %@\n", NSStringFromCGRect(wvFrame));
    [aWebView sizeToFit];
    NSLog(@"wvFrame after sizeToFit: %@\n", NSStringFromCGRect(wvFrame));
    wvFrame.size.height = 1.0;
    aWebView.frame = wvFrame;
    CGSize sz = [aWebView sizeThatFits:CGSizeZero];
    NSLog(@"sizeThatFits A: %@\n", NSStringFromCGSize(sz));
    sz = CGSizeMake(wvFrame.size.width, 0.0);
    sz = [aWebView sizeThatFits:sz];
    NSLog(@"sizeThatFits B: %@\n", NSStringFromCGSize(sz));
}

そして、生成される出力は次のとおりです。

2014-12-16 17:29:38.055 App[...] original wvFrame: {{0, 0}, {320, 100}}
2014-12-16 17:29:38.055 App[...] wvFrame after sizeToFit: {{0, 0}, {320, 100}}
2014-12-16 17:29:38.056 App[...] wvFrame after sizeThatFits A: {320, 1}
2014-12-16 17:29:38.056 App[...] wvFrame after sizeThatFits B: {320, 1}

SizeToFit呼び出しは効果がなく、sizeThatFitsは常に高さ1を返します。

45
Mark Smith

私はこのテーマに関するすべての答えを読んだと思いますが、私が持っていたのは解決策の一部でした。 @davewで説明されているように、KVOメソッドの実装を試みるのにほとんどの時間を費やしましたが、ほとんどの場合、WKWebViewコンテナーのコンテンツの下に空白が残っていました。また、@ David Beckの提案を実装し、コンテナーの高さを0にして、コンテナーの高さがコンテンツの高さよりも大きい場合に問題が発生する可能性を回避しました。それにもかかわらず、私はその時々の空きスペースがありました。そのため、私にとって「contentSize」オブザーバーには多くの欠陥がありました。私はウェブ技術の経験があまりないので、このソリューションの問題点に答えることはできませんが、コンソールで高さを印刷するだけで何もしない場合(たとえば、制約のサイズを変更する)、ある番号(例:5000)にジャンプしてから、その最高の番号(例:2500-正しい番号)の前の番号にジャンプします。 「contentSize」から取得する高さに高さの制約を設定すると、取得した最大数に設定され、正しいサイズにサイズ変更されません-これも@David Beckコメントで言及されています。

多くの実験の後、私は自分に合った解決策を見つけることができました。

func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
    self.webView.evaluateJavaScript("document.readyState", completionHandler: { (complete, error) in
        if complete != nil {
            self.webView.evaluateJavaScript("document.body.scrollHeight", completionHandler: { (height, error) in
                self.containerHeight.constant = height as! CGFloat
            })
        }

        })
}

もちろん、containerHeight制約に従ってscrollViewのサイズが変更されるように、制約を正しく設定することが重要です。

結局のところ、didFinishナビゲーションメソッドは必要なときに呼び出されることはありませんが、document.readyStateステップ、次のステップ(document.body.offsetHeight)適切なタイミングで呼び出され、身長に対して適切な数値を返します。

66
IvanMih

Key-Value Observing(KVO)を使用できます...

ViewControllerで:

- (void)viewDidLoad {
    ...
    [self.webView.scrollView addObserver:self forKeyPath:@"contentSize" options:NSKeyValueObservingOptionNew context:nil];
}


- (void)dealloc
{
    [self.webView.scrollView removeObserver:self forKeyPath:@"contentSize" context:nil];
}


- (void)observeValueForKeyPath:(NSString *)keyPath
                      ofObject:(id)object
                        change:(NSDictionary *)change
                       context:(void *)context
{
    if (object == self.webView.scrollView && [keyPath isEqual:@"contentSize"]) {
        // we are here because the contentSize of the WebView's scrollview changed.

        UIScrollView *scrollView = self.webView.scrollView;
        NSLog(@"New contentSize: %f x %f", scrollView.contentSize.width, scrollView.contentSize.height);
    }
}

これにより、JavaScriptの使用が節約され、すべての変更がループされます。

21
davew

最近、この問題に自分で対処しなければなりませんでした。結局、私は Chris McClenaghanによって提案された解決策 の修正を使用していました。

実際、彼の元のソリューションは非常に優れており、ほとんどの単純なケースで機能します。ただし、テキストのあるページでしか機能しませんでした。おそらく、高さが静的な画像を含むページでも機能します。ただし、max-heightおよびmax-width属性でサイズが定義されている画像がある場合は、確実に機能しません。

これは、これらの要素のサイズを変更できるためですafterページがロードされます。したがって、実際には、onLoadで返される高さは常に正しいです。しかし、それはその特定のインスタンスに対してのみ正しいでしょう。回避策は、bodyの高さの変化を監視し、それに対応することです。

document.bodyのサイズ変更を監視する

var shouldListenToResizeNotification = false
lazy var webView:WKWebView = {
    //Javascript string
    let source = "window.onload=function () {window.webkit.messageHandlers.sizeNotification.postMessage({justLoaded:true,height: document.body.scrollHeight});};"
    let source2 = "document.body.addEventListener( 'resize', incrementCounter); function incrementCounter() {window.webkit.messageHandlers.sizeNotification.postMessage({height: document.body.scrollHeight});};"

    //UserScript object
    let script = WKUserScript(source: source, injectionTime: .atDocumentEnd, forMainFrameOnly: true)

    let script2 = WKUserScript(source: source2, injectionTime: .atDocumentEnd, forMainFrameOnly: true)

    //Content Controller object
    let controller = WKUserContentController()

    //Add script to controller
    controller.addUserScript(script)
    controller.addUserScript(script2)

    //Add message handler reference
    controller.add(self, name: "sizeNotification")

    //Create configuration
    let configuration = WKWebViewConfiguration()
    configuration.userContentController = controller

    return WKWebView(frame: CGRect.zero, configuration: configuration)
}()

func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
    guard let responseDict = message.body as? [String:Any],
    let height = responseDict["height"] as? Float else {return}
    if self.webViewHeightConstraint.constant != CGFloat(height) {
        if let _ = responseDict["justLoaded"] {
            print("just loaded")
            shouldListenToResizeNotification = true
            self.webViewHeightConstraint.constant = CGFloat(height)
        }
        else if shouldListenToResizeNotification {
            print("height is \(height)")
            self.webViewHeightConstraint.constant = CGFloat(height)
        }

    }
}

この解決策は、私が思いつく限りでは最もエレガントです。ただし、注意すべき点が2つあります。

まず、URLを読み込む前に、shouldListenToResizeNotificationfalseに設定する必要があります。この追加のロジックは、ロードされたURLが急速に変化する可能性がある場合に必要です。これが発生すると、何らかの理由で古いコンテンツからの通知が新しいコンテンツからの通知と重複する可能性があります。このような動作を防ぐために、この変数を作成しました。新しいコンテンツのロードを開始すると、古いコンテンツからの通知は処理されなくなり、新しいコンテンツがロードされた後にのみサイズ変更通知の処理が再開されます。

ただし、最も重要なことは、これについて注意する必要があることです。

このソリューションを採用する場合、WKWebViewのサイズを通知によって報告されたサイズ以外に変更すると、通知が再度トリガーされることを考慮する必要があります。

無限ループに入るのは簡単なので、これに注意してください。たとえば、高さを報告された高さ+追加のパディングに等しくすることで通知を処理することにした場合:

func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
        guard let responseDict = message.body as? [String:Float],
        let height = responseDict["height"] else {return}
        self.webViewHeightConstraint.constant = CGFloat(height+8)
    }

ご覧のように、報告された高さに8を追加しているため、これが完了するとbodyのサイズが変更され、通知が再度投稿されます。

そのような状況に注意してください、そうでなければあなたは大丈夫です。

そして、この解決策で問題を発見した場合は私に知らせてください-私は自分でそれを頼りにしているので、私が見つけていないいくつかの障害があるかどうかを知ることが最善です!

11

以下を試してください。 WKWebViewインスタンスをインスタンス化する場合は常に、次のようなものを追加します。

    //Javascript string
    NSString * source = @"window.webkit.messageHandlers.sizeNotification.postMessage({width: document.width, height: document.height});";

    //UserScript object
    WKUserScript * script = [[WKUserScript alloc] initWithSource:source injectionTime:WKUserScriptInjectionTimeAtDocumentEnd forMainFrameOnly:YES];

    //Content Controller object
    WKUserContentController * controller = [[WKUserContentController alloc] init];

    //Add script to controller
    [controller addUserScript:script];

    //Add message handler reference
    [controller addScriptMessageHandler:self name:@"sizeNotification"];

    //Create configuration
    WKWebViewConfiguration * configuration = [[WKWebViewConfiguration alloc] init];

    //Add controller to configuration
    configuration.userContentController = controller;

    //Use whatever you require for WKWebView frame
    CGRect frame = CGRectMake(...?);

    //Create your WKWebView instance with the configuration
    WKWebView * webView = [[WKWebView alloc] initWithFrame:frame configuration:configuration];

    //Assign delegate if necessary
    webView.navigationDelegate = self;

    //Load html
    [webView loadHTMLString:@"some html ..." baseURL:[[NSBundle mainBundle] bundleURL]];

次に、クラスがWKScriptMessageHandlerプロトコルに従ってメッセージを処理する次のようなメソッドを追加します。

- (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message {
    CGRect frame = message.webView.frame;
    frame.size.height = [[message.body valueForKey:@"height"] floatValue];
    message.webView.frame = frame;}

これは私のために動作します。

文書にテキスト以上のものがある場合、すべてが確実に読み込まれるように、次のようにjavascriptをラップする必要があります。

@"window.onload=function () { window.webkit.messageHandlers.sizeNotification.postMessage({width: document.width, height: document.height});};"

注:このソリューションは、ドキュメントの継続的な更新には対応していません。

5

WebViewの読み込みが完了するまで待つ必要があります。ここに私が使用した実例があります

WKWebViewコンテンツがロードされた関数は呼び出されません

次に、webviewの読み込みが完了したら、必要な高さを決定できます

func webView(webView: WKWebView!, didFinishNavigation navigation: WKNavigation!) {

   println(webView.scrollView.contentSize.height)

}
4
Brian Wells

私のために働く

extension TransactionDetailViewController: WKNavigationDelegate {
    func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
        DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
            self.webviewHeightConstraint.constant = webView.scrollView.contentSize.height
        }
    }
}
3
luhuiya

EvaluateJavaScriptによってWKWebViewのコンテンツの高さを取得することもできます。

- (void)webView:(WKWebView *)webView didFinishNavigation:(WKNavigation *)navigation {
    [webView evaluateJavaScript:@"Math.max(document.body.scrollHeight, document.body.offsetHeight, document.documentElement.clientHeight, document.documentElement.scrollHeight, document.documentElement.offsetHeight)"
              completionHandler:^(id _Nullable result, NSError * _Nullable error) {
                  if (!error) {
                      CGFloat height = [result floatValue];
                      // do with the height

                  }
              }];
}
2
Ravi Kumar

@Andriyの回答と この回答 を使用して、WKWebViewでcontentSizeのget heightを設定し、その高さを変更することができました。

いっぱいですSwift 4 code:

    var neededConstraints: [NSLayoutConstraint] = []

    @IBOutlet weak var webViewContainer: UIView!
    @IBOutlet weak var webViewHeight: NSLayoutConstraint! {
        didSet {
            if oldValue != nil, oldValue.constant != webViewHeight.constant {
                view.layoutIfNeeded()
            }
        }
    }


   lazy var webView: WKWebView = {
        var source = """
var observeDOM = (function(){
    var MutationObserver = window.MutationObserver || window.WebKitMutationObserver,
        eventListenerSupported = window.addEventListener;

    return function(obj, callback){
        if( MutationObserver ){
            // define a new observer
            var obs = new MutationObserver(function(mutations, observer){
                if( mutations[0].addedNodes.length || mutations[0].removedNodes.length )
                    callback();
            });
            // have the observer observe foo for changes in children
            obs.observe( obj, { childList:true, subtree:true });
        }
        else if( eventListenerSupported ){
            obj.addEventListener('DOMNodeInserted', callback, false);
            obj.addEventListener('DOMNodeRemoved', callback, false);
        }
    };
})();

// Observe a specific DOM element:
observeDOM( document.body ,function(){
    window.webkit.messageHandlers.sizeNotification.postMessage({'scrollHeight': document.body.scrollHeight,'offsetHeight':document.body.offsetHeight,'clientHeight':document.body.clientHeight});
});

"""

        let script = WKUserScript(source: source, injectionTime: .atDocumentEnd, forMainFrameOnly: true)
        let controller = WKUserContentController()
        controller.addUserScript(script)
        controller.add(self, name: "sizeNotification")
        let configuration = WKWebViewConfiguration()
        configuration.userContentController = controller
        let this = WKWebView(frame: .zero, configuration: configuration)
        webViewContainer.addSubview(this)
        this.translatesAutoresizingMaskIntoConstraints = false
        this.scrollView.isScrollEnabled = false
        // constraint for webview when added to it's superview
        neededConstraints += NSLayoutConstraint.constraints(withVisualFormat: "H:|[web]|",
                                                            options: [],
                                                            metrics: nil,
                                                            views: ["web": this])
        neededConstraints += NSLayoutConstraint.constraints(withVisualFormat: "V:|[web]|",
                                                            options: [],
                                                            metrics: nil,
                                                            views: ["web": this])
        return this
    }()


    override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)
        _  = webView // to create constraints needed for webView
        NSLayoutConstraint.activate(neededConstraints)
        let url = URL(string: "https://www.awwwards.com/")!
        let request = URLRequest(url: url)
        webView.load(request)
    }

    func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
        if let body = message.body as? Dictionary<String, CGFloat>,
            let scrollHeight = body["scrollHeight"],
            let offsetHeight = body["offsetHeight"],
            let clientHeight = body["clientHeight"] {
            webViewHeight.constant = scrollHeight
            print(scrollHeight, offsetHeight, clientHeight)
        }
    }
2
Mohammadalijf

ほとんどの答えは「document.body.offsetHeight」を使用しています。

これにより、ボディの最後のオブジェクトが非表示になります。

WKWebview "contentSize"の変更をリッスンしているKVOオブザーバーを使用してこの問題を克服し、次のコードを実行しました。

self.webView.evaluateJavaScript(
    "(function() {var i = 1, result = 0; while(true){result = 
    document.body.children[document.body.children.length - i].offsetTop + 
    document.body.children[document.body.children.length - i].offsetHeight;
    if (result > 0) return result; i++}})()",
    completionHandler: { (height, error) in
        let height = height as! CGFloat
        self.webViewHeightConstraint.constant = height
    }
)

それは可能な限り美しいコードではありませんが、私にとってはうまくいきました。

2
Stratubas

これは、@ IvanMihの答えを少し編集したものです。あなたのWKWebviewの終わりに大きな空白が発生している人にとって、この解決策は私にとってうまくいきました:

func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
  webView.evaluateJavaScript("document.readyState", completionHandler: { (complete, error) in

    if complete != nil {
      let height = webView.scrollView.contentSize
      print("height of webView is: \(height)")
    }
  })
}

したがって、基本的にscrollHeightに基づいて高さを計算する代わりに、webView.scrollView.contentSizeを使用して高さを計算します。これが壊れるシナリオは確かにありますが、静的コンテンツや、ユーザーがスクロールしなくてもすべてのコンテンツを表示する場合には、かなりうまくいくと思います。

1
Ronak Vora

スクロールビューKVOを試し、clientHeightoffsetHeightなどを使用してドキュメントのJavaScriptを評価しようとしました。

最終的に私のために働いたのは:document.body.scrollHeight。または、最上位要素のscrollHeightを使用します。コンテナdiv

KVOを使用してloading WKWebviewプロパティの変更を聞きます:

[webview addObserver: self forKeyPath: NSStringFromSelector(@selector(loading)) options: NSKeyValueObservingOptionNew context: nil];

その後:

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
    if(object == self.webview && [keyPath isEqualToString: NSStringFromSelector(@selector(loading))]) {
        NSNumber *newValue = change[NSKeyValueChangeNewKey];
        if(![newValue boolValue]) {
            [self updateWebviewFrame];
        }
    }
}

updateWebviewFrame実装:

[self.webview evaluateJavaScript: @"document.body.scrollHeight" completionHandler: ^(id response, NSError *error) {
     CGRect frame = self.webview.frame;
     frame.size.height = [response floatValue];
     self.webview.frame = frame;
}];
1
natanavra

UITableViewCellでJavascriptバージョンを試しましたが、完全に機能します。ただし、それをscrollViewに配置する場合。理由はわかりませんが、高さを高くすることはできますが、短くすることはできません。しかし、私はここでUIWebViewソリューションを見つけました。 https://stackoverflow.com/a/48887971/5514452

WKWebViewでも機能します。問題は、WebViewが再レイアウトを必要とするからだと思いますが、どういうわけか縮小せず、拡大しかできません。高さをリセットする必要があり、それは間違いなくサイズ変更されます。

編集:制約を設定した後にフレームの高さをリセットしました。フレームの高さを0に設定するために動作しない場合があるためです。

func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
    self.webView.frame.size.height = 0
    self.webView.evaluateJavaScript("document.readyState", completionHandler: { (complete, error) in
        if complete != nil {
            self.webView.evaluateJavaScript("document.body.scrollHeight", completionHandler: { (height, error) in
                let webViewHeight = height as! CGFloat
                self.webViewHeightConstraint.constant = webViewHeight
                self.webView.frame.size.height = webViewHeight
            })
        }
    })
}
0
Jeff Zhang

また、さまざまな方法を実装しようとし、最終的に解決策に到達しました。その結果、コンテンツサイズに合わせてintrinsicContentSizeを適応させる自己サイズのWKWebViewを作成しました。そのため、自動レイアウトで使用できます。例として、iOSアプリで数式を表示するのに役立つビューを作成しました: https://github.com/Mazorati/SVLatexView

0
mazy