Jak określić rozmiar zawartości WKWebView?

Eksperymentuję z zastąpieniem dynamicznie przydzielanej instancji UIWebView instancją WKWebView, gdy działa pod systemem iOS 8 i nowszym, i nie mogę znaleźć sposobu, aby określić rozmiar zawartości WKWebView.

Mój widok sieci Web jest osadzony w większym kontenerze UIScrollView, dlatego muszę określić idealny rozmiar dla widoku sieci web. To pozwoli mi zmodyfikować jego ramkę, aby pokazać całą zawartość HTML bez konieczności przewijania w widoku sieci, a ja będę możliwość ustawienia właściwej wysokości dla kontenera widoku przewijania (poprzez ustawienie widoku przewijania.contentSize).

Próbowałem sizeToFit i sizeThatFits bez powodzenia. Oto Mój kod, który tworzy instancję WKWebView i dodaje ją do container scrollview:

// 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];

Oto eksperymentalna metoda 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));
}

A oto wynik, który jest generowany:

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}

Wywołanie sizeToFit nie ma wpływu i sizeThatFits zawsze Zwraca wysokość 1.

Author: Mark Smith, 2014-12-17

10 answers

Myślę, że przeczytałem każdą odpowiedź na ten temat i wszystko, co miałem, było częścią rozwiązania. Większość czasu spędziłem próbując zaimplementować metodę KVO opisaną przez @ davew, która czasami działała, ale większość czasu pozostawiła białą spację pod zawartością kontenera WKWebView. Zaimplementowałem również sugestię @ David Beck i sprawiłem, że wysokość kontenera wynosi 0, unikając w ten sposób możliwości wystąpienia problemu, jeśli wysokość kontenera jest większa niż zawartość. Na przekór, że ja czasami miałem puste miejsce. Jak dla mnie "contentSize" obserwator miał wiele wad. Nie mam dużego doświadczenia z technologiami webowymi więc nie mogę odpowiedzieć na co był problem z tym rozwiązaniem, ale widziałem, że jeśli tylko drukuję wysokość w konsoli, ale nic z tym nie robię (np. 5000), a następnie przechodzi do liczby przed tą najwyższą (np. 2500-która okazuje się być prawidłowa). Jeśli ustawię ograniczenie wysokości na wysokość, którą dostaję z "contentSize" ustawia się na najwyższą liczbę, jaką dostaje i nigdy nie zmienia rozmiaru na poprawną - o czym ponownie wspomina komentarz @David Beck.

Po wielu eksperymentach udało mi się znaleźć rozwiązanie, które działa dla mnie:

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

        })
}

Oczywiście ważne jest prawidłowe ustawienie ograniczeń tak, aby scrollView zmieniał rozmiar zgodnie z ograniczeniem containerHeight.

Jak się okazuje, metoda nawigacji didFinish nigdy nie zostanie wywołana, gdy Chciałem, ale po ustawieniu document.readyState kroku, następny (document.body.offsetHeight) zostanie wywołany w odpowiednim momencie, zwracając mi odpowiedni numer dla wysokości.

 31
Author: IvanMih,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2018-10-02 13:46:34

Możesz użyć obserwacji wartości klucza (KVO)...

In your 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);
    }
}

Zapisałoby to użycie JavaScript i utrzymywałoby cię w pętli na wszystkich zmianach.

 20
Author: davew,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2015-10-28 18:10:25

Sam musiałem się ostatnio z tym uporać. W końcu korzystałem z modyfikacji rozwiązania zaproponowanego przez Chrisa Mcclenaghana.

Właściwie, jego oryginalne rozwiązanie jest całkiem dobre i działa w najprostszych przypadkach. Jednak u mnie działało to tylko na stronach z tekstem. Prawdopodobnie działa również na stronach z obrazami o wysokości statycznej. Jednak zdecydowanie nie działa, gdy masz obrazy, których rozmiar jest zdefiniowany za pomocą atrybutów max-height i max-width.

I dzieje się tak dlatego, że te elementy mogą zostać zmienione Po załadowaniu strony. Tak więc wysokość zwrócona w onLoad zawsze będzie prawidłowa. Ale będzie to poprawne tylko dla tego konkretnego przypadku. Obejście polega na monitorowaniu zmiany wysokości body i reagowaniu na nią.

Zmiana rozmiaru monitora 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)
        }

    }
}

To rozwiązanie jest zdecydowanie najbardziej eleganckie, jakie mogłem wymyślić. Są jednak dwie rzeczy, o których powinieneś wiedzieć.

Po pierwsze, przed załadowaniem adresu URL należy ustawić shouldListenToResizeNotification na false. Ta dodatkowa logika jest potrzebna w przypadkach, gdy załadowany adres URL może się szybko zmienić. Gdy tak się stanie, powiadomienia ze starej zawartości z jakiegoś powodu mogą pokrywać się z powiadomieniami z nowej zawartości. Aby zapobiec takim zachowaniom, stworzyłem tę zmienną. Zapewnia to, że po rozpoczęciu ładowania nowej zawartości nie przetwarzamy już powiadomień ze starego i wznawiamy przetwarzanie powiadomień o zmianie rozmiaru dopiero po załadowaniu nowej zawartości.

Najbardziej co ważne, musisz być tego świadomy:

Jeśli zastosujesz To rozwiązanie, musisz wziąć pod uwagę, że jeśli zmienisz rozmiar WKWebView na cokolwiek innego niż rozmiar zgłoszony przez powiadomienie-powiadomienie zostanie uruchomione ponownie .

Uważaj z tym, ponieważ łatwo jest wejść w nieskończoną pętlę. Na przykład, jeśli zdecydujesz się obsłużyć powiadomienie, czyniąc wysokość równą zgłoszonej wysokości + dodatkowe padding:

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

Jak widać, ponieważ dodaję 8 do zgłoszonej wysokości, po tym jak to się stanie rozmiar mojego body zmieni się i powiadomienie zostanie ponownie wysłane.

Bądź czujny w takich sytuacjach, a w przeciwnym razie powinieneś być w porządku.

I daj mi znać, jeśli odkryjesz jakiekolwiek problemy z tym rozwiązaniem - sam na nie liczę, więc najlepiej jest wiedzieć, czy są jakieś usterki, których nie zauważyłem!

 7
Author: Andriy Gordiychuk,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2018-05-25 14:45:58

Musisz poczekać na zakończenie ładowania widoku sieci Web. Oto działający przykład, którego użyłem

WKWebView content loaded function never get called

Następnie po zakończeniu ładowania webview możesz określić wysokość potrzebną przez

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

   println(webView.scrollView.contentSize.height)

}
 5
Author: Brian Wells,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-05-23 12:10:26

Spróbuj wykonać następujące czynności. Gdziekolwiek tworzysz instancję swojej instancji WKWebView, dodaj coś podobnego do następującego:

    //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]];

Następnie dodaj metodę podobną do poniższej, do której zawsze Klasa używa protokołu WKScriptMessageHandler do obsługi wiadomości:

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

Jeśli masz więcej niż tekst w dokumencie, może być konieczne zawinięcie javascript w ten sposób, aby upewnić się, że wszystko jest załadowane:

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

Uwaga: To rozwiązanie nie dotyczy bieżących aktualizacji do dokumentu.

 5
Author: Chris McClenaghan,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2015-01-28 18:03:47

Próbowałem widoku przewijania KVO i próbowałem Oceniać javascript na dokumencie, używając clientHeight, offsetHeight, itd...

W końcu udało mi się: document.body.scrollHeight. Możesz też użyć scrollHeight swojego najwyższego elementu, np. kontenera div.

Słucham zmian właściwości loading WKWebview przy użyciu KVO:

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

A potem:

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

Realizacja 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
Author: natanavra,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2016-12-25 13:49:01

Używając odpowiedzi @Andriy i tej odpowiedzi udało mi się ustawić get height of contentSize w WKWebView I zmienić jego wysokość.

Oto Pełny kod swift 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'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)
        }
    }
 1
Author: Mohammadalijf,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-10-23 13:51:04

Musisz dodać opóźnienie, jego działa dla mnie, zamiast rozwiązań z js powyżej:

func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
    DispatchQueue.main.asyncAfter(deadline: .now() + 0.1, execute: {
        print(size: webView.scrollView.contentSize)
    })
}
 1
Author: Сергей Билык,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2018-02-28 09:27:07

Większość odpowiedzi używa "document.ciało.offsetHeight".

To ukrywa ostatni przedmiot ciała.

Pokonałem ten problem używając obserwatora KVO nasłuchującego zmian w WKWebview "contentSize", a następnie uruchamiając ten kod:

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
    }
)
Nie jest to najładniejszy kod, ale mi się udało.
 0
Author: Stratubas,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2018-02-24 22:11:51

Wypróbowałem wersję Javascript w UITableViewCell i działa idealnie. Jeśli jednak chcesz umieścić go w widoku przewijania. Nie wiem dlaczego, wysokość może być wyższa, ale nie może być krótsza. Jednak znalazłem rozwiązanie UIWebView tutaj. https://stackoverflow.com/a/48887971/5514452

Działa również w WKWebView. Myślę, że problem polega na tym, że WebView wymaga relayout, ale jakoś nie kurczy się i może tylko powiększyć. Musimy zresetować wysokość i na pewno będzie Zmień rozmiar.

Edit: resetuję wysokość Ramki po ustawieniu ograniczenia, ponieważ czasami nie będzie działać z powodu ustawienia wysokości ramki na 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
Author: Jeff Zhang,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2018-08-24 04:11:41