Showing a UIProgressView inside or on top of a UINavigationController's UINavigationBar
I finally found a solution:
I made a custom UINavigationController and added this to viewDidLoad
- (void)viewDidLoad
{
[super viewDidLoad];
// Do any additional setup after loading the view.
progress = [[UIProgressView alloc] init];
[[self view] addSubview:progress];
UIView *navBar = [self navigationBar];
[[self view] addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:[navBar]-[progress(2@20)]"
options:NSLayoutFormatDirectionLeadingToTrailing
metrics:nil
views:NSDictionaryOfVariableBindings(progress, navBar)]];
[[self view] addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|[progress]|"
options:NSLayoutFormatDirectionLeadingToTrailing
metrics:nil
views:NSDictionaryOfVariableBindings(progress)]];
[progress setTranslatesAutoresizingMaskIntoConstraints:NO];
[progress setProgress:0 animated:NO];
}
I created a new UIProgressView (I declared in @interface
) added the constraints to position it beneath the navigation bar and (this step is important:) set translatesAutoresizingMaskIntoConstraints
to NO
.
I reworked the original poster's answer so that the bar is actually just inside the navigation bar. What's nice about this is that when its showing, it overlaps the one pixel bottom line (in effect replacing it), so when you animate the progress bar to hidden, the progress bar fades out and the separator line fades in. The key part of this solution is adding the progress bar to the Navigation Controller's view, not the Navigation bar.
- (void)viewDidLoad
{
[super viewDidLoad];
// Do any additional setup after loading the view.
UIProgressView *progress = [[UIProgressView alloc] initWithProgressViewStyle:UIProgressViewStyleBar];;
[self.view addSubview:progress];
UINavigationBar *navBar = [self navigationBar];
#if 1
[self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:[navBar]-0-[progress]"
options:NSLayoutFormatDirectionLeadingToTrailing
metrics:nil
views:NSDictionaryOfVariableBindings(progress, navBar)]];
[self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|[progress]|"
options:NSLayoutFormatDirectionLeadingToTrailing
metrics:nil
views:NSDictionaryOfVariableBindings(progress)]];
#else
NSLayoutConstraint *constraint;
constraint = [NSLayoutConstraint constraintWithItem:progress attribute:NSLayoutAttributeBottom relatedBy:NSLayoutRelationEqual toItem:navBar attribute:NSLayoutAttributeBottom multiplier:1 constant:-0.5];
[self.view addConstraint:constraint];
constraint = [NSLayoutConstraint constraintWithItem:progress attribute:NSLayoutAttributeLeft relatedBy:NSLayoutRelationEqual toItem:navBar attribute:NSLayoutAttributeLeft multiplier:1 constant:0];
[self.view addConstraint:constraint];
constraint = [NSLayoutConstraint constraintWithItem:progress attribute:NSLayoutAttributeRight relatedBy:NSLayoutRelationEqual toItem:navBar attribute:NSLayoutAttributeRight multiplier:1 constant:0];
[self.view addConstraint:constraint];
#endif
[progress setTranslatesAutoresizingMaskIntoConstraints:NO];
[progress setProgress:0.5 animated:NO];
}
I'm not sure why its necessary to add the 0.5 offset to the NSLayoutContstraints code to get the same match, but it is. I use these not the visual formats, but the choice is yours. Note that contraining to the bottoms makes this seamless in rotation too.
Building on what's already been suggested here, if you want to make it display on every navigation bar in the app, you can make it into an extension (Swift) on UINavigationController:
extension UINavigationController {
public override func viewDidLoad() {
super.viewDidLoad()
let progressView = UIProgressView(progressViewStyle: .Bar)
self.view.addSubview(progressView)
let navBar = self.navigationBar
self.view.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("V:[navBar]-0-[progressView]", options: .DirectionLeadingToTrailing, metrics: nil, views: ["progressView" : progressView, "navBar" : navBar]))
self.view.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("H:|[progressView]|", options: .DirectionLeadingToTrailing, metrics: nil, views: ["progressView" : progressView]))
progressView.translatesAutoresizingMaskIntoConstraints = false
progressView.setProgress(0.5, animated: false)
}
}
UPDATE (Uses Swift 3 Syntax)
Here is a bit more complete solution. I put this extension into a file called UINavigationController+Progress.swift. (Notice I'm using the UIView
tag property to find the UIProgressView
with the optional progressView
property. There may be more elegant ways to do that, but this seems the most straightforward)
import UIKit
let kProgressViewTag = 10000
let kProgressUpdateNotification = "kProgressUpdateNotification"
extension UINavigationController {
open override func viewDidLoad() {
super.viewDidLoad()
let progressView = UIProgressView(progressViewStyle: .bar)
progressView.tag = kProgressViewTag
self.view.addSubview(progressView)
let navBar = self.navigationBar
self.view.addConstraints(NSLayoutConstraint.constraints(withVisualFormat: "V:[navBar]-0-[progressView]", options: .directionLeadingToTrailing, metrics: nil, views: ["progressView" : progressView, "navBar" : navBar]))
self.view.addConstraints(NSLayoutConstraint.constraints(withVisualFormat: "H:|[progressView]|", options: .directionLeadingToTrailing, metrics: nil, views: ["progressView" : progressView]))
progressView.translatesAutoresizingMaskIntoConstraints = false
progressView.setProgress(0.0, animated: false)
NotificationCenter.default.addObserver(self, selector: #selector(UINavigationController.didReceiveNotification(notification:)), name: NSNotification.Name(rawValue: kProgressUpdateNotification), object: nil)
}
var progressView : UIProgressView? {
return self.view.viewWithTag(kProgressViewTag) as? UIProgressView
}
func didReceiveNotification(notification:NSNotification) {
if let progress = notification.object as? ProgressNotification {
if progress.current == progress.total {
self.progressView?.setProgress(0.0, animated: false)
} else {
let perc = Float(progress.current) / Float(progress.total)
self.progressView?.setProgress(perc, animated: true)
}
}
}
}
class ProgressNotification {
var current: Int = 0
var total: Int = 0
}
So I've given a specific implementation here that assumes you want a current count and a total count value to be used to update the progress bar. Now, what you need is to post the notification from the code that is performing whatever tasks are to be used to determine progress--for example downloading a list of files. Here's the code to post the notification:
let notification = ProgressNotification()
notification.current = processedTaskCount
notification.total = totalTaskCount
DispatchQueue.main.async {
NotificationCenter.default.post(name: NSNotification.Name(rawValue: kProgressUpdateNotification), object: notification)
}