There are a lot of ways to show users that there's some long-running process in the background. The most obvious and easy way is using a blocking progress view. Lots of libraries are on Github and MBProgressHUD https://github.com/jdg/MBProgressHUD is one of them.
But this way is only good when we don't want a user to interact with the app while the operation is being executed.
The default non-blocking approach is to change networkActivityIndicatorVisible of an application instance object like this:
UIApplication.shared.isNetworkActivityIndicatorVisible = true
This way a user will see an activity indicator in a status bar. But it's an obscure way. We can see it in a Twitter app every time we open the app. But Twitter also has an indicator that there's new content available.
So, what else? Let's check some popular messengers like Telegram, WhatsApp. They have an activity indicator in navigation bar. It shows every time a list of chats is getting updated or an internet connection is being established.
We have a titleView property in UINavigationItem which is an optional property of UIView type. Documentation states that it's a
Custom view to use in lieu of a title. May be sized horizontally. Only used when item is topmost on the stack.
So, let's make our own NavigationBarProgressView and make it reusable and easy-to-attach to any UIViewController.
First of all we need one of the UIActivityIndicatorView and one UILabel object to display text. Let's group them in our custom UIView.
public class NavigationBarProgressView: UIView {
let activityIndicator: UIActivityIndicatorView = .init(style: .gray)
let titleLabel: UILabel = {
let label = UILabel(frame: .zero)
return label
}()
}
But we also want to be able to configure them. To fully configure our view we need at least a font, font color, texts, style of activity indicator, and a frame of our view. Let's write a configurator structure describing our view:
public struct NavigationBarProgressViewConfigurator {
public let activityStyle: UIActivityIndicatorView.Style
public let frame: CGRect
public let interItemSpace: CGFloat
public let regularTitle: String
public let pendingTitle: String
public let titleColor: UIColor
public let titleFont: UIFont
public init(
activityStyle: UIActivityIndicatorView.Style = .gray,
frame: CGRect = .init(origin: .zero, size: .init(width: 120, height: 20)),
interItemSpace: CGFloat,
regularTitle: String,
pendingTitle: String,
titleColor: UIColor = .darkText,
titleFont: UIFont
) {
self.activityStyle = activityStyle
self.frame = frame
self.interItemSpace = interItemSpace
self.regularTitle = regularTitle
self.pendingTitle = pendingTitle
self.titleColor = titleColor
self.titleFont = titleFont
}
}
And then we add configuration code to our view class:
let config: NavigationBarProgressViewConfigurator
public init(config: NavigationBarProgressViewConfigurator) {
self.config = config
self.activityIndicator = UIActivityIndicatorView(style: config.activityStyle)
self.activityIndicator.hidesWhenStopped = true
super.init(frame: config.frame)
self.titleLabel.font = config.titleFont
self.titleLabel.text = config.regularTitle
self.addSubview(self.activityIndicator)
self.addSubview(self.titleLabel)
}
@available(*, unavailable)
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
Are we cool now? Yes and no. We need to place our label and activity indicator. We can use either Autolayout or layout them manually. Let's do it manually, there's no need to use heavy linear inequalities (that's what Autolayout is) in such a simple view.
public override func layoutSubviews() {
super.layoutSubviews()
self.activityIndicator.frame.origin = .zero
let maxWidth = self.isPending
? self.frame.width - self.activityIndicator.frame.size.width - self.config.interItemSpace
: self.frame.width
let maxTextSize = CGSize(
width: maxWidth,
height: self.activityIndicator.frame.height
)
let minTextSize = self.titleLabel.sizeThatFits(maxTextSize)
let titleX = self.isPending
? self.activityIndicator.frame.maxX + self.config.interItemSpace
: (maxWidth - minTextSize.width) / 2
self.titleLabel.frame = CGRect(origin: .init(x: titleX,
y: (maxTextSize.height - minTextSize.height)/2),
size: minTextSize)
}