¿Cómo determinar el tamaño de contenido de un WKWebView?

Estoy experimentando con la sustitución de una instancia asignada dinámicamente de UIWebView con una instancia de WKWebView cuando se ejecuta con iOS 8 y posterior, y no puedo encontrar una manera de determinar el tamaño de contenido de una WKWebView.

Mi vista web está incrustada en un contenedor UIScrollView más grande y, por lo tanto, necesito determinar el tamaño ideal para la vista web. Esto me permitirá modificar su marco para mostrar todo su contenido HTML sin la necesidad de desplazarme dentro de la vista web, y seré capaz de establecer la altura correcta para el contenedor de vista de desplazamiento (configurando scrollview.contentSize).

He intentado sizeToFit y sizeThatFits sin éxito. Aquí está mi código que crea una instancia de WKWebView y la agrega al contenedor 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]; 

Aquí hay un método experimental de 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)); } 

Y aquí está la salida que se genera:

 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} 

La llamada sizeToFit no tiene efecto y sizeThatFits siempre devuelve una altura de 1.

Podría usar Key-Value Observing (KVO) …

En su 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: %fx %f", scrollView.contentSize.width, scrollView.contentSize.height); } } 

Esto saveía el uso de JavaScript y lo mantendrá al tanto de todos los cambios.

Recientemente tuve que lidiar con este problema. Al final, estaba usando una modificación de la solución propuesta por Chris McClenaghan.

En realidad, su solución original es bastante buena y funciona en la mayoría de los casos simples. Sin embargo, solo funcionó para mí en páginas con text. Probablemente también funcione en páginas con imágenes que tengan una altura estática. Sin embargo, definitivamente no funciona cuando tienes imágenes cuyo tamaño se define con attributes max-height y max-width .

Y esto se debe a que esos elementos pueden cambiar de tamaño después de cargar la página. Entonces, en realidad, la altura devuelta en onLoad siempre será correcta. Pero solo será correcto para esa instancia en particular. La solución consiste en controlar el cambio en la altura del body y responder a él.

Monitorizar el cambio de tamaño del 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) } } } 

Esta solución es, con mucho, la más elegante que podría imaginar. Hay, sin embargo, dos cosas de las que debe tener en count.

En primer lugar, antes de cargar su URL, debe establecer shouldListenToResizeNotification en false . Esta lógica extra es necesaria para casos en los que la URL cargada puede cambiar rápidamente. Cuando esto ocurre, las notifications de contenido antiguo por alguna razón pueden superponerse con las del contenido nuevo. Para evitar tal comportamiento, creé esta variable. Asegura que una vez que comencemos a cargar nuevo contenido, ya no procesemos la notificación del anterior y solo reanudaremos el procesamiento de las notifications de cambio de tamaño una vez que se haya cargado el contenido nuevo.

Lo más importante, sin embargo, debe ser consciente de esto:

Si adopta esta solución, debe tener en count que si cambia el tamaño de su WKWebView a cualquier otro que no sea el tamaño informado por la notificación, la notificación se activará nuevamente.

Tenga cuidado con esto, ya que es fácil ingresar en un bucle infinito. Por ejemplo, si decide manejar la notificación haciendo que su altura sea igual a la altura reportada + algún relleno adicional:

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

Como puedes ver, porque agrego 8 a la altura reportada, después de esto, el tamaño de mi body cambiará y la notificación se publicará nuevamente.

Esté alerta a tales situaciones y, de lo contrario, debería estar bien.

Y, por favor, hágamelo saber si descubre algún problema con esta solución. ¡Confío en él mismo, así que es mejor saber si hay fallas que no he visto!

Prueba lo siguiente. Dondequiera instale su instancia de WKWebView, agregue algo similar a lo siguiente:

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

A continuación, agregue un método similar al siguiente al que siempre obedece el protocolo WKScriptMessageHandler para manejar el post:

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

Esto funciona para mí.

Si tiene algo más que text en su documento, es posible que necesite envolver el javascript de esta forma para asegurarse de que todo está cargado:

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

NOTA: esta solución no aborda las actualizaciones en curso del documento.

Debe esperar a que la vista web termine de cargarse. Aquí hay un ejemplo de trabajo que utilicé

La function cargada de contenido WKWebView nunca se llama.

Luego, una vez finalizada la carga de la vista web, puede determinar las alturas que necesita

 func webView(webView: WKWebView!, didFinishNavigation navigation: WKNavigation!) { println(webView.scrollView.contentSize.height) } 

Aunque la pregunta se ha preguntado desde hace bastante time, y es probable que el autor haya seguido adelante, deseo dar mi respuesta porque podría haber alguien como yo golpeando su cabeza tratando de encontrar la solución.

Creo que leí todas las respuestas sobre este tema y todo lo que tuve fue parte de la solución. La mayoría de las veces pasé tratando de implementar el método KVO como lo describió @davew, que ocasionalmente funcionaba, pero la mayor parte del time dejaba un espacio en blanco bajo el contenido de un contenedor WKWebView. También implementé la sugerencia de @David Beck e hice que la altura del contenedor sea 0, evitando así la posibilidad de que el problema ocurra si la altura del contenedor es mayor que la del contenido. A pesar de eso tuve ese espacio en blanco ocasional. Entonces, para mí, el observador "contentSize" tenía muchos defectos. No tengo mucha experiencia con las tecnologías web, así que no puedo responder cuál fue el problema con esta solución, pero vi que si solo imprimo altura en la console pero no hago nada con ella (por ejemplo, cambiar el tamaño de las restricciones), Salta a algún número (por ejemplo, 5000) y va al número anterior a ese más alto (por ejemplo, 2500, que resulta ser el correcto). Si ajusté la restricción de altura a la altura que obtengo de "contentSize", se establece en el número más alto que se obtiene y nunca se cambia de tamaño a la correcta, lo que, una vez más, es mencionado por @David Beck.

Después de muchos experimentos he logrado encontrar una solución que funcione para mí:

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

Por supuesto, es importante establecer las restricciones correctamente para cambiar el tamaño de scrollView de acuerdo con la restricción containerHeight.

Resulta que el método de navigation Finish nunca se invoca cuando quería, pero después de haber configurado el paso "document.readyState", el siguiente ("document.body.offsetHeight") recibe el llamado en el momento correcto, devolviéndome el número correcto para la altura .

¡Espero que funcione para usted también!

He probado la vista de desplazamiento KVO y he intentado evaluar javascript en el documento, usando clientHeight , offsetHeight , etc.

Lo que funcionó para mí eventualmente es: document.body.scrollHeight . O use el scrollHeight de su elemento principal, por ejemplo, un contenedor div .

Escucho los cambios de la propiedad de loading WKWebview usando KVO:

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

Y entonces:

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

La implementación 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; }];