Как определить размер содержимого WKWebView?

Я экспериментирую с заменой динамически распределенного экземпляра UIWebView экземпляром WKWebView при работе под iOS 8 и новее, и я не могу найти способ определить размер содержимого WKWebView.

Мой веб-просмотр встроен в большой контейнер UIScrollView, поэтому мне нужно определить идеальный размер для веб-представления. Это позволит мне изменить свой фрейм, чтобы показать весь его контент HTML, без необходимости прокрутки в веб-представлении, и я смогу установить правильную высоту для контейнера просмотра прокрутки (путем установки scrollview.contentSize).

Я пробовал размерToFit и 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.

Ответы

Ответ 1

Я думаю, что прочитал каждый ответ на эту тему, и все, что у меня было, было частью решения. Большую часть времени я потратил на попытки реализовать метод KVO, описанный @davew, который иногда работал, но большую часть времени оставлял пустое пространство под содержимым контейнера 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
            })
        }

        })
}

Конечно, важно правильно установить ограничения, чтобы scrollView изменял размеры в соответствии с ограничением containerHeight.

Как оказалось, метод навигации didFinish никогда не вызывается, когда я хотел, но, установив шаг document.readyState, следующий (document.body.offsetHeight) вызывается в нужный момент, возвращая мне правильное число для высоты.

Ответ 2

Вы можете использовать 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 scrollview changed.

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

Это позволит сэкономить использование JavaScript и оставить вас в курсе всех изменений.

Ответ 3

Мне приходилось заниматься этим вопросом совсем недавно. В конце концов, я использовал модификацию решения, предложенного Крисом МакКленагеном.

Собственно, его оригинальное решение довольно хорошо, и оно работает в самых простых случаях. Однако он работал только для меня на страницах с текстом. Вероятно, он также работает на страницах с изображениями, которые имеют статическую высоту. Однако это определенно не работает, если у вас есть изображения, размер которых определяется атрибутами max-height и max-width.

И это потому, что эти элементы могут быть изменены после загрузки страницы. Таким образом, высота, возвращаемая в 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)
        }

    }
}

Это решение, безусловно, самое элегантное, что я мог бы придумать. Есть, однако, две вещи, о которых вы должны знать.

Во-первых, перед загрузкой URL-адреса вы должны установить для параметра shouldListenToResizeNotification значение false. Эта дополнительная логика необходима для случаев, когда загруженный URL-адрес может быстро изменяться. Когда это происходит, уведомления из старого контента по какой-то причине могут пересекаться с сообщениями из нового контента. Чтобы предотвратить такое поведение, я создал эту переменную. Это гарантирует, что как только мы начнем загрузку нового контента, мы больше не будем обрабатывать уведомление из старого, и только возобновим обработку уведомлений о размерах после загрузки нового контента.

Самое главное, однако, вы должны знать об этом:

Если вы примете это решение, вам необходимо учесть, что если вы измените размер своего WKWebView на все, кроме размера, указанного уведомлением, уведомление будет снова 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 изменится, и уведомление будет опубликовано снова.

Будьте внимательны к таким ситуациям, и в противном случае вы должны быть в порядке.

И, пожалуйста, дайте мне знать, если вы обнаружите какие-либо проблемы с этим решением. Я полагаюсь на него сам, поэтому лучше знать, есть ли какие-то недостатки, которые я не заметил!

Ответ 4

Попробуйте следующее. Везде, где вы создаете экземпляр своего экземпляра 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

Вам нужно подождать, пока веб-просмотр завершит загрузку. Вот рабочий пример, который я использовал

Функция загрузки WKWebView Content никогда не вызывается

Затем, после того как веб-просмотр завершит загрузку, вы сможете определить высоту, в которой вы нуждаетесь,

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

   println(webView.scrollView.contentSize.height)

}

Ответ 6

Работает для меня

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

Ответ 7

используя @Andriy ответ, и этот ответ мне удалось установить высоту contentSize в WKWebView и изменить его высоту.

вот полный быстрый код 4:

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

Ответ 8

В большинстве ответов используется "document.body.offsetHeight".

Это скрывает последний объект тела.

Я преодолел эту проблему, используя наблюдателя KVO, который прослушивает изменения в WKWebview "contentSize", а затем запускает этот код:

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

Это не самый красивый код, но он работал для меня.

Ответ 9

Вы также можете получить высоту содержимого WKWebView с помощью valuJavaScript.

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

                  }
              }];
}

Ответ 10

Я пробовал просмотр прокрутки clientHeight, и я попытался оценить javascript в документе, используя clientHeight, offsetHeight и т.д.

Для меня в конечном итоге это было: document.body.scrollHeight. Или используйте scrollHeight вашего верхнего элемента, например, div контейнера.

Я слушаю loading свойств WKWebview loading KVO:

[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;
}];

Ответ 11

Я пробовал Javascript версию в UITableViewCell, и он отлично работает. Однако, если вы хотите поместить его в scrollView. Я не знаю, почему, высота может быть выше, но не может быть короче. Однако здесь я нашел решение UIWebView. fooobar.com/questions/1570520/...

Он также работает в 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
            })
        }
    })
}

Ответ 12

Это небольшое редактирование ответа @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. Я уверен, что есть сценарии, в которых это может сломаться, но я думаю, что это будет хорошо для статического контента, и если вы отображаете весь контент без необходимости прокрутки пользователем.

Ответ 13

Также пытались реализовать различные методы и, наконец, пришли к решению. В результате я создал WKWebView с самоопределением размера, который адаптирует его intrinsicContentSize к размеру его содержимого. Таким образом, вы можете использовать его в Auto Layouts. В качестве примера я сделал вид, который может помочь вам отобразить математическую формулу в приложениях iOS: https://github.com/Mazorati/SVLatexView

Ответ 14

Вам нужно добавить задержку, ее работу для меня, а не решения с JS выше:

func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
    DispatchQueue.main.asyncAfter(deadline: .now() + 0.1, execute: {
        print(size: webView.scrollView.contentSize)
    })
}

Ответ 15

После многих экспериментов мне удалось найти решение, которое работает для меня, и я нашел, что сделать динамический рост веб-просмотра без использования javascript, а также без использования постоянной высоты из веб-просмотра, это работает со мной, как талисман, а также работает, когда я добавляю новый стиль HTML и играть с размерами шрифта и высотой

код в Swift

1- дайте вашему делегату по Webview навигации

  webView.navigationDelegate = self

2- в расширении делегации

extension yourclass : WKNavigationDelegate {
      func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
        // Handel Dynamic Height For Webview Loads with HTML
       // Most important to reset webview height to any desired height i prefer 1 or 0  
        webView.frame.size.height = 1
        DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
        // here get height constant and assign new height in it 
            if let constraint = (webView.constraints.filter{$0.firstAttribute == .height}.first) {
                constraint.constant = webView.scrollView.contentSize.height
            }
 }

надеюсь, это сработает и у вас, ребята ** обратите внимание, это не все мои усилия, я много искал в StackOverflow и на других сайтах, и это то, что в конечном итоге работает со мной и с большим количеством тестирования