https://s3-us-west-2.amazonaws.com/secure.notion-static.com/f6554375-1a6d-451e-871f-bc1a43b967ff/Group.png

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.

How does that work?

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)
  }