WKWebView didn't finish loading, when didFinishNavigation is called - Bug in WKWebView? - ios

Goal: To take a screenshot of WKWebView after the website finished loading
Method employed:
Defined a WKWebView var in UIViewController
Created an extension method called screen capture() that takes image of WKWebView
Made my UIViewController to implement WKNavigationDelegate
Set the wkwebview.navigationDelegate = self ( in the UIViewController init)
Implemented the didFinishNavigation delegation func in UIViewcontroller to call screen capture extension method for WKWebView
func webView(webView: WKWebView, didFinishNavigation navigation: WKNavigation!) {
let img = webView.screenCapture()
}
Questions:
When I debug i simulator, I notice that the control reaches the didFinishNavigation() func even though the website has not yet rendered in the WKWebView
Correspondingly the image screenshot taken is a white blob.
What am I missing here? I looked at all possible delegate functions for WKWebView and nothing else seem to represent the completion of content loading in WKWebView. Would appreciate help on if there is a work around
Update: Adding screenshot code that I am using to take a screenshot for web view
class func captureEntireUIWebViewImage(webView: WKWebView) -> UIImage? {
var webViewFrame = webView.scrollView.frame
if (webView.scrollView.contentSize != CGSize(width: 0,height: 0)){
webView.scrollView.frame = CGRectMake(webViewFrame.origin.x, webViewFrame.origin.y, webView.scrollView.contentSize.width, webView.scrollView.contentSize.height)
UIGraphicsBeginImageContextWithOptions(webView.scrollView.contentSize, webView.scrollView.opaque, 0)
webView.scrollView.layer.renderInContext(UIGraphicsGetCurrentContext())
var image:UIImage = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
webView.scrollView.frame = webViewFrame
return image
}
return nil
}

For those still looking for an answer to this, the marked answer is BS, he just forced his way into getting it accepted.
Using property,
"loading"
and
webView(webView: WKWebView, didFinishNavigation navigation: WKNavigation!)
both do the same thing, indicate if the main resource is loaded.
Now, that does not mean the entire webpage/website is loaded, because it really depends on the implementation of the website. If it needs to load scripts and resources (images, fonts etc) to make itself visible, you'll still see nothing after the navigation is completed, because the network calls made by the website are not tracked by the webview, only the navigation is tracked, so it wouldn't really know when the website loaded completely.

WKWebView doesn't use delegation to let you know when content loading is complete (that's why you can't find any delegate method that suits your purpose). The way to know whether a WKWebView is still loading is to use KVO (key-value observing) to watch its loading property. In this way, you receive a notification when loading changes from true to false.
Here's a looping animated gif showing what happens when I test this. I load a web view and respond to its loading property through KVO to take a snapshot. The upper view is the web view; the lower (squashed) view is the snapshot. As you can see, the snapshot does capture the loaded content:
[NSTimer scheduledTimerWithTimeInterval:1.0 repeats:YES block:^(NSTimer * _Nonnull timer) {
if (self->_webKitView.isLoading == true) {
NSLog(#"Still loading...");
}else {
NSLog(#"Finished loading...");
[timer invalidate];
dispatch_async(dispatch_get_main_queue(), ^{
[self->_activityIndicator stopAnimating];
});
}
}];

Here is how I solved it:
class Myweb: WKWebView {
func setupWebView(link: String) {
let url = NSURL(string: link)
let request = NSURLRequest(URL: url!)
loadRequest(request)
addObserver(self, forKeyPath: "loading", options: .New, context: nil)
}
override func observeValueForKeyPath(keyPath: String?, ofObject object: AnyObject?, change: [String : AnyObject]?, context: UnsafeMutablePointer<Void>) {
guard let _ = object as? WKWebView else { return }
guard let keyPath = keyPath else { return }
guard let change = change else { return }
switch keyPath {
case "loading":
if let val = change[NSKeyValueChangeNewKey] as? Bool {
if val {
} else {
print(self.loading)
//do something!
}
}
default:break
}
}
deinit {
removeObserver(self, forKeyPath: "loading")
}
}
Update Swift 3.1
override public func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
guard let _ = object as? WKWebView else { return }
guard let keyPath = keyPath else { return }
guard let change = change else { return }
switch keyPath {
case "loading":
if let val = change[NSKeyValueChangeKey.newKey] as? Bool {
//do something!
}
default:
break
}
}

Lots of hand waiving in here, for incomplete solutions. The observer on "loading" is not reliable because when it's switched to NO, the layout hasn't happened yet and you can't get an accurate reading on page size.
The injection of JS into the view to report page size can also be problematic depending on actual page content.
The WKWebView uses, obviously, a scroller (a "WKWebScroller" subclass to be exact). So, your best bet is to monitor that scroller's contentSize.
- (void) viewDidLoad {
//...super etc
[self.webKitView.scrollView addObserver: self
forKeyPath: #"contentSize"
options: NSKeyValueObservingOptionNew
context: nil];
}
- (void) dealloc
{
// remove observer
[self.webKitView.scrollView removeObserver: self
forKeyPath: #"contentSize"];
}
- (void) observeValueForKeyPath: (NSString*) keyPath
ofObject: (id) object
change: (NSDictionary<NSKeyValueChangeKey,id>*) change
context: (void*) context
{
if ([keyPath isEqualToString: #"contentSize"])
{
UIScrollView* scroller = (id) object;
CGSize scrollFrame = scroller.contentSize;
NSLog(#"scrollFrame = {%#,%#}",
#(scrollFrame.width), #(scrollFrame.height));
}
}
Watch out for the contentSize: it gets triggered A LOT. If your webView is embedded into another scrolling area (like if you're using it as a form element) then when you scroll your entire view, that subview webView will trigger the changes for the same values. So, make sure you dont resize needlessly or cause needless refreshes.
This solution tested on iOS 12 on iPad Air 2 sim off High Sierra XCode 10.

It's not a good choice to check if the page content loaded from swift or objective-c especially for very complex page with many dynamic content.
A better way to inform you ios code from webpage's javascript. This make it very flexible and effective, you can notify ios code when a dom or the page is loaded.
You can check my post here for further info.

Most of these answers likely won't give you the results you're looking for.
Let the html document tell you when it's loaded.
Here is how it is done.
script message handler delegate
#interface MyClass : UIView <WKScriptMessageHandler>
Initialize the WKView to handle whatever event you'd like (e.g. window.load)
WKWebView* webView = yourWebView;
NSString* jScript = #"window.addEventListener('load', function () { window.webkit.messageHandlers.loadEvent.postMessage('loaded');})";
WKUserScript *wkUScript = [[WKUserScript alloc] initWithSource:jScript injectionTime:WKUserScriptInjectionTimeAtDocumentStart forMainFrameOnly:YES];
[webView.configuration.userContentController addScriptMessageHandler:self name:#"loadEvent"];
[webView.configuration.userContentController addUserScript:wkUScript];
Handle the delegate message.
- (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message{
NSString* name = message.name;
if([name compare:#"loadEvent"] == 0)
{
}
}

You can inject JavaScript into the web view to either wait for onDOMContentLoaded or check the document.readyState state. I have an app that has been doing this for years, and it was the only reliable way to wait for the DOM to be populated. If you need all the images and other resources to be loaded, then you need to wait for the load event using JavaScript. Here are some docs:
https://developer.mozilla.org/en-US/docs/Web/API/Window/DOMContentLoaded_event
https://developer.mozilla.org/en-US/docs/Web/API/Window/load_event
https://developer.mozilla.org/en-US/docs/Web/API/Document/readyState

Related

Swift -AVPlayer' KVO in cell's parent vc causing Xcode to freeze

I have a cell that takes up the entire screen so there is only 1 visible cell at a time. Inside the cell I have an AVPlayer. Inside the cell's parent vc I have a KVO observer that listens to the "timeControlStatus". When the player stops playing I call a function stopVideo() inside the cell to stop the player and either show a replay button or a play button etc.
3 problems:
1- When the video stops/reaches end if I don't use DispatchQueue inside the KVO the app crashes when the cell's function is called.
2- When the video stops and I do use DispatchQueue inside the KVO it's observer keeps observing and Xcode freezes (no crash). There is a print statement inside the KVO that prints indefinitely and that's why Xcode freezes. The only way to stop it is to kill Xcode otherwise the beachball of death keeps spinning.
3- In the KVO I tried to use a notification to send to the cell in place of calling the cell.stopVideo() function but the function inside the cell never runs.
How can i fix this issue?
It should be noted that outside of the KVO not working everything else works fine. I have a periodic time observer that runs perfectly for every cell whenever I scroll, the videos load fine, and when I press the cell stop/play the video it all works fine.
cell:
protocol MyCellDelegate: class {
func sendBackPlayerAndIndexPath(_ player: AVPlayer?, currentIndexPath: IndexPath?)
}
var player: AVPlayer?
var indexPath: IndexPath?
var playerItem: AVPlayerItem? {
didSet {
// add playerItem to player
delegate?.sendBackPlayerAndIndexPath(player, indexPath)
}
}
override init(frame: CGRect) {
super.init(frame: frame)
player = AVPlayer()
// set everything else relating to the player
}
// both get initialized in cellForItem
var delegate: MyCellDelegate?
var myModel: MyModel? {
didSet {
let url = URL(string: myModel!.videUrlStr!)
asset = AVAsset(url: url)
playerItem = AVPlayerItem(asset: asset, automaticallyLoadedAssetKeys: ["playable"])
}
}
// tried using this with NotificationCenter but it didn't trigger from parent vc
#objc public func playVideo() {
if !player?.isPlaying {
player?.play()
}
// depending on certain conditions show a mute button, etc
}
// tried using this with NotificationCenter but it didn't trigger from parent vc
#objc public func stopVideo() {
player?.pause()
// depending on certain conditions show a reload button or a play button etc
}
parent vc
MyVC: ViewController, UICollectionViewDataSource, UICollectionViewDelegateFlowLayout {
var player: AVPlayer?
var currentIndexPath: IndexPath?
var isObserving = false
func sendBackPlayerAndIndexPath(_ player: AVPlayer?, currentIndexPath: IndexPath?) {
if isObserving {
self.player?.removeObserver(self, forKeyPath: "status", context: nil)
self.player?.removeObserver(self, forKeyPath: "timeControlStatus", context: nil)
}
guard let p = player, let i = currentIndexPath else { return }
self.player = p
self.currentIndexPath = i
isObserving = true
self.player?.addObserver(self, forKeyPath: "status", options: [.old, .new], context: nil)
self.player?.addObserver(self, forKeyPath: "timeControlStatus", options: [.old, .new], context: nil)
}
// If I don't use DispatchQueue below the app crashes
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
if object as AnyObject? === player {
if keyPath == "status" {
if player?.status == .readyToPlay {
DispatchQueue.main.async { [weak self] in
self?.playVideoInCell()
}
}
}
} else if keyPath == "timeControlStatus" {
if player?.timeControlStatus == .playing {
DispatchQueue.main.async { [weak self] in
self?.playVideoInCell()
}
} else {
print("3. Player is Not Playing *** ONCE STOPPED THIS PRINTS FOREVER and Xcode freezes but doesn't crash.\n")
DispatchQueue.main.async { [weak self] in
self?.stopVideoInCell()
}
}
}
}
}
func playVideoInCell() {
guard let indexPath = currentIndexPath else { return }
guard let cell = collectionView.cellForItem(at: indexPath) as? MyCell else { return }
cell.playVideo()
// also tried sending a NotificationCenter message to the cell but it didn't trigger
}
func stopVideoInCell() {
guard let indexPath = currentIndexPath else { return }
guard let cell = collectionView.cellForItem(at: indexPath) as? MyCell else { return }
cell.stopVideo()
// also tried sending a NotificationCenter message to the cell but it didn't trigger
}
In the comments #matt asked for the crash log (only occurs when not using DispatchQueue inside the KVO). I have zombies enabled and it didn't give me any info. The crash happens instantaneously and then just goes blank. It doesn't give me any info. I had to quickly take a screen shot just to get the pic otherwise it disappears right after the crash.
Inside the KVO, the 3rd one that prints forever, I removed the DispatchQueue and just added the stopVideoInCell() function. When the function calls the cell's stopVideo() function, player?.pause briefly gets a EXC_BAD_ACCESS (code=2, address=0x16b01ff0) crash.
The app then terminates. Inside the console the only thing that is printed is:
Message from debugger: The LLDB RPC server has crashed. The crash log
is located in ~/Library/Logs/DiagnosticReports and has a prefix
'lldb-rpc-server'. Please file a bug and attach the most recent crash
log
When I go to terminal and see what prints out the only thing I get is a bunch of lldb-rpc-server_2020-06-14-155514_myMacName.crash statements from all the days I ran into this crash.
When using DispatchQueue this doesn't occur and the video does stop but of course that print statement inside the KVO runs forever and Xcode freezes.
The problem is that, in your property observer, you are making a change in the property that you are observing. That is a vicious circle, an infinite recursion; Xcode displays this by freezing your app until ultimately you crash with (oh, the irony) a stack overflow.
Let's take a simpler, self-contained example. We have a UISwitch in the interface. It's On. If the user switches it Off, we want to detect that and switch it back to On. The example is a silly way to do this, but it perfectly illustrates the issue you are facing:
class ViewController: UIViewController {
#IBOutlet weak var theSwitch: UISwitch!
class SwitchHelper: NSObject {
#objc dynamic var switchState : Bool = true
}
let switchHelper = SwitchHelper()
var observer: NSKeyValueObservation!
override func viewDidLoad() {
super.viewDidLoad()
self.observer = self.switchHelper.observe(\.switchState, options: .new) {
helper, change in
self.theSwitch.isOn.toggle()
self.theSwitch.sendActions(for: .valueChanged)
}
}
#IBAction func doSwitch(_ sender: Any) {
self.switchHelper.switchState = (sender as! UISwitch).isOn
}
}
What's going to happen? The user turns the switch Off. We are observing that, as switchState; in response, we switch the switch back to On and call sendActions. And sendActions changes switchState. But we are still in the middle of the code that observes switchState! So we do it again and it happens again. And again and it happens again. Infinite loop...
How would you get out of this? You need to break the recursion somehow. I can think of two obvious ways. One is to think to yourself, "Well, I only care about a switch from On to Off. I don't care about the other way." Assuming that's true, you can solve the problem with a simple if, rather like the solution you elected to use:
self.observer = self.switchHelper.observe(\.switchState, options: .new) {
helper, change in
if let val = change.newValue, !val {
self.theSwitch.isOn.toggle()
self.theSwitch.sendActions(for: .valueChanged)
}
}
A more elaborate solution, which I like to use sometimes, is to stop observing when the observer is triggered, make whatever the change is, and then start observing again. You have to plan ahead a little to implement that, but it's sometimes worth it:
var observer: NSKeyValueObservation!
func startObserving() {
self.observer = self.switchHelper.observe(\.switchState, options: .new) {
helper, change in
self.observer?.invalidate()
self.observer = nil
self.theSwitch.isOn.toggle()
self.theSwitch.sendActions(for: .valueChanged)
self.startObserving()
}
}
override func viewDidLoad() {
super.viewDidLoad()
self.startObserving()
}
That looks recursive, because startObserving calls itself, but it isn't really, because what it does when it is called is to configure the observation; the code in the inner curly braces doesn't run until we get an observed change.
(In real life, I would probably make the NSKeyValueObservation a local variable in that configuration. But that's just an additional bit of elegance, not essential to the point of the example.)
I resolved this using a Boolean. It's not the most elegant answer but it works. If someone can come up with a better answer I'll accept it. It doesn't make sense what's going because of what I put under more:
answer:
var isPlayerStopped = false
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
if object as AnyObject? === player {
if keyPath == "status" {
if player?.status == .readyToPlay {
DispatchQueue.main.async { [weak self] in
self?.playVideoInCell()
}
}
}
} else if keyPath == "timeControlStatus" {
if player?.timeControlStatus == .playing {
DispatchQueue.main.async { [weak self] in
self?.playVideoInCell()
}
} else {
if isPlayerStopped { return }
print("3. Player is Not Playing *** NOW THIS ONLY PRINTS ONCE.\n")
DispatchQueue.main.async { [weak self] in
self?.stopVideoInCell()
}
}
}
}
}
func playVideoInCell() {
guard let indexPath = currentIndexPath else { return }
guard let cell = collectionView.cellForItem(at: indexPath) as? MyCell else { return }
isPlayerStopped = false
cell.playVideo()
}
func stopVideoInCell() {
guard let indexPath = currentIndexPath else { return }
guard let cell = collectionView.cellForItem(at: indexPath) as? MyCell else { return }
isPlayerStopped = true
cell.stopVideo()
}
more:
If I completely remove the DispatchQueues and the functions inside of them and use just print statements, the print statement that prints indefinitely print("3. Player is Not Playing... \n") only prints twice, it no longer prints indefinitely so I don't know what's going on with this.
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
if object as AnyObject? === player {
if keyPath == "status" {
if player?.status == .readyToPlay {
print("1. Player is Playing\n")
}
} else if keyPath == "timeControlStatus" {
if player?.timeControlStatus == .playing {
print("2. Player is Playing\n")
} else {
print("3. Player is Not Playing... \n")
}
}
}
}

WKWebView decidePolicyFor gets called after page loaded

I try to catch the url about to load in WKWebView before it loads. Based on documents decidePolicyFor navigationAction (WKNavigationDelegate) should do the job but my problem is this delegate gets called after new url get loaded not before that.
here is the extension I wrote.
extension MyWebViewController: WKNavigationDelegate {
public func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: #escaping (WKNavigationActionPolicy) -> Void) {
guard let navigationURL = navigationAction.request.url else {
decisionHandler(.allow)
return
}
let forbiddenUrlPattern = Configuration.current.links.forbiddenUrlPattern
if forbiddenUrlPattern.matches(url: navigationURL) {
decisionHandler(.cancel)
showFullScreenError(error: .forbidden)
return
}
// Default policy is to allow navigation to any links the subclass doesn't know about
decisionHandler(.allow)
}
}
*PS the matches extension checks the pattern and it works fine.
now the problem is that the content of forbiddenUrl showed for a time before this delegate func gets called and then the error page comes to screen, and if I close it the last visible webPage is from forbidden link pattern.
is there any way to understand about the link before actually loading it in webView?
I am using Xcode 11.2.1 & Swift 5.0
I wrote the answer I found here that it may help someone else as well.
After lots of struggle I found out decisionHandler wont get called if the url is relative (not absolute urls).
So why decisionHandler got called after loading that page?
the answer I found is: when we have urls like href:"/foo/ba" then after calling that url, it will load and resolve as www.domain.com/foo/ba and only then desicionHandler got called.
Also didCommit only called once when I wanted to load the url for the first time in webView.
so the solution helped me was to add an observer to webView
webView.addObserver(self, forKeyPath: "URL", options: [.new,.old], context: nil)
and
override open func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
/// This observer is in addition to the navigationAction delegate to block relative urls and intrrupt them and do native
/// action if possible.
if let newValue = change?[.newKey] as? URL, let oldValue = change?[.oldKey] as? URL, newValue != oldValue {
let forbiddenUrlPattern = Configuration.current.links.forbiddenUrlPattern
if forbiddenUrlPattern.matches(url: newValue) {
showFullScreenError(error: .forbidden)
return
}
/// These two action needed to cancel the webView loading the offerDetail page.
/// otherwise as we stop loading the about to start process webView will show blank page.
webView.stopLoading()
///This is small extension for make webView go one step back
webView.handleBackAction(sender: newValue)
return
}
}
}
So this observer in addition to decisionHandler would cover both absolute and relative urls any one wants to listen and take action if needed.

Autoresize WKWebView to content height [duplicate]

I am experimenting with replacing a dynamically allocated instance of UIWebView with a WKWebView instance when running under iOS 8 and newer, and I cannot find a way to determine the content size of a WKWebView.
My web view is embedded within a larger UIScrollView container, and therefore I need to determine the ideal size for the web view. This will allow me to modify its frame to show all of its HTML content without the need to scroll within the web view, and I will be able to set the correct height for the scroll view container (by setting scrollview.contentSize).
I have tried sizeToFit and sizeThatFits without success. Here is my code that creates a WKWebView instance and adds it to the 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];
Here is an experimental didFinishNavigation method:
- (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));
}
And here is the output that is generated:
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}
The sizeToFit call has no effect and sizeThatFits always returns a height of 1.
I think I read every answer on this subject and all I had was part of the solution. Most of the time I spent trying to implement KVO method as described by #davew, which occasionally worked, but most of the time left a white space under the content of a WKWebView container. I also implemented #David Beck suggestion and made the container height to be 0 thus avoiding the possibility that the problem occurs if the container height is larger that that of the content. In spite of that I had that occasional blank space.
So, for me, "contentSize" observer had a lot of flaws. I do not have a lot of experience with web technologies so I cannot answer what was the problem with this solution, but i saw that if I only print height in the console but do not do anything with it (eg. resize the constraints), it jumps to some number (e.g. 5000) and than goes to the number before that highest one (e.g. 2500 - which turns out to be the correct one). If I do set the height constraint to the height which I get from "contentSize" it sets itself to the highest number it gets and never gets resized to the correct one - which is, again, mentioned by #David Beck comment.
After lots of experiments I've managed to find a solution that works for me:
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
})
}
})
}
Of course, it is important to set the constraints correctly so that scrollView resizes according to the containerHeight constraint.
As it turns out didFinish navigation method never gets called when I wanted, but having set document.readyState step, the next one (document.body.offsetHeight) gets called at the right moment, returning me the right number for height.
You could use Key-Value Observing (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);
}
}
This would save the use of JavaScript and keep you in the loop on all changes.
I had to deal with this issue myself recently. In the end, I was using a modification of the solution proposed by Chris McClenaghan.
Actually, his original solution is pretty good and it works in most simple cases. However, it only worked for me on pages with text. It probably also works on pages with images that have a static height. However, it definitely doesn't work when you have images whose size is defined with max-height and max-width attributes.
And this is because those elements can get resized after the page is loaded. So, actually, the height returned in onLoad will always be correct. But it will only be correct for that particular instance. The workaround is to monitor the change of the body height and respond to it.
Monitor resizing of the 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)
}
}
}
This solution is by far the most elegant that I could come up with. There are, however, two things you should be aware of.
Firstly, before loading your URL you should set shouldListenToResizeNotification to false. This extra logic is needed for cases when the loaded URL can change rapidly. When this occurs, notifications from old content for some reason can overlap with those from the new content. To prevent such behaviour, I created this variable. It ensures that once we start loading new content we no longer process notification from the old one and we only resume processing of resize notifications after new content is loaded.
Most importantly, however, you need to be aware about this:
If you adopt this solution you need to take into account that if you change the size of your WKWebView to anything other than the size reported by the notification - the notification will be triggered again.
Be careful with this as it is easy to enter an infinite loop. For example, if you decide to handle the notification by making your height equal to reported height + some extra 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)
}
As you can see, because I am adding 8 to the reported height, after this is done the size of my body will change and the notification will be posted again.
Be alert to such situations and otherwise you should be fine.
And please let me know if you discover any problems with this solution - I am relying on it myself so it is best to know if there are some faults which I haven't spotted!
Works for me
extension TransactionDetailViewController: WKNavigationDelegate {
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
self.webviewHeightConstraint.constant = webView.scrollView.contentSize.height
}
}
}
Try the following. Wherever you instantiate your WKWebView instance, add something similar to the following:
//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]];
Then add a method similar to the following to which ever class obeys WKScriptMessageHandler protocol to handle the 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;}
This works for me.
If you have more than text in your document you may need to wrap the javascript like this to ensure everything is loaded:
#"window.onload=function () { window.webkit.messageHandlers.sizeNotification.postMessage({width: document.width, height: document.height});};"
NOTE: This solution does not address ongoing updates to the document.
Most answers are using "document.body.offsetHeight".
This hides the last object of the body.
I overcame this issue by using a KVO observer listening for changes in WKWebview "contentSize", then running this 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
}
)
It's not the prettiest code possible, but it worked for me.
You can also got content height of WKWebView by evaluateJavaScript.
- (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
}
}];
}
You need to wait for the webview to finish loading. Here is a working example I used
WKWebView Content loaded function never get called
Then after webview has finished loading, then you can determine the heights you need by
func webView(webView: WKWebView!, didFinishNavigation navigation: WKNavigation!) {
println(webView.scrollView.contentSize.height)
}
I found that the answer by hlung here, extending the WKWebView as follows was the simplest and most effective solution for me:
https://gist.github.com/pkuecuekyan/f70096218a6b969e0249427a7d324f91
His comment follows:
"Nice! For me, instead of setting the webView.frame, I set autolayout intrinsicContentSize."
And his code was as follows:
import UIKit
import WebKit
class ArticleWebView: WKWebView {
init(frame: CGRect) {
let configuration = WKWebViewConfiguration()
super.init(frame: frame, configuration: configuration)
self.navigationDelegate = self
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override var intrinsicContentSize: CGSize {
return self.scrollView.contentSize
}
}
extension ArticleWebView: WKNavigationDelegate {
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
webView.evaluateJavaScript("document.readyState", completionHandler: { (_, _) in
webView.invalidateIntrinsicContentSize()
})
}
}
This is a slight edit of #IvanMih's answer. For those of you experiencing a large white space at the end of your WKWebview this solution worked well for me:
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
webView.evaluateJavaScript("document.readyState", completionHandler: { (complete, error) in
if complete != nil {
let height = webView.scrollView.contentSize
print("height of webView is: \(height)")
}
})
}
so basically instead of calculating the height based on scrollHeight you calculate height using webView.scrollView.contentSize. I'm sure there are scenarios where this will break, but I think it'll do pretty well for static content and if you are displaying all the content without the user having to scroll.
After lots of experiments I've managed to find a solution that works for me I found to make a webview heigh dynamic without using evaluating javascript and also without taking height constant from webview this work with me like a charm and also work when I inject new style to HTML and play with font sizes and heights
code in Swift
1- give your Webview navigation delegate
webView.navigationDelegate = self
2- in delegation extension
extension yourclass : WKNavigationDelegate {
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
// Handel Dynamic Height For Webview Loads with HTML
// Most important to reset webview height to any desired height i prefer 1 or 0
webView.frame.size.height = 1
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
// here get height constant and assign new height in it
if let constraint = (webView.constraints.filter{$0.firstAttribute == .height}.first) {
constraint.constant = webView.scrollView.contentSize.height
}
}
hope it works also with you guys
** note this not my entire effort I searched a lot in StackOverflow and other sites and this is what finally works with me with a lot of testing also
using #Andriy's answer and this answer i was able to set get height of contentSize in WKWebView and change it's height.
here is full swift 4 code:
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)
}
}
I've tried the scroll view KVO and I've tried evaluating javascript on the document, using clientHeight, offsetHeight, etc...
What worked for me eventually is: document.body.scrollHeight. Or use the scrollHeight of your top most element, e.g. a container div.
I listen to the loading WKWebview property changes using KVO:
[webview addObserver: self forKeyPath: NSStringFromSelector(#selector(loading)) options: NSKeyValueObservingOptionNew context: nil];
And then:
- (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];
}
}
}
The updateWebviewFrame implementation:
[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;
}];
I tried Javascript version in UITableViewCell, and it works perfectly. However, if you want to put it in the scrollView. I don't know why, the height can be higher but cannot be shorter. However, I found a UIWebView solution here. https://stackoverflow.com/a/48887971/5514452
It also works in WKWebView. I think the problem is because the WebView need relayout, but somehow it will not shrink and can only enlarge. We need to reset the height and it will definitely resize.
Edit: I reset the frame height after setting the constraint because sometime it will not working due to setting the frame height to 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
})
}
})
}
Also tried to implement different methods and finally came to a solution. As a result I made a self-sizing WKWebView, that adapts its intrinsicContentSize to the size of its contents. So you can use it in Auto Layouts. As an example I made a view, which might help you display math formula on iOS apps: https://github.com/Mazorati/SVLatexView
The following code has worked perfectly for me, for any content in the webkit. Make sure to add the following delegate to your class: WKNavigationDelegate.
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
self.bodyWebView.evaluateJavaScript("document.readyState", completionHandler: { (complete, error) in
if complete != nil {
self.bodyWebView.evaluateJavaScript("document.body.scrollHeight", completionHandler: { (height, error) in
let heightWebView = height as! CGFloat
//heightWebView is the height of the web view
})
}
})
}
}
The dispatch is important, because this way you ensure that the height obtained at the end of loading the web view is correct, this happens because of the type of elements that the html may have.
I want to contribute with the solution for a special case that is not mentioned in the answers above and that may happen to you if you are using custom fonts in your WKWebView.
I tried ALL the solutions explained here, and many others mentioned in other StackOverflow questions. Nothing was working 100% correctly for me. I had always the same problem: the height returned was always a little smaller than the real height of the WkWebView. I tried WKNavigationDelegate way, and I tried to listen to self-generated events by injecting js into the HTML rendered, without success, the height was always wrong in all cases.
The first thing I learned: the webview has to be added to the layout before loading the html and waiting for the finished event. If you try to render the webview in an isolated way without adding it before to the layout, then the height will be very wrong.
Curiously, I found out that setting a breakpoint after the html was rendered, and before calling the height evaluation method, then the returned height was correct. It was not important which height was measured (scrollHeight or offsetheight), both were always correct.
That pointed me in the right direction. The conclusion was obvious (although I needed a lot of days making debug to realize it): after the didFinishNavigation event is received, or if you are using custom js and listening to the window.onload event or similar, the height returned is almost correct but not completely because the rendering is not finished yet.
As explained here, Firefox, Chrome, and Safari trigger the DomContenLoaded event before the font-face is applied to the document (and maybe, before the css is applied to the document too?). In my case, I was using a custom font embedded in my app and referenced in the HTML in the classical way:
<style>
#font-face {
font-family: 'CustomFont';
src: url('montserrat.ttf');
format('truetype');
}
body{
font-family: 'CustomFont';
font-size: 12px;
}
Solution? You have to listen to the event document.fonts.ready, that happens after the event window.onload and the like. Embeed the following js in the html you are loading in the WkWebView:
document.fonts.ready.then(function() {
window.webkit.messageHandlers.iosEventListener.postMessage('custom_event_fonts_ready');
});
Then in your iOS app, listen to the event with
self.webView.configuration.userContentController.add(self, name: "iosEventListener")
and when received
public func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
if let body = message.body as? String {
if (body == "custom_event_fonts_ready") {
self.evaluateBodyHeight()
}
}
}
private func evaluateBodyHeight() {
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
//Do something with the height.
})
}
})
}
I'm not sure, but I think that with this solution, all the different ways to measure the height of web view will return the correct one. After almost one month of debugging and being desperate, I have no desire to test them
Apologizes for my bad English.
The best way is to observe contentSize property of webView.scrollView and update height constraint of webView accordingly:
private var contentSizeObserver: NSKeyValueObservation?
contentSizeObserver = webView.scrollView.observe(\.contentSize, options: .new) { [weak self] _, change in
guard let contentSize = change.newValue else { return }
self?.csWebViewHeight?.update(offset: contentSize.height)
}
override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
super.viewWillTransition(to: size, with: coordinator)
// Recalculate webView size
csWebViewHeight?.update(offset: 0)
webView.setNeedsLayout()
webView.layoutIfNeeded()
}
None of the listed JS funcs to get the content height worked reliably for me. What I found to consistently work is to find the last element in the DOM and get its position explicitly:
webView.evaluateJavaScript(
"document.body.lastChild.getBoundingClientRect().bottom + window.scrollY"
) { [weak self] (result, _) in
guard let self = self,
let height = result as? CGFloat,
height > 0 else { return }
self.heightConstraint?.constant = height
}

Done button click event in AVPlayerViewController

I want to play local video in AVPlayerViewController but did not find click event of Done button.
My video is able to play in AVPlayerViewController but I did not find next button , previous button and done button click event.
I've done this to get Done button click event from AVPlayerViewController.
First of all, Create an extension of Notification.Name like bellow
extension Notification.Name {
static let kAVPlayerViewControllerDismissingNotification = Notification.Name.init("dismissing")
}
Now, Create an extension of AVPlayerViewController and override viewWillDisappear like bellow
// create an extension of AVPlayerViewController
extension AVPlayerViewController {
// override 'viewWillDisappear'
open override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
// now, check that this ViewController is dismissing
if self.isBeingDismissed == false {
return
}
// and then , post a simple notification and observe & handle it, where & when you need to.....
NotificationCenter.default.post(name: .kAVPlayerViewControllerDismissingNotification, object: nil)
}
}
THIS IS FOR ONLY BASIC FUNCTIONALITY THAT HANDLES DONE BUTTON'S EVENT.
happy coding...
Officially there is no Done Button click event, a engineer from Apple said about it here.
As for my research, I found out that there is one but really indirect way to get an event if Done Button was clicked.
I found out that the only variable of AVPlayerViewController, which is changing when done button is clicked is AVPlayerViewController.view.frame.
First of all view.frame is appearing in the center of the viewController.
If you present it with animated : true it goes to the bottom of viewController and the back to the center. When done is clicked it goes back to the bottom.
If you present it with animated : false there will be only 2 changes: frame will be at the center of viewController when you start to play video, and at the bottom, when Done is clicked.
So if you add observer to the AVPlayerViewController.view.frame in the callback to present(PlayerViewController, animated : true) you'll get only one call of the observer, right when done button is clicked and video view will be out of the screen.
In my case AVPlayerViewController was presented modally with animation. Code below worked for me:
Swift 3.0
override func viewDidLoad()
{
super.viewDidLoad()
let videoURL = NSURL(fileURLWithPath:Bundle.main.path(forResource: "MyVideo", ofType:"mp4")!)
let player = AVPlayer(url: videoURL as URL)
present(playerViewController, animated: false) { () -> Void in
player.play()
self.playerViewController.player = player
self.playerViewController.addObserver(self, forKeyPath: #keyPath(UIViewController.view.frame), options: [.old, .new], context: nil)
}}
override func observeValue(forKeyPath keyPath: String?,
of object: Any?,
change: [NSKeyValueChangeKey : Any]?,
context: UnsafeMutableRawPointer?)
{
print(self.playerViewController.view.frame)
//Player view is out of the screen and
//You can do your custom actions
}
Also, I found out when you click Done, AVPlayerViewController is not dismissed and you can see it in ParentViewController.presentedViewController, so you can't add observer to this property
Objective c version: (Based on vmchar answer)
- (void)viewDidLoad
{
[super viewDidLoad];
NSURL *url = [NSURL fileURLWithPath:path];
AVPlayer *player = [AVPlayer playerWithURL:url];
AVPlayerViewController *AVPlayerVc = [[AVPlayerViewController alloc] init];
if (AVPlayerVc) {
[self presentViewController:AVPlayerVc animated:YES completion:^{
[player play];
AVPlayerVc.player = player;
[AVPlayerVc addObserver:self forKeyPath:#"view.frame" options:(NSKeyValueObservingOptionNew | NSKeyValueObservingOptionInitial) context:nil];
}];
}
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
if ([keyPath isEqualToString:#"view.frame"]) {
CGRect newValue = [change[NSKeyValueChangeNewKey]CGRectValue];
CGFloat y = newValue.origin.y;
if (y != 0) {
NSLog(#"Video Closed");
}
}
}
This is a small improvement from the answer by #vmchar:
We can use the .isBeingDismissed method to ensure that the AVPlayerViewController is being closed instead of analysing the frame.
...
let videoURL = NSURL(fileURLWithPath:"video.mp4")
let player = AVPlayer(url: videoURL as URL)
present(playerViewController!, animated: false) { () -> Void in
player.play()
self.playerViewController!.player = player
self.playerViewController!.addObserver(self, forKeyPath:#keyPath(UIViewController.view.frame), options: [.old, .new], context: nil)
...
Then to observe the value
override func observeValue(forKeyPath keyPath: String?,
of object: Any?,
change: [NSKeyValueChangeKey : Any]?,
context: UnsafeMutableRawPointer?)
{
if (playerViewController!.isBeingDismissed) {
// Video was dismissed -> apply logic here
}
}
The easiest solution for me was to subclass AVPlayerViewController and add simple completion block to it.
class MYVideoController: AVPlayerViewController {
typealias DissmissBlock = () -> Void
var onDismiss: DissmissBlock?
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
if isBeingDismissed {
onDismiss?()
}
}
}
Usage
...
let avPlayerViewController = MYVideoController()
avPlayerViewController.onDismiss = { [weak self] in
print("dismiss")
}
I found a workaround without having to subclass the AVPlayerViewController, which the documentation clearly states you should not (https://developer.apple.com/documentation/avkit/avplayerviewcontroller). It is not pretty, but it got the job done for me by giving me somewhere to run code when the AVPlayerViewController is dismissed.
AnyViewController: UIViewController, AVPlayerViewControllerDelegate {
private let playerVC = AVPlayerViewController()
override func viewDidLoad() {
playerVC.delegate = self
}
func playerViewController(_ playerViewController: AVPlayerViewController, willEndFullScreenPresentationWithAnimationCoordinator coordinator: UIViewControllerTransitionCoordinator) {
if playerViewController.isBeingDismissed {
playerViewController.dismiss(animated: false) { [weak self] in
self?.someDismissFunc()
}
}
}
}
It is really messed up that there is no clean way of doing this as far as I can tell :(
I had the same problem, and I found another trick (works with YoutubePlayer swift framework which plays videos in a webview, but you might be able to adapt it to other uses).
In my case, pressing the Done button and pressing the Pause button on the fullscreen video player both results in a pause event on the YoutubePlayer. However, as the video plays in a different window, I subclassed my application's main window and overrode the becomeKey and resignKey functions to store whether my window is the key window or not, like that:
class MyWindow:UIWindow {
override func becomeKey() {
Services.appData.myWindowIsKey = true
}
override func resignKey() {
Services.appData.myWindowIsKey = false
}
}
Once I have that, I can check whether my window is key when the video state goes to pause - when the Done button was pressed, my window is the key window and I can dismiss my video view controller, and in the Pause case my window is not the key window, so I do nothing.
I guess there are lots of ways to skin a cat. I needed to handle the 'Done' click event. In my case, I wanted to make sure I was able to hook into the event after the "X" was clicked and the AVPlayerViewController was COMPLETELY closed (dismissed).
Swift 4.0
protocol TableViewCellVideoPlayerViewControllerDelegate: class {
func viewControllerDismissed()
}
class MyPlayerViewController: AVPlayerViewController {
weak var navDelegate: TableViewCellVideoPlayerViewControllerDelegate?
open override func viewDidDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
if self.isBeingDismissed {
self.navDelegate?.viewControllerDismissed()
}
}
}
class TodayCell: UITableViewCell, SFSafariViewControllerDelegate, TableViewCellVideoPlayerViewControllerDelegate {
func viewControllerDismissed() {
self.continueJourney()
}
}
You can look into this Stackoverflow post and here is a github's project for reference. You will have to use this :
self.showsPlaybackControls = true
Please also have a look into Apple's documentation

How to determine the content size of a WKWebView?

I am experimenting with replacing a dynamically allocated instance of UIWebView with a WKWebView instance when running under iOS 8 and newer, and I cannot find a way to determine the content size of a WKWebView.
My web view is embedded within a larger UIScrollView container, and therefore I need to determine the ideal size for the web view. This will allow me to modify its frame to show all of its HTML content without the need to scroll within the web view, and I will be able to set the correct height for the scroll view container (by setting scrollview.contentSize).
I have tried sizeToFit and sizeThatFits without success. Here is my code that creates a WKWebView instance and adds it to the 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];
Here is an experimental didFinishNavigation method:
- (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));
}
And here is the output that is generated:
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}
The sizeToFit call has no effect and sizeThatFits always returns a height of 1.
I think I read every answer on this subject and all I had was part of the solution. Most of the time I spent trying to implement KVO method as described by #davew, which occasionally worked, but most of the time left a white space under the content of a WKWebView container. I also implemented #David Beck suggestion and made the container height to be 0 thus avoiding the possibility that the problem occurs if the container height is larger that that of the content. In spite of that I had that occasional blank space.
So, for me, "contentSize" observer had a lot of flaws. I do not have a lot of experience with web technologies so I cannot answer what was the problem with this solution, but i saw that if I only print height in the console but do not do anything with it (eg. resize the constraints), it jumps to some number (e.g. 5000) and than goes to the number before that highest one (e.g. 2500 - which turns out to be the correct one). If I do set the height constraint to the height which I get from "contentSize" it sets itself to the highest number it gets and never gets resized to the correct one - which is, again, mentioned by #David Beck comment.
After lots of experiments I've managed to find a solution that works for me:
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
})
}
})
}
Of course, it is important to set the constraints correctly so that scrollView resizes according to the containerHeight constraint.
As it turns out didFinish navigation method never gets called when I wanted, but having set document.readyState step, the next one (document.body.offsetHeight) gets called at the right moment, returning me the right number for height.
You could use Key-Value Observing (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);
}
}
This would save the use of JavaScript and keep you in the loop on all changes.
I had to deal with this issue myself recently. In the end, I was using a modification of the solution proposed by Chris McClenaghan.
Actually, his original solution is pretty good and it works in most simple cases. However, it only worked for me on pages with text. It probably also works on pages with images that have a static height. However, it definitely doesn't work when you have images whose size is defined with max-height and max-width attributes.
And this is because those elements can get resized after the page is loaded. So, actually, the height returned in onLoad will always be correct. But it will only be correct for that particular instance. The workaround is to monitor the change of the body height and respond to it.
Monitor resizing of the 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)
}
}
}
This solution is by far the most elegant that I could come up with. There are, however, two things you should be aware of.
Firstly, before loading your URL you should set shouldListenToResizeNotification to false. This extra logic is needed for cases when the loaded URL can change rapidly. When this occurs, notifications from old content for some reason can overlap with those from the new content. To prevent such behaviour, I created this variable. It ensures that once we start loading new content we no longer process notification from the old one and we only resume processing of resize notifications after new content is loaded.
Most importantly, however, you need to be aware about this:
If you adopt this solution you need to take into account that if you change the size of your WKWebView to anything other than the size reported by the notification - the notification will be triggered again.
Be careful with this as it is easy to enter an infinite loop. For example, if you decide to handle the notification by making your height equal to reported height + some extra 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)
}
As you can see, because I am adding 8 to the reported height, after this is done the size of my body will change and the notification will be posted again.
Be alert to such situations and otherwise you should be fine.
And please let me know if you discover any problems with this solution - I am relying on it myself so it is best to know if there are some faults which I haven't spotted!
Works for me
extension TransactionDetailViewController: WKNavigationDelegate {
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
self.webviewHeightConstraint.constant = webView.scrollView.contentSize.height
}
}
}
Try the following. Wherever you instantiate your WKWebView instance, add something similar to the following:
//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]];
Then add a method similar to the following to which ever class obeys WKScriptMessageHandler protocol to handle the 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;}
This works for me.
If you have more than text in your document you may need to wrap the javascript like this to ensure everything is loaded:
#"window.onload=function () { window.webkit.messageHandlers.sizeNotification.postMessage({width: document.width, height: document.height});};"
NOTE: This solution does not address ongoing updates to the document.
Most answers are using "document.body.offsetHeight".
This hides the last object of the body.
I overcame this issue by using a KVO observer listening for changes in WKWebview "contentSize", then running this 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
}
)
It's not the prettiest code possible, but it worked for me.
You can also got content height of WKWebView by evaluateJavaScript.
- (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
}
}];
}
You need to wait for the webview to finish loading. Here is a working example I used
WKWebView Content loaded function never get called
Then after webview has finished loading, then you can determine the heights you need by
func webView(webView: WKWebView!, didFinishNavigation navigation: WKNavigation!) {
println(webView.scrollView.contentSize.height)
}
I found that the answer by hlung here, extending the WKWebView as follows was the simplest and most effective solution for me:
https://gist.github.com/pkuecuekyan/f70096218a6b969e0249427a7d324f91
His comment follows:
"Nice! For me, instead of setting the webView.frame, I set autolayout intrinsicContentSize."
And his code was as follows:
import UIKit
import WebKit
class ArticleWebView: WKWebView {
init(frame: CGRect) {
let configuration = WKWebViewConfiguration()
super.init(frame: frame, configuration: configuration)
self.navigationDelegate = self
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override var intrinsicContentSize: CGSize {
return self.scrollView.contentSize
}
}
extension ArticleWebView: WKNavigationDelegate {
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
webView.evaluateJavaScript("document.readyState", completionHandler: { (_, _) in
webView.invalidateIntrinsicContentSize()
})
}
}
This is a slight edit of #IvanMih's answer. For those of you experiencing a large white space at the end of your WKWebview this solution worked well for me:
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
webView.evaluateJavaScript("document.readyState", completionHandler: { (complete, error) in
if complete != nil {
let height = webView.scrollView.contentSize
print("height of webView is: \(height)")
}
})
}
so basically instead of calculating the height based on scrollHeight you calculate height using webView.scrollView.contentSize. I'm sure there are scenarios where this will break, but I think it'll do pretty well for static content and if you are displaying all the content without the user having to scroll.
After lots of experiments I've managed to find a solution that works for me I found to make a webview heigh dynamic without using evaluating javascript and also without taking height constant from webview this work with me like a charm and also work when I inject new style to HTML and play with font sizes and heights
code in Swift
1- give your Webview navigation delegate
webView.navigationDelegate = self
2- in delegation extension
extension yourclass : WKNavigationDelegate {
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
// Handel Dynamic Height For Webview Loads with HTML
// Most important to reset webview height to any desired height i prefer 1 or 0
webView.frame.size.height = 1
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
// here get height constant and assign new height in it
if let constraint = (webView.constraints.filter{$0.firstAttribute == .height}.first) {
constraint.constant = webView.scrollView.contentSize.height
}
}
hope it works also with you guys
** note this not my entire effort I searched a lot in StackOverflow and other sites and this is what finally works with me with a lot of testing also
using #Andriy's answer and this answer i was able to set get height of contentSize in WKWebView and change it's height.
here is full swift 4 code:
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)
}
}
I've tried the scroll view KVO and I've tried evaluating javascript on the document, using clientHeight, offsetHeight, etc...
What worked for me eventually is: document.body.scrollHeight. Or use the scrollHeight of your top most element, e.g. a container div.
I listen to the loading WKWebview property changes using KVO:
[webview addObserver: self forKeyPath: NSStringFromSelector(#selector(loading)) options: NSKeyValueObservingOptionNew context: nil];
And then:
- (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];
}
}
}
The updateWebviewFrame implementation:
[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;
}];
I tried Javascript version in UITableViewCell, and it works perfectly. However, if you want to put it in the scrollView. I don't know why, the height can be higher but cannot be shorter. However, I found a UIWebView solution here. https://stackoverflow.com/a/48887971/5514452
It also works in WKWebView. I think the problem is because the WebView need relayout, but somehow it will not shrink and can only enlarge. We need to reset the height and it will definitely resize.
Edit: I reset the frame height after setting the constraint because sometime it will not working due to setting the frame height to 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
})
}
})
}
Also tried to implement different methods and finally came to a solution. As a result I made a self-sizing WKWebView, that adapts its intrinsicContentSize to the size of its contents. So you can use it in Auto Layouts. As an example I made a view, which might help you display math formula on iOS apps: https://github.com/Mazorati/SVLatexView
The following code has worked perfectly for me, for any content in the webkit. Make sure to add the following delegate to your class: WKNavigationDelegate.
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
self.bodyWebView.evaluateJavaScript("document.readyState", completionHandler: { (complete, error) in
if complete != nil {
self.bodyWebView.evaluateJavaScript("document.body.scrollHeight", completionHandler: { (height, error) in
let heightWebView = height as! CGFloat
//heightWebView is the height of the web view
})
}
})
}
}
The dispatch is important, because this way you ensure that the height obtained at the end of loading the web view is correct, this happens because of the type of elements that the html may have.
I want to contribute with the solution for a special case that is not mentioned in the answers above and that may happen to you if you are using custom fonts in your WKWebView.
I tried ALL the solutions explained here, and many others mentioned in other StackOverflow questions. Nothing was working 100% correctly for me. I had always the same problem: the height returned was always a little smaller than the real height of the WkWebView. I tried WKNavigationDelegate way, and I tried to listen to self-generated events by injecting js into the HTML rendered, without success, the height was always wrong in all cases.
The first thing I learned: the webview has to be added to the layout before loading the html and waiting for the finished event. If you try to render the webview in an isolated way without adding it before to the layout, then the height will be very wrong.
Curiously, I found out that setting a breakpoint after the html was rendered, and before calling the height evaluation method, then the returned height was correct. It was not important which height was measured (scrollHeight or offsetheight), both were always correct.
That pointed me in the right direction. The conclusion was obvious (although I needed a lot of days making debug to realize it): after the didFinishNavigation event is received, or if you are using custom js and listening to the window.onload event or similar, the height returned is almost correct but not completely because the rendering is not finished yet.
As explained here, Firefox, Chrome, and Safari trigger the DomContenLoaded event before the font-face is applied to the document (and maybe, before the css is applied to the document too?). In my case, I was using a custom font embedded in my app and referenced in the HTML in the classical way:
<style>
#font-face {
font-family: 'CustomFont';
src: url('montserrat.ttf');
format('truetype');
}
body{
font-family: 'CustomFont';
font-size: 12px;
}
Solution? You have to listen to the event document.fonts.ready, that happens after the event window.onload and the like. Embeed the following js in the html you are loading in the WkWebView:
document.fonts.ready.then(function() {
window.webkit.messageHandlers.iosEventListener.postMessage('custom_event_fonts_ready');
});
Then in your iOS app, listen to the event with
self.webView.configuration.userContentController.add(self, name: "iosEventListener")
and when received
public func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
if let body = message.body as? String {
if (body == "custom_event_fonts_ready") {
self.evaluateBodyHeight()
}
}
}
private func evaluateBodyHeight() {
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
//Do something with the height.
})
}
})
}
I'm not sure, but I think that with this solution, all the different ways to measure the height of web view will return the correct one. After almost one month of debugging and being desperate, I have no desire to test them
Apologizes for my bad English.
The best way is to observe contentSize property of webView.scrollView and update height constraint of webView accordingly:
private var contentSizeObserver: NSKeyValueObservation?
contentSizeObserver = webView.scrollView.observe(\.contentSize, options: .new) { [weak self] _, change in
guard let contentSize = change.newValue else { return }
self?.csWebViewHeight?.update(offset: contentSize.height)
}
override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
super.viewWillTransition(to: size, with: coordinator)
// Recalculate webView size
csWebViewHeight?.update(offset: 0)
webView.setNeedsLayout()
webView.layoutIfNeeded()
}
None of the listed JS funcs to get the content height worked reliably for me. What I found to consistently work is to find the last element in the DOM and get its position explicitly:
webView.evaluateJavaScript(
"document.body.lastChild.getBoundingClientRect().bottom + window.scrollY"
) { [weak self] (result, _) in
guard let self = self,
let height = result as? CGFloat,
height > 0 else { return }
self.heightConstraint?.constant = height
}

Resources