/ ios

Simple HUD with Swift Protocols

Call them HUD, spinner, loading indicator or loading view...

This article is about building and implementing your own and simple HUD and activity indicators with Swift protocol.

Why, when and where do you need it?

You have a wonderful app. The user is doing something on your app that requires an asynchronous task or is computing data and it just needs time. You need to display something indicating your app is doing something.

And this can happen on different levels:

  • on the whole screen (a view controller's main view)
  • on multiple or single graphical components (eg: a child view controller's view, custom views/cells, any other subviews)

What you could do...

You could chose to integrate a dependency that does this very well: MBProgressHUD, SVProgressHUD, PKHUD, JGProgressHUD... anythingHUD.

But even if those libraries do really well what they're made for, you might just want to do your own (for many reasons) or being able to do more than just display a big giant HUD on the entire screen of your app when only some parts of your UI are affected.

What you can do with Swift protocols

In our use case, with a Swift protocol you'll be able to define the behavior of any of your component when it is related to a loadable content.

Let's start step by step.

First, let's define a Loadable protocol.

protocol Loadable {
    func showLoadingView()
    func hideLoadingView()
}

Pretty simple. Any class conforming to the Loadable protocol will have to implement these two methods.

In the following examples, we will define the behavior of our Loadable protocol for some components your already familiar with.

UIViewController

You're on a screen of your app, a UIViewController. You are performing a short but time consuming task (eg: loading remote data). You want to display a HUD on your screen.

To begin with, we need to define a custom view for our HUD that we will simply call LoadingView.

In my implementation, it will be a view with a translucent black background, rounded corners and will contain a UIActivityIndicatorView in its center.

final class LoadingView: UIView {
    private let activityIndicatorView = UIActivityIndicatorView(activityIndicatorStyle: .whiteLarge)
    
    override func layoutSubviews() {
        super.layoutSubviews()
        
        backgroundColor = UIColor.black.withAlphaComponent(0.7)
        layer.cornerRadius = 5
        
        if activityIndicatorView.superview == nil {
            addSubview(activityIndicatorView)
            
            activityIndicatorView.translatesAutoresizingMaskIntoConstraints = false
            activityIndicatorView.centerXAnchor.constraint(equalTo: centerXAnchor).isActive = true
            activityIndicatorView.centerYAnchor.constraint(equalTo: centerYAnchor).isActive = true
            activityIndicatorView.startAnimating()
        }
    }
    
    public func animate() {
        activityIndicatorView.startAnimating()
    }
}

Now, we can define the default Loadable implementation for UIViewController that will display our LoadingView.

What's nice with Swift protocol is that we can write the implementation of Loadable for any UIViewController outside of this latter. In other words, we don't have to write the implementation of Loadable in an extension of our UIViewController or its subclasses, we just have to make sure our UIViewController and/or its subclasses conforms to Loadable and put its implementation in a protocol extension.

fileprivate struct Constants {
    /// an arbitrary tag id for the loading view, so it can be retrieved later without keeping a reference to it
    fileprivate static let loadingViewTag = 1234
}

/// Default implementation for UIViewController
extension Loadable where Self: UIViewController {
    
    func showLoadingView() {
        let loadingView = LoadingView()
        view.addSubview(loadingView)
        
        loadingView.translatesAutoresizingMaskIntoConstraints = false
        loadingView.widthAnchor.constraint(equalToConstant: 100).isActive = true
        loadingView.heightAnchor.constraint(equalToConstant: 100).isActive = true
        loadingView.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
        loadingView.centerYAnchor.constraint(equalTo: view.centerYAnchor).isActive = true
        loadingView.animate()
        
        loadingView.tag = Constants.loadingViewTag
    }
    
    func hideLoadingView() {
        view.subviews.forEach { subview in
            if subview.tag == Constants.loadingViewTag {
                subview.removeFromSuperview()
            }
        }
    }
}

Done! Any UIViewController or its subclasses can now show or hide a LoadingView! 🎉

Here is a sample code example:

class MyViewController: UIViewController, Loadable {
    override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)
        
        showLoadingView()
        DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + .seconds(3)) {
            /// ... 3 seconds later
            self.hideLoadingView()
        }
    }
}

hud_viewcontroller-1

UITableViewCell

Now let's say you have a UITableView with a list of items. When you tap on a row, you want to fetch data before doing any other action (eg: presenting or pushing a new screen).

Your task can take a few seconds and you don't want your interface to appear frozen. So you need to display a UIActivityIndicatorView for the selected cell only.

Since it's a UITableViewCell, we can do it by simply setting an instance of UIActivityIndicatorView as our cell's accessory view.

Which is exactly what we'll be doing in the following example.

final public class CustomTableViewCell: UITableViewCell { }

extension CustomTableViewCell: Loadable {
    func showLoadingView() {
        let activityIndicatorView = UIActivityIndicatorView(activityIndicatorStyle: .gray)
        accessoryView = activityIndicatorView
        activityIndicatorView.startAnimating()
    }
    
    func hideLoadingView() {
        accessoryView = nil
    }
}

Easy peasy!

Our CustomTableViewCell now conforms to Loadable and implements its methods.

Just call showLoadingView() whenever needed and the cell will display a native activity indicator view in its accessory view area.

spinner_tableviewcell

To go further...

We did it with a UIViewController and a UITableViewCell. It can easily be done with any custom views.

In our examples, we could have done even cleaner by:

  • implementing the default Loadable behaviors for UIView and changing the implementation for UIViewController so it will call its view's Loadable protocol implementation
  • chosing to call an external dependency (MBProgressHUD or anything that looks like it) as the default implementation for UIViewController

... and beyond

The great thing with a protocol is that it can be related to anything... What about ARKit?

SpiderManHUD
Image via Sony Pictures

But I'll leave you here. 😄

Conclusion

To make it short, you may have custom views that display loadable content and behave in their own ways. To make things clean, you can make them conform to your own Loadable protocol and implement its behaviors.

Adopt Swift protocols!

And congratulations! If you got here safe, you just started playing with Protocol-oriented Programming! 😉

Simple HUD with Swift Protocols
Share this

Subscribe to David Yang