web-dev-qa-db-fra.com

Comment déterminer la taille du contenu d'un WKWebView?

J'essaie de remplacer une instance d'UIWebView allouée dynamiquement par une instance de WKWebView sous iOS 8 et versions ultérieures, et je ne trouve aucun moyen de déterminer la taille du contenu d'un WKWebView.

Ma vue Web est intégrée à un conteneur UIScrollView plus grand. Par conséquent, je dois déterminer la taille idéale pour la vue Web. Cela me permettra de modifier son cadre pour afficher tout son contenu HTML sans qu'il soit nécessaire de faire défiler l'affichage Web, et je serai en mesure de définir la hauteur correcte pour le conteneur d'affichage de défilement (en définissant scrollview.contentSize).

J'ai essayé sizeToFit et sizeThatFits sans succès. Voici mon code qui crée une instance WKWebView et l'ajoute au conteneur 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];

Voici une méthode expérimentale 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));
}

Et voici le résultat généré:

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}

L'appel sizeToFit n'a pas d'effet et sizeThatFits renvoie toujours une hauteur de 1.

35
Mark Smith

Je pense avoir lu toutes les réponses à ce sujet et que tout ce qui me restait faisait partie de la solution. La plupart du temps, j'ai passé à essayer d'implémenter la méthode KVO décrite par @davew, ce qui a parfois fonctionné, mais la plupart du temps, il a laissé un espace sous le contenu d'un conteneur WKWebView. J'ai également implémenté la suggestion de @David Beck et fait en sorte que la hauteur du conteneur soit égale à 0, évitant ainsi que le problème se produise si la hauteur du conteneur est supérieure à celle du contenu. En dépit de cela, j'ai eu cet espace vide occasionnel. Donc, pour moi, l’observateur de "contentSize" avait beaucoup de défauts. Je n’ai pas beaucoup d’expérience avec les technologies Web, je ne peux donc pas vous dire quel était le problème avec cette solution, mais j’ai bien compris que si j’imprimais uniquement la hauteur dans la console mais ne faisais rien avec elle (par exemple, redimensionnez les contraintes), il saute à un certain nombre (par exemple 5000) et que va au numéro qui précède celui le plus élevé (par exemple 2500 - qui s'avère être le bon). Si je règle la contrainte de hauteur à la hauteur que je tire de "contentSize", elle se fixe elle-même au nombre le plus élevé qu'elle obtient et ne soit jamais redimensionnée à la valeur correcte - ce qui est, à nouveau, mentionné par @David Beck comment.

Après de nombreuses expériences, j'ai réussi à trouver une solution qui fonctionne pour moi:

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

        })
}

Bien entendu, il est important de définir les contraintes correctement pour que scrollView soit redimensionné en fonction de la contrainte containerHeight. 

Comme il s'avère que la méthode de navigation didFinish ne s'appelle jamais quand je le voulais, mais après avoir défini document.readyState step, la suivante (document.body.offsetHeight) est appelée au bon moment, me renvoyant le bon numéro pour la hauteur.

46
IvanMih

Vous pouvez utiliser l'observation de valeur-clé (KVO) ...

Dans votre 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);
    }
}

Cela économiserait l'utilisation de JavaScript et vous tiendrait au courant de toutes les modifications.

20
davew

J'ai dû faire face à ce problème moi-même récemment. Au final, j’utilisais une modification de la solution proposée par Chris McClenaghan .

En fait, sa solution initiale est très bonne et fonctionne dans la plupart des cas simples. Cependant, cela ne fonctionnait que pour moi sur les pages contenant du texte. Cela fonctionne probablement aussi sur les pages contenant des images statiques. Cependant, cela ne fonctionne vraiment pas si vous avez des images dont la taille est définie avec les attributs max-height et max-width.

Et c’est parce que ces éléments peuvent être redimensionnés après la page est chargée. Donc, en réalité, la hauteur retournée dans onLoad sera toujours correcte. Mais ce ne sera correct que pour ce cas particulier. La solution de contournement consiste à surveiller le changement de la hauteur body et à y répondre.

Surveiller le redimensionnement du 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)
        }

    }
}

Cette solution est de loin la plus élégante que je puisse trouver. Il y a cependant deux choses dont vous devez être conscient.

Tout d’abord, avant de charger votre URL, vous devez définir shouldListenToResizeNotification sur false. Cette logique supplémentaire est nécessaire dans les cas où l'URL chargée peut changer rapidement. Lorsque cela se produit, les notifications de l'ancien contenu pour une raison quelconque peuvent se chevaucher avec celles du nouveau contenu. Pour prévenir un tel comportement, j'ai créé cette variable. Cela garantit qu'une fois que nous avons commencé à charger du nouveau contenu, nous ne traitons plus les notifications de l'ancien et que nous reprenons le traitement des notifications de redimensionnement une fois le nouveau contenu chargé.

Plus important encore, vous devez être conscient de ceci:

Si vous adoptez cette solution, vous devez prendre en compte le fait que si vous modifiez la taille de votre WKWebView par une valeur autre que celle indiquée par la notification, la notification sera déclenchée à nouveau.

Soyez prudent avec cela car il est facile d'entrer dans une boucle infinie. Par exemple, si vous décidez de gérer la notification en rendant votre hauteur égale à celle signalée + un rembourrage supplémentaire:

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

Comme vous pouvez le constater, étant donné que j'ajoute 8 à la hauteur indiquée, une fois cette opération effectuée, la taille de ma body changera et la notification sera à nouveau publiée.

Soyez vigilant face à de telles situations et sinon, ça devrait aller.

Et s'il vous plaît laissez-moi savoir si vous découvrez des problèmes avec cette solution - je m'y fie moi-même, il est donc préférable de savoir s'il y a des défauts que je n'ai pas repérés!

9
Andriy Gordiychuk

Essayez ce qui suit. Où que vous instanciez votre instance WKWebView, ajoutez quelque chose de similaire au suivant:

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

Ajoutez ensuite une méthode semblable à la suivante, à laquelle la classe obéit toujours au protocole WKScriptMessageHandler pour gérer le message:

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

Cela fonctionne pour moi.

Si votre document contient plus que du texte, vous devrez peut-être encapsuler le code javascript comme ceci pour vous assurer que tout est chargé:

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

REMARQUE: cette solution n'aborde pas les mises à jour en cours du document.

5
Chris McClenaghan

Vous devez attendre que la webview termine le chargement. Voici un exemple de travail que j'ai utilisé 

La fonction WKWebView Content chargée ne se fait jamais appeler

Ensuite, une fois le chargement de WebView terminé, vous pouvez déterminer les hauteurs dont vous avez besoin en 

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

   println(webView.scrollView.contentSize.height)

}
5
Brian Wells

Vous pouvez également obtenir la hauteur de contenu de WKWebView en evaluJavaScript.

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

                  }
              }];
}
1
Ravi H Malviya

en utilisant la réponse de @ Andriy et cette réponse i a pu définir la hauteur de contentSize dans WKWebView et la modifier.

voici le code complet de 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
Mohammadalijf

J'ai essayé la vue par défilement KVO et j'ai essayé d'évaluer javascript sur le document en utilisant clientHeight, offsetHeight, etc.

Ce qui a finalement fonctionné pour moi est: document.body.scrollHeight. Ou utilisez la scrollHeight de votre élément le plus haut, par exemple. un conteneur div.

J'écoute les modifications de la propriété loading WKWebview à l'aide de KVO:

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

Et alors:

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

L'implémentation 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

La plupart des réponses utilisent "document.body.offsetHeight".

Cela cache le dernier objet du corps.

J'ai surmonté ce problème en utilisant un observateur KVO à l'écoute des changements dans WKWebview "contentSize", puis en exécutant ce code:

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

Ce n'est pas le code le plus joli possible, mais cela a fonctionné pour moi.

1
Stratubas

Travaille pour moi

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

J'ai essayé la version Javascript dans UITableViewCell, et cela fonctionne parfaitement. Cependant, si vous voulez le mettre dans le scrollView. Je ne sais pas pourquoi, la hauteur peut être plus haute mais pas plus courte. Cependant, j'ai trouvé une solution UIWebView ici. https://stackoverflow.com/a/48887971/5514452

Cela fonctionne aussi dans WKWebView. Je pense que le problème vient du fait que la WebView a besoin de relais, mais elle ne diminuera pas et ne pourra que s’agrandir. Nous devons réinitialiser la hauteur et le redimensionner.

Éditer: je réinitialise la hauteur du cadre après avoir défini la contrainte, car elle ne fonctionnera parfois pas car la hauteur du cadre est définie sur 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

Vous devez ajouter un délai, cela fonctionne pour moi, au lieu de solutions avec JS ci-dessus:

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