previous - wkwebview before ios11
¿Cómo determinar el tamaño del contenido de un WKWebView? (10)
Creo que leí todas las respuestas sobre este tema y todo lo que tenía era parte de la solución. La mayor parte del tiempo pasé tratando de implementar el método KVO como lo describe @davew, que en ocasiones funcionó, pero la mayor parte del tiempo dejó un espacio en blanco debajo del contenido de un contenedor WKWebView. También implementé la sugerencia de @David Beck e hice que la altura del contenedor fuera 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" tuvo muchos defectos. No tengo mucha experiencia con las tecnologías web, por lo que no puedo responder cuál fue el problema con esta solución, pero vi que si solo imprimo altura en la consola pero no hago nada con ella (por ejemplo, redimensionar las restricciones), salta a algún número (por ejemplo, 5000) y luego va al número anterior al más alto (por ejemplo, 2500, que resulta ser el correcto). Si configuro la restricción de altura a la altura que obtengo de "contentSize", se establece en el número más alto que obtiene y nunca se redimensiona al correcto, lo que, de nuevo, lo menciona el comentario de @David Beck.
Después de muchos experimentos, he logrado encontrar una solución que me funciona:
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 que scrollView cambie de tamaño de acuerdo con la restricción containerHeight.
Resulta que el método de navegación Finish nunca se llama cuando quería, pero después de configurar document.readyState
step, se llama al siguiente ( document.body.offsetHeight
) en el momento correcto, devolviéndome el número correcto para la altura.
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 más reciente, y no puedo encontrar una manera de determinar el tamaño del contenido de un WKWebView.
Mi vista web está incrustada dentro de un contenedor UIScrollView más grande y, por lo tanto, debo 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 podré 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.
He probado la vista de desplazamiento KVO y he intentado evaluar javascript en el documento, usando clientHeight
, offsetHeight
, etc.
Lo que finalmente me funcionó es: document.body.scrollHeight
. O use el scrollHeight
de su elemento más superior, por ejemplo, un contenedor div
.
Escucho los cambios de propiedad de WKWebview loading
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 de 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;
}];
Intenta lo siguiente. Siempre que cree una instancia de 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]];
Luego, agregue un método similar al siguiente al que cada clase obedece el protocolo WKScriptMessageHandler para manejar el mensaje:
- (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 mi
Si tiene más de texto en su documento, es posible que deba ajustar el javascript de esta manera 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 continuas del documento.
La mayoría de las respuestas están utilizando "document.body.offsetHeight".
Esto oculta el último objeto del cuerpo.
Superé este problema usando un observador KVO que escucha los cambios en "contentSize" de WKWebview y luego ejecuto este código:
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
}
)
No es el código más bonito posible, pero funcionó para mí.
Necesita agregar demora, funciona para mí, en lugar de soluciones con JS arriba:
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1, execute: {
print(size: webView.scrollView.contentSize)
})
}
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: %f x %f", scrollView.contentSize.width, scrollView.contentSize.height);
}
}
Esto ahorraría el uso de JavaScript y lo mantendría informado sobre todos los cambios.
Probé la versión de Javascript en UITableViewCell, y funciona perfectamente. Sin embargo, si quieres ponerlo en el scrollView. No sé por qué, la altura puede ser más alta pero no puede ser más corta. Sin embargo, encontré una solución UIWebView aquí. https://.com/a/48887971/5514452
También funciona en WKWebView. Creo que el problema se debe a que WebView necesita un retransmisión, pero de alguna manera no se reducirá y solo se podrá ampliar. Necesitamos restablecer la altura y definitivamente se redimensionará.
Edición: reinicio la altura del marco después de establecer la restricción porque en algún momento no funcionará debido a que la altura del marco se establece en 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
})
}
})
}
Tienes que esperar a que la webview termine de cargar. Aquí hay un ejemplo de trabajo que utilicé.
La función de contenido cargado de WKWebView nunca se llama
Luego, una vez que se haya cargado la vista web, puede determinar las alturas que necesita por
func webView(webView: WKWebView!, didFinishNavigation navigation: WKNavigation!) {
println(webView.scrollView.contentSize.height)
}
Tuve que lidiar con este problema yo mismo recientemente. 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 los casos más simples. Sin embargo, solo funcionó para mí en páginas con texto. Probablemente también funciona en páginas con imágenes que tienen una altura estática. Sin embargo, definitivamente no funciona cuando tienes imágenes cuyo tamaño está definido con los atributos de max-height
max-width
.
Y esto se debe a que esos elementos pueden redimensionarse después de que se carga 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 es controlar el cambio de la altura del body
y responder a él.
Monitor de redimensionamiento del document.body
Cuerpo
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 pueda encontrar. Hay, sin embargo, dos cosas que debes tener en cuenta.
En primer lugar, antes de cargar su URL, debe establecer shouldListenToResizeNotification
en false
. Esta lógica adicional es necesaria para los casos en que la URL cargada puede cambiar rápidamente. Cuando esto ocurre, las notificaciones de contenido antiguo por alguna razón pueden superponerse con las del contenido nuevo. Para prevenir tal comportamiento, creé esta variable. Asegura que una vez que comencemos a cargar nuevo contenido ya no procesemos la notificación de la anterior y solo reanudaremos el procesamiento de notificaciones de cambio de tamaño después de que se cargue el nuevo contenido.
Lo más importante, sin embargo, debe ser consciente de esto:
Si adopta esta solución, debe tener en cuenta que si cambia el tamaño de su WKWebView
a otro que no sea el tamaño notificado por la notificación, la notificación se activará nuevamente.
Tenga cuidado con esto, ya que es fácil entrar en un bucle infinito. Por ejemplo, si decide manejar la notificación haciendo que su altura sea igual a la altura informada + 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 puede ver, debido a que estoy agregando 8 a la altura informada, una vez hecho 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, avíseme si descubre algún problema con esta solución. Confío en mí mismo, por lo que es mejor saber si hay fallas que no he detectado.
Usando la respuesta de @ Andriy y esta respuesta, pude establecer get height of contentSize en WKWebView y cambiar su altura.
Aquí está el código swift 4 completo:
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)
}
}