20

I'm trying to implement Estimated Progress in my WKWebView but can't seem to figure it out. Can you help?

Here's what I've got:

self.view = self.webView;

NSURL *url = [NSURL URLWithString:stringWeb];
NSURLRequest *request = [NSURLRequest requestWithURL:url];

WKWebViewConfiguration *configuration = [[WKWebViewConfiguration alloc] init];

self.webView = [[WKWebView alloc] initWithFrame:self.view.frame configuration:configuration];

[self.webView loadRequest:request];

I see this answer gets at it a bit, but thats for a spinner: UIWebView with Progress Bar

And Apple documents some sort of estimatedProgress (I'm assuming its the thin blue bar right below the navigation bar that shows progress like in Safari) but I don't see generally how that would be implemented: https://developer.apple.com/library/ios/documentation/WebKit/Reference/WKWebView_Ref/#//apple_ref/occ/instp/WKWebView/estimatedProgress

So I'm stuck here. Any help would be appreciated, thanks!

UPDATE: This is what I have right now. Getting a crash because it looks like my Progress View and WKWebView are loading twice, and I'm not sure why that would be. Getting an error that observer needs to be removed. Here's my code as it stands-

ViewController.h

@interface WebPageViewController : UIViewController <UIWebViewDelegate>
@property (strong, nonatomic) NSString *stringMobile;
@property (strong, nonatomic) NSString *stringWeb;
@property (strong, nonatomic) IBOutlet UIView *view;
@property (nonatomic, strong) WKWebView *webView;
@property (nonatomic) UIProgressView *progressView;

ViewController.m

- (void)viewDidLoad
{
    [super viewDidLoad];
    // Do any additional setup after loading the view.

    self.webView = [[WKWebView alloc] initWithFrame:self.view.bounds];
    [self.view addSubview:self.webView];

    [self.webView addObserver:self forKeyPath:@"estimatedProgress" options:NSKeyValueObservingOptionNew context:NULL];

    self.progressView = [[UIProgressView alloc] initWithProgressViewStyle:UIProgressViewStyleDefault];
    self.progressView.center = self.view.center;
    [self.view addSubview:self.progressView];

    NSURLRequest *URLRequest = [[NSURLRequest alloc] initWithURL:[NSURL URLWithString:stringWeb]];
    [self.webView loadRequest:URLRequest];


}

- (void)dealloc {
    [self.webView removeObserver:self forKeyPath:@"estimatedProgress"];

    // if you have set either WKWebView delegate also set these to nil here
    [self.webView setNavigationDelegate:nil];
    [self.webView setUIDelegate:nil];
}

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
    if ([keyPath isEqualToString:@"estimatedProgress"] && object == self.webView) {
        [self.progressView setAlpha:1.0f];
        [self.progressView setProgress:self.webView.estimatedProgress animated:YES];

        if(self.webView.estimatedProgress >= 1.0f) {
            [UIView animateWithDuration:0.3 delay:0.3 options:UIViewAnimationOptionCurveEaseOut animations:^{
                [self.progressView setAlpha:0.0f];
            } completion:^(BOOL finished) {
                [self.progressView setProgress:0.0f animated:NO];
            }];
        }
    }
    else {
        [super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
    }
}

UPDATE: Using the CocoaPods this is what I have but its showing two Views instead of just one webview

- (void)viewDidLoad {
    [super viewDidLoad];
    NSURL *myURL = [NSURL URLWithString: [self.url stringByAddingPercentEscapesUsingEncoding:
                                          NSUTF8StringEncoding]];
    NSURLRequest *request = [NSURLRequest requestWithURL:myURL];
    //[self.webView loadRequest:request];

    // KIN
    // Deleted UIWebView in Storyboard
    KINWebBrowserViewController *webBrowser = [[KINWebBrowserViewController alloc] init];
    [self.navigationController pushViewController:webBrowser animated:YES];
    [webBrowser loadURL:myURL];
}
Community
  • 1
  • 1
Realinstomp
  • 532
  • 2
  • 13
  • 30
  • Check out the update that I made to my answer where you can remove the observer in the dealloc method. – dfmuir Oct 05 '14 at 22:45

3 Answers3

59

Check KINWebBrowser on GitHub to see a full implementation of the solution below.

If you look closely at the documentation for the estimatedProgress property of WKWebView that you linked to you will see:

The WKWebView class is key-value observing (KVO) compliant for this property.

This means that you can set up key value observing on the estimatedProgress property to observe changes to it's value. From the observeValueForKeyPath method you can update your UI.

The KVO design pattern in Cocoa is pretty messy. Check out this excellent NSHipster article about the best practices of Key Value Observing.

Here is the KVO implementation for estimatedProgress on WKWebView:

From your UIViewController, set up your WKWebView and add self as an observer of estimatedProgress

    self.webView = [[WKWebView alloc] initWithFrame:self.view.bounds];
    [self.view addSubview:self.webView];

    [self.webView addObserver:self forKeyPath:NSStringFromSelector(@selector(estimatedProgress)) options:NSKeyValueObservingOptionNew context:NULL];

In the same UIViewController set up your observeValueForKeyPath method to filter out the estimatedProgress property of webView. You can then access the estimatedProgress value directly and update your UI accordingly.

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
    if ([keyPath isEqualToString:NSStringFromSelector(@selector(estimatedProgress))] && object == self.webView) {
        NSLog(@"%f", self.webView.estimatedProgress);
        // estimatedProgress is a value from 0.0 to 1.0
        // Update your UI here accordingly
    }
    else {
        // Make sure to call the superclass's implementation in the else block in case it is also implementing KVO
        [super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
    }
}

Make sure to remove KVO from the UIViewController in the dealloc method of that UIViewController. It is important to check if isViewLoaded to avoid a crash if the observer has not already been added.

- (void)dealloc {

    if ([self isViewLoaded]) {
        [self.wkWebView removeObserver:self forKeyPath:NSStringFromSelector(@selector(estimatedProgress))];
    }

    // if you have set either WKWebView delegate also set these to nil here
    [self.wkWebView setNavigationDelegate:nil];
    [self.wkWebView setUIDelegate:nil];
}

To see this in action on some large files load a huge image file of this sweet galaxy. (This file is 35MB. Make sure you are on WiFi!)

    NSURLRequest *URLRequest = [[NSURLRequest alloc] initWithURL:[NSURL URLWithString:@"http://www.spacetelescope.org/static/archives/images/large/opo0328a.jpg"]];
    [self.webView loadRequest:URLRequest];

If you are using a UIProgressView you can achieve a safari like fade-out effect with this code:

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
    if ([keyPath isEqualToString:NSStringFromSelector(@selector(estimatedProgress))] && object == self.wkWebView) {
        [self.progressView setAlpha:1.0f];
        [self.progressView setProgress:self.wkWebView.estimatedProgress animated:YES];

        if(self.wkWebView.estimatedProgress >= 1.0f) {
            [UIView animateWithDuration:0.3 delay:0.3 options:UIViewAnimationOptionCurveEaseOut animations:^{
                [self.progressView setAlpha:0.0f];
            } completion:^(BOOL finished) {
                [self.progressView setProgress:0.0f animated:NO];
            }];
        }
    }
    else {
        [super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
    }
}
dfmuir
  • 2,048
  • 1
  • 17
  • 14
  • This looks really thorough and makes sense, let me try to implement it! Thanks! – Realinstomp Oct 05 '14 at 13:56
  • 1
    Where would I put the progress bar in? Is it where you have commented "Update your UI here accordingly"? Or am I not seeing the progress bar in the webView because I have something covering it or something? – Realinstomp Oct 05 '14 at 14:52
  • WKWebView does not include a UIProgressView automatically. You will have to create, position, and add it manually. I also added some code to show how to interact with a UIProgressView. – dfmuir Oct 05 '14 at 16:17
  • ProgressView is working it looks like, but I'm getting a crash (as described in my UPDATE added to my original question). Any ideas? *** Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'An instance 0x79077d90 of class WKWebView was deallocated while key value observers were still registered with it. Current observation info: ( Context: 0x0, Property: 0x78644ec0> )' – Realinstomp Oct 05 '14 at 21:35
  • I should have included that you need to remove KVO in the dealloc method of the UIViewController. Otherwise, there will be a reference to a nil observer. I updated my answer above with some sample code. – dfmuir Oct 05 '14 at 21:48
  • Awesome that solved it. Marked your answer. One quick question if you have time: The Progress Bar is animation twice (so it'll run once all the way thru and then once more again after that), do you know why that would be? Thanks! – Realinstomp Oct 06 '14 at 01:05
  • Without seeing your code that will be difficult to diagnose. Maybe the URL that you are loading has a redirect? I have tested it with http://www.apple.com and this does not happen. – dfmuir Oct 06 '14 at 01:07
  • Last quick question, I swear: I can't seem to get the Progress View to show up unless I do `self.progressView.center = self.view.center;` and I obviously am not wanting it to be in the center of the page. Do you have any idea what I should use instead? I updated by code for you in my original question. Thanks! – Realinstomp Oct 06 '14 at 23:30
  • Check out my project https://github.com/dfmuir/KINWebBrowser to see how I added it to the bottom of the UINavigationBar of the UINavigationController. This allows you to achieve a Safari like look. Otherwise you should be able to use setFrame and setCenter to put it where you need it. I can't see anything wrong with your sample code which sets it to the center of the view, but if you want to post your whole project on github I will take a look. – dfmuir Oct 07 '14 at 00:12
  • Cool, I just starred your project on GitHub and will check that out to see! – Realinstomp Oct 07 '14 at 01:37
  • Awesome. Right now it uses UIWebView, but I am in the process of updating it to use WKWebView when on iOS 8. The result will be a customizable drop in browser module optimized for iOS 7 & 8. – dfmuir Oct 07 '14 at 02:54
  • I'm actually trying to use your CocoaPod now (good job by you!), and am getting an error on `KINWebBrowserViewController *webBrowser = [KINWebBrowserViewController webBrowserViewController];`, what should I be using for my specific project instead of that? – Realinstomp Oct 18 '14 at 13:47
  • Updated my answer to show what I'm trying out. It's pushing from the `tableview` to a blank view to the KIN view. I'm not sure what's making it do the blank view. Any ideas? – Realinstomp Oct 18 '14 at 14:22
  • I assume its probably the `pushToViewController` line, but I'm already on that view controller so I don't want it to push to any other view controller necessarily, I just want it to load in the view controller its already in. – Realinstomp Oct 18 '14 at 15:31
  • Two things 1) Make sure that you update to the latest version that I pushed out this morning 1.0.2, I also updated the README to reflect the new interface. Thanks! 2) Try setting [KINWebBrowserViewController webBrowser] as the rootViewController of your UINavigationController. – dfmuir Oct 18 '14 at 17:09
  • Got #1, not sure how to do #2 since I'm using a storyboard. Do I set that Navigation Controller storyboard to `KINWebBrowserViewController` or something? – Realinstomp Oct 18 '14 at 23:09
  • Or would I do it through code with something like '[self.navigationController initWithRootViewController:webBrowser];` – Realinstomp Oct 19 '14 at 00:02
  • In storyboard you will need to change the class of your rootViewController to KINWebBrowserViewController. You can also do it in code the way that you have above. – dfmuir Oct 19 '14 at 18:07
  • The only problem with that is that its not my root view controller ya know? Theres a 2 view controllers that precede it. – Realinstomp Oct 19 '14 at 20:27
  • @dfmuir Any reason you are calling `removeObserver` method in `dealloc` and not `viewWillDisappear`? – Pierre Espenan Dec 15 '16 at 13:25
  • @PierreEspenan I would say `dealloc` is be better. This will ensure that the observer will only be removed when the `UIViewController` instance has been fully destroyed and will not be used again. What if you wanted to reuse the `UIViewController` instance? Let's say you put `removeObserver` in `viewWillDisappear`. If you were to call `popViewController`, but still retain the instance of the `UIViewController`, the view controller would no longer receive the event if you were to use it again at a later time. – dfmuir Dec 16 '16 at 20:18
27

Swift-like:

@IBOutlet weak var progressView: UIProgressView!

//...

func viewDidLoad() {
    webView.addObserver(self, forKeyPath: "estimatedProgress", options: .New, context: nil) // add observer for key path
}

// ...

override func observeValueForKeyPath(keyPath: String, ofObject object: AnyObject, change: [NSObject : AnyObject], context: UnsafeMutablePointer<Void>) {
    if (keyPath == "estimatedProgress") { // listen to changes and updated view
        progressView.hidden = webView.estimatedProgress == 1
        progressView.setProgress(Float(webView.estimatedProgress), animated: true)
    }
}

Swift 3 update:

// Add Observer in viewDidLoad
webView.addObserver(self, forKeyPath: "estimatedProgress", options: .new, context: nil)


override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) 
{
        if (keyPath == "estimatedProgress") { // listen to changes and updated view
            progress.isHidden = webView.estimatedProgress == 1
            progress.setProgress(Float(webView.estimatedProgress), animated: false)
        }
}


Also please make sure to implement "WKNavigationDelegate" and add webview reference navigationDelegate to self like below

webView.navigationDelegate=self
Rakesh Yembaram
  • 433
  • 4
  • 7
Oleg Novosad
  • 2,261
  • 1
  • 27
  • 28
  • 19
    You missed this `deinit { webView.removeObserver(self, forKeyPath: "estimatedProgress") }` – Husam Mar 20 '16 at 20:02
  • `animated: false` makes more sense here. Otherwise, you'll see the progress bar going back first and then forward (as you go from one page to another). – RawMean Feb 05 '17 at 17:18
  • please make sure to implement "WKNavigationDelegate" and add webview reference navigationDelegate to self like below webView.navigationDelegate=self – Rakesh Yembaram Jun 14 '17 at 14:32
  • 2
    For Swift 3 I'd suggest the keyPath Macro: `#keyPath(WKWebView.estimatedProgress)`, for Swift 4 it will just be `\WKWebView.estimatedProgress`. – ff10 Aug 07 '17 at 16:00
  • If anyone know please suggest, When a second webview is created at a later stage and added to the view how to update the progressview with estimate from the new webview. – t_godd Jul 18 '18 at 07:53
4

Swift 3.2 and higher:

private var progressKVOhandle: NSKeyValueObservation?
@IBOutlet weak var progressView: UIProgressView!
// ...

override func viewDidLoad() {
    super.viewDidLoad()

    // ...

    progressKVOhandle = webView.observe(\.estimatedProgress) { [weak self] (object, _) in
        self?.progressView.setProgress(Float(object.estimatedProgress), animated: true)
    }
}
Vitalii
  • 4,267
  • 1
  • 40
  • 45