Как определить размер содержимого WKWebView?
я экспериментирую с заменой динамически выделенного экземпляра UIWebView экземпляром WKWebView при работе под iOS 8 и новее, и я не могу найти способ определить размер содержимого WKWebView.
мой веб-вид встроен в больший контейнер UIScrollView, и поэтому мне нужно определить идеальный размер для веб-представления. Это позволит мне изменить его фрейм, чтобы показать все его содержимое HTML без необходимости прокрутки в веб-представлении, и я сможет установить правильную высоту для контейнера вида прокрутки (установив scrollview.contentSize).
Я пробовал sizeToFit и sizeThatFits без успеха. Вот мой код, который создает экземпляр WKWebView и добавляет его в контейнер 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];
вот экспериментальный метод дидфинишнавигации:
- (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.
10 ответов
Я думаю, что прочитал каждый ответ на этот вопрос, и все, что у меня было, было частью решения. Большую часть времени я пытался реализовать метод KVO, описанный @davew, который иногда работал, но большую часть времени оставлял пустое пространство под содержимым контейнера WKWebView. Я также реализовал предложение @ David Beck и сделал высоту контейнера равной 0, таким образом, избегая возможности возникновения проблемы, если высота контейнера больше, чем у содержимого. Несмотря на это, я было это случайное пустое место. Итак, для меня "contentSize" observer имел много недостатков. У меня нет большого опыта работы с веб-технологиями, поэтому я не могу ответить, в чем проблема с этим решением, но я видел, что если я только печатаю высоту в консоли, но ничего не делаю с ней (например. измените размер ограничений), он переходит к некоторому числу (например, 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
) вызывается в нужный момент, возвращая мне правильный номер для высоты.
вы можете использовать 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's scrollview changed.
UIScrollView *scrollView = self.webView.scrollView;
NSLog(@"New contentSize: %f x %f", scrollView.contentSize.width, scrollView.contentSize.height);
}
}
это сохранит использование JavaScript и сохранит вас в цикле всех изменений.
недавно мне пришлось самому заниматься этим вопросом. В конце концов, я использовал модификацию решение, предложенное Крис Макленаган.
на самом деле, его оригинальное решение довольно хорошо, и это работает в большинстве простых случаев. Однако, это работало только для меня на страницах с текстом. Вероятно, он также работает на страницах с изображениями, которые имеют статическую высоту. Однако это определенно не работает, когда у вас есть изображения, размер которых определяется с помощью 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
to false
. Эта дополнительная логика необходима для случаев, когда загруженный URL-адрес может быстро измениться. Когда это происходит, уведомления из старого контента по какой-то причине могут перекрываться с уведомлениями из нового контента. Чтобы предотвратить такое поведение, я создал эту переменную. Это гарантирует, что как только мы начнем загружать новый контент, мы больше не обрабатываем уведомления от старого и только возобновляем обработка уведомлений об изменении размера после загрузки нового содержимого.
самое главное, однако, вы должны знать об этом:
если вы принимаете это решение, вам нужно учитывать, что если вы измените размер вашего 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
изменится, и уведомление будет опубликовано снова.
быть готовым к таким ситуациям и в противном случае вы должны быть хорошо.
и, пожалуйста, дайте мне знать, если вы обнаружите какие-либо проблемы с этим решением - я опираюсь на это сам, так что лучше знайте, если есть некоторые недостатки, которые я не заметил!
вам нужно дождаться завершения загрузки webview. Вот рабочий пример, который я использовал
загруженная функция содержимого WKWebView никогда не вызывается
затем после завершения загрузки webview вы можете определить необходимые высоты с помощью
func webView(webView: WKWebView!, didFinishNavigation navigation: WKNavigation!) {
println(webView.scrollView.contentSize.height)
}
попробуйте следующее. Везде, где вы создаете экземпляр 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});};"
Примечание: это решение не касается текущих обновлений документа.
я попробовал просмотр прокрутки KVO, и я попытался оценить javascript в документе, используя clientHeight
, offsetHeight
, etc...
то, что сработало для меня в конечном итоге:document.body.scrollHeight
. Или используйте scrollHeight
вашего верхнего самого элемента, например контейнера div
.
слушаю loading
wkwebview изменения свойств с помощью 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;
}];
используя ответ @Andriy и ответ я смог установить get height of contentSize в WKWebView и изменить его высоту.
вот полный 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)
}
}
вам нужно добавить задержку, ее работу для меня, а не решения с JS выше:
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1, execute: {
print(size: webView.scrollView.contentSize)
})
}
большинство ответов используют " документ.тело.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
}
)
это не самый красивый код, но он сработал для меня.
Я пробовал версию Javascript в UITableViewCell, и она отлично работает. Однако, если вы хотите поместить его в scrollView. Я не знаю, почему, высота может быть выше, но не может быть короче. Тем не менее, я нашел решение UIWebView здесь. https://stackoverflow.com/a/48887971/5514452
Он также работает в WKWebView. Я думаю, проблема в том, что WebView нужен relayout, но каким-то образом он не будет сжиматься и может только увеличиваться. Нам нужно сбросить высоту, и это будет определенно изменить размер.
Edit: я сбрасываю высоту кадра после установки ограничения, потому что когда-нибудь он не будет работать из-за установки высоты кадра в 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
})
}
})
}