24

Hi I'm new to programming and I'm trying to make my first app for iPhones on Xcode. My app contains of a button which opens a UIWebView when pressed and loads up a homepage. Now I also want to add a Progress View to the WebView like Safari also uses, which indicates the progress of loading the page. How can I do that?
My code so far for loading the URL in the UIWebView:

.h

IBOutlet UIWebView *webView;

.m

- (void)viewDidLoad
{
[webView loadRequest:[NSURLRequest requestWithURL: [NSURL URLWithString:@"http:/www.google.com/"]]];

Thanks for your help!

rmaddy
  • 314,917
  • 42
  • 532
  • 579
user3220034
  • 271
  • 1
  • 2
  • 4
  • Check the answers here for a better solution regarding the place where you put the code for the progress bar http://stackoverflow.com/questions/23966697/show-progress-bar-until-it-load-the-data-in-uiwebview-ios7 – Suhaib Sep 25 '16 at 06:36

9 Answers9

74

To have an accurate UIProgressView, you need to have some task that:

  • You can get information from while it isn't complete
  • Quantify its "completeness" as a percentage based on that information.

Now when you are loading your UIWebView, thats not possible. And Apple doesn't do it either. Apple often uses fake UIProgressViews to give you something to look at while the page is loading. Mail also uses fake progress views. Go try it out for yourself. This is how Apple's fake progress views work:

  • The progress view starts moving at a slow, constant rate
  • If the task finishes before the bar completes, it suddenly zips across the rest to 100% before disappearing
  • If the task takes a long time, the progress view will stop at 95% and will stay there until the task is complete.

To achieve this, you will have to animate the progressView manually. You could subclass it but that would probably be a bit advanced for you. The simplest way would be this:

In myViewController.h

@interface myViewController : UIViewController {
     BOOL theBool;
     //IBOutlet means you can place the progressView in Interface Builder and connect it to your code
     IBOutlet UIProgressView* myProgressView;
     NSTimer *myTimer;
}
@end

In myViewController.m

#import "myViewController.h"
@implementation myViewController
- (void)webViewDidStartLoad:(UIWebView *)webView{
     myProgressView.progress = 0;
     theBool = false;
     //0.01667 is roughly 1/60, so it will update at 60 FPS
     myTimer = [NSTimer scheduledTimerWithTimeInterval:0.01667 target:self selector:@selector(timerCallback) userInfo:nil repeats:YES];
}
- (void)webViewDidFinishLoad:(UIWebView *)webView{
     theBool = true;
}
-(void)timerCallback {
    if (theBool) {
         if (myProgressView.progress >= 1) {
              myProgressView.hidden = true;
              [myTimer invalidate];
         }
         else {
              myProgressView.progress += 0.1;
         }
    }
    else {
         myProgressView.progress += 0.05;
         if (myProgressView.progress >= 0.95) {
              myProgressView.progress = 0.95;
         }
    }
}
@end

Then, where your task gets completed, set theBool = true; and the progress view will take care of itself. Change the values in the if statement thing to control the speed of the animation.

Nora
  • 167
  • 2
  • 3
  • 14
WolfLink
  • 3,308
  • 2
  • 26
  • 44
  • 2
    _+1_ I didn't read your answer properly before submitting mine which also suggest fake progress bar :) – Tricertops Jan 21 '14 at 22:41
  • Thank you for your help. But it won't work for me. What am I doing wrong? I put a Progress View into my Web View and connected it with the 'IBOutlet'. Now I can see a Progress View while running the simulator but it won't animate. – user3220034 Jan 21 '14 at 23:06
  • When I put e.g. 0,5 in the Progress field inside the Attributes Inspector of the Progress View, the Progress View will start at 0.5 and then suddenly jump to 0 without animation. – user3220034 Jan 21 '14 at 23:33
  • 1
    Make sure your timer is working. Is the `timerCallback` being called? – WolfLink Jan 22 '14 at 02:20
  • What do you mean with "being called"? I put you codes into the viewDidAppear and the viewDidLoad but I'm not sure about the timerCallBack. This void doesn't seem to work. – user3220034 Jan 23 '14 at 20:32
  • First of all, `viewDidLoad`/`viewDidAppear` is not where your `UIWebView` finishes loading, but it may be where you start loading. You need to implement the `UIWebViewDelegate`. Secondly, when you run a method, it is called "calling" that method. Example `[self runMethod]` calls `-(void)runMethod` of the current object. If your `NSTimer` is set up properly, it should call the `timerCallback` method repeatedly. Put `NSLog(@"Hello World");` in there to test if it's being called. It should print "Hello World" in the console if it does work. – WolfLink Jan 24 '14 at 00:27
  • 1
    @user3220034 danihvilla noticed a mistake I made. use `scheduledTimerWithInterval:` instead of `timerWithInterval:` – WolfLink Jan 30 '14 at 01:53
  • 3
    I understand you are hiding the progress bar but the timer should be still running right? Shouldn't we stop the timer also? – KD. Jul 09 '14 at 11:00
  • @KD good point! The answer has been modified. The function to stop the timer is `[MyTimer invalidate]` – WolfLink Jul 22 '14 at 18:28
  • Why not use CADisplayLink instead of NSTimer? – akasaka Jun 29 '16 at 09:22
  • I don't really think we need the additional functionality of CADisplayLink for this simple use case but maybe it will make a slight difference. If you want to use it, go ahead! – WolfLink Jun 29 '16 at 22:25
5

Swift 2.2

class ViewController: UIViewController,UIWebViewDelegate {

    @IBOutlet weak var webView: UIWebView!
    @IBOutlet weak var activityIndicator: UIActivityIndicatorView!
    @IBOutlet weak var myProgressView: UIProgressView!

    var myTimer = NSTimer()
    var theBool = Bool()

    override func viewDidLoad() {
        super.viewDidLoad()

        let url = NSURL(string: "https://www.youtube.com")
        let request = NSURLRequest(URL: url!)
        activityIndicator.hidesWhenStopped = true
        activityIndicator.startAnimating()
        webView.loadRequest(request)

    }
    func timerCallback(){
        if theBool {
            if myProgressView.progress >= 1 {
                myProgressView.hidden = true
                myTimer.invalidate()
            }else{
                myProgressView.progress += 0.1
            }
        }else{
            myProgressView.progress += 0.05
            if myProgressView.progress >= 0.95 {
                myProgressView.progress = 0.95
            }
        }
    }

    func webViewDidStartLoad(webView: UIWebView) {
        activityIndicator.startAnimating()
        myProgressView.progress = 0
        theBool = false
        myTimer =  NSTimer.scheduledTimerWithTimeInterval(0.01667,target: self,selector: #selector(ViewController.timerCallback),userInfo: nil,repeats: true)
    }
    func webViewDidFinishLoad(webView: UIWebView) {
        activityIndicator.stopAnimating()
        theBool = true
    }
}
ZAFAR007
  • 3,049
  • 1
  • 34
  • 45
4

If anyone wants to do it in swift 3, I spent a few days trying to figure it out and finally did.

Here is the code :)

override func viewDidLoad() {
    super.viewDidLoad()

    let url = NSURL(string: "http://google.com")
    let request = NSURLRequest(url: url as! URL)
    webView.loadRequest(request as URLRequest)
    webView.delegate=self

}

func webViewDidStartLoad(_ webView: UIWebView) {

    self.progressView.setProgress(0.1, animated: false)
}


func webViewDidFinishLoad(_ webView: UIWebView) {

    self.progressView.setProgress(1.0, animated: true)
}

func webView(_ webView: UIWebView, didFailLoadWithError error: Error) {

    self.progressView.setProgress(1.0, animated: true)
}

override func didReceiveMemoryWarning() {
    super.didReceiveMemoryWarning()
    // Dispose of any resources that can be recreated.
}
Matej Marić
  • 125
  • 1
  • 12
3

I followed Wolflink's answer and found the same problem not having the progressView animated.

After reading NSTimer Class Reference:

https://developer.apple.com/library/Mac/documentation/Cocoa/Reference/Foundation/Classes/NSTimer_Class/Reference/NSTimer.html#//apple_ref/occ/clm/NSTimer/timerWithTimeInterval:target:selector:userInfo:repeats:

In Discussion you can read: "You must add the new timer to a run loop, using addTimer:forMode:. "

so I added

[[NSRunLoop currentRunLoop] addTimer:myTimer forMode:NSDefaultRunLoopMode];

after

myTimer = [NSTimer timerWithTimeInterval:0.01667 target:self selector:@selector(timerCallback) userInfo:nil repeats:YES];

It worked for me (As WolfLink recommended I did subclass UIProgressView )

danihvilla
  • 31
  • 2
  • Thanks for noticing! I meant to use `[NSTimer scheduledtimerWithTimeInterval:0.01667 target:self selector:@selector(timerCallback) userInfo:nil repeats:YES];` which adds the timer to the run loop automatically. – WolfLink Jan 29 '14 at 04:56
  • Even cleaner, I'll use that way. I've seen you already edited your answer so @user3220034 can mark it as the correct one. At least it was what I was looking for. – danihvilla Jan 29 '14 at 10:26
2

It's difficult (if even possible), since you would have to track all resources loaded by the site, but …

I have one idea. It's more of a trick than a real solution, but I think even Apple uses this in Messages app :)

  1. When you start loading the page, begin an animation to 90% of the progress (let's say with duration of 1.5 seconds, maybe be different for Wi-Fi, LTE, 3G, …).
  2. When page loads in meantime, quickly animate the progress to 100%. Done!
  3. If the page takes more time to load, the progress bar will stop at 90% and will wait there. Frustrating moment when the user watches slow spinning indicator in status bar! And then finally, the page finish loading and (as in bullet 2.) you play quick animation to 100%. Done!

I think we all know, that this is how Messages app works, since I don't believe sending SMS can be tracked with such accurate progress :)

Tricertops
  • 8,492
  • 1
  • 39
  • 41
1

Apple now supplies the WebKit framework, which includes the WKWebView class which allows you to query the estimated progress using the property 'estimatedProgress', so you no longer must 'fake' the progress like you do to show a progress bar on a UIWebView.

Patrick T Nelson
  • 1,234
  • 12
  • 21
0

I would have added a comment, instead of adding an answer, but I don't yet have enough reputation to comment!

Only a couple of days ago I asked almost exactly the same question. In that question I included the solution I came up with. My solution is more complicated than WolfLink's but it does track the real progress of loading the page, although it isn't 100% accurate. As WolfLink has said that's not possible. My question can be found here: Progress bar for UIWebView

Note whichever way you do it you'll need to check the loading property of UIWebView to determine when the loading of the page is complete.

Community
  • 1
  • 1
Stuart Fisher
  • 201
  • 3
  • 8
0

I have tried the following pattern. It gave me a little bit better result. I am using WKWebview.

@IBOutlet weak var progressView: UIProgressView! // added in storyboard
@IBOutlet weak var webView: WKWebView!

    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view.
        setupEstimatedProgressObserver()
    }

    // Observe the WebViewClient for estimatedProgress value
    private func setupEstimatedProgressObserver() {
        estimatedProgressObserver = webView.observe(\.estimatedProgress, options: [.new]) { [weak self] webView, _ in
            self?.progressView.progress = Float(webView.estimatedProgress)
        }
    }

    //When WebView load finished in WKNavigationDelegate in didFinish method we will use the following approach
    func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
        self.progressView.progress =  1.0
        DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
            // your code here
             self.progressView.isHidden = true
        }
    }
Bidhan Roy
  • 52
  • 4
0

Some of these answers here are not completely correct, since they forgot to register the delegate for the webview. Unfortunately it is not enough to implement UIWebViewDelegate and override the two methods. You also have to set the delegate of the webview to self.

class ViewController: UIViewController,UIWebViewDelegate {

    @IBOutlet weak var webView: UIWebView!
    @IBOutlet weak var activityIndicator: UIActivityIndicatorView!
    @IBOutlet weak var myProgressView: UIProgressView!

    var myTimer = NSTimer()
    var theBool = Bool()

    override func viewDidLoad() {
        super.viewDidLoad()

        let url = NSURL(string: "https://www.youtube.com")
        let request = NSURLRequest(URL: url!)
        activityIndicator.hidesWhenStopped = true
        activityIndicator.startAnimating()

        // !
        webView.delegate = self // <--- important
        // ! 

        webView.loadRequest(request)

    }

    func timerCallback(){
        if theBool {
            if myProgressView.progress >= 1 {
                myProgressView.hidden = true
                myTimer.invalidate()
            }else{
                myProgressView.progress += 0.1
            }
        }else{
            myProgressView.progress += 0.05
            if myProgressView.progress >= 0.95 {
                myProgressView.progress = 0.95
            }
        }
    }

    func webViewDidStartLoad(webView: UIWebView) {
        activityIndicator.startAnimating()
        myProgressView.progress = 0
        theBool = false
        myTimer =  NSTimer.scheduledTimerWithTimeInterval(0.01667,target: self,selector: #selector(ViewController.timerCallback),userInfo: nil,repeats: true)
    }

    func webViewDidFinishLoad(webView: UIWebView) {
        activityIndicator.stopAnimating()
        theBool = true
    } 
}

This code refers to the answer @ZAFAR007

NiklasLehnfeld
  • 984
  • 6
  • 14