Skip to content

Instantly share code, notes, and snippets.

@edc0der
Last active December 17, 2023 13:37
Show Gist options
  • Save edc0der/e4bed05b4c6653ffcd36c0609f27c7c6 to your computer and use it in GitHub Desktop.
Save edc0der/e4bed05b4c6653ffcd36c0609f27c7c6 to your computer and use it in GitHub Desktop.
iOS + Swift: Display activity indicator overlay - UIViewController extension
import UIKit
fileprivate let overlayViewTag: Int = 999
fileprivate let activityIndicatorViewTag: Int = 1000
// Public interface
extension UIView {
func displayAnimatedActivityIndicatorView() {
setActivityIndicatorView()
}
func hideAnimatedActivityIndicatorView() {
removeActivityIndicatorView()
}
}
extension UIViewController {
private var overlayContainerView: UIView {
if let navigationView: UIView = navigationController?.view {
return navigationView
}
return view
}
func displayAnimatedActivityIndicatorView() {
overlayContainerView.displayAnimatedActivityIndicatorView()
}
func hideAnimatedActivityIndicatorView() {
overlayContainerView.hideAnimatedActivityIndicatorView()
}
}
// Private interface
extension UIView {
private var activityIndicatorView: UIActivityIndicatorView {
let view: UIActivityIndicatorView = UIActivityIndicatorView(style: .large)
view.translatesAutoresizingMaskIntoConstraints = false
view.tag = activityIndicatorViewTag
return view
}
private var overlayView: UIView {
let view: UIView = UIView()
view.translatesAutoresizingMaskIntoConstraints = false
view.backgroundColor = .black
view.alpha = 0.5
view.tag = overlayViewTag
return view
}
private func setActivityIndicatorView() {
guard !isDisplayingActivityIndicatorOverlay() else { return }
let overlayView: UIView = self.overlayView
let activityIndicatorView: UIActivityIndicatorView = self.activityIndicatorView
//add subviews
overlayView.addSubview(activityIndicatorView)
addSubview(overlayView)
//add overlay constraints
overlayView.heightAnchor.constraint(equalTo: heightAnchor).isActive = true
overlayView.widthAnchor.constraint(equalTo: widthAnchor).isActive = true
//add indicator constraints
activityIndicatorView.centerXAnchor.constraint(equalTo: overlayView.centerXAnchor).isActive = true
activityIndicatorView.centerYAnchor.constraint(equalTo: overlayView.centerYAnchor).isActive = true
//animate indicator
activityIndicatorView.startAnimating()
}
private func removeActivityIndicatorView() {
guard let overlayView: UIView = getOverlayView(), let activityIndicator: UIActivityIndicatorView = getActivityIndicatorView() else {
return
}
UIView.animate(withDuration: 0.2, animations: {
overlayView.alpha = 0.0
activityIndicator.stopAnimating()
}) { _ in
activityIndicator.removeFromSuperview()
overlayView.removeFromSuperview()
}
}
private func isDisplayingActivityIndicatorOverlay() -> Bool {
getActivityIndicatorView() != nil && getOverlayView() != nil
}
private func getActivityIndicatorView() -> UIActivityIndicatorView? {
viewWithTag(activityIndicatorViewTag) as? UIActivityIndicatorView
}
private func getOverlayView() -> UIView? {
viewWithTag(overlayViewTag)
}
}
@rick7661
Copy link

rick7661 commented Apr 7, 2019

Thanks for this code snippet. Very handy!

@zykis
Copy link

zykis commented Nov 25, 2019

Thanks, dude!

@edc0der
Copy link
Author

edc0der commented Nov 25, 2019

you're welcome. If you have any suggestions, be sure to let me know.

@nanshaki
Copy link

thank you. very cool

@acamill
Copy link

acamill commented May 28, 2020

Update to this in case it serves anyone:

import UIKit
import With

fileprivate let overlayViewTag = 999
fileprivate let activityIndicatorViewTag = 1000

extension UIView {
   private var activityIndicatorView: UIActivityIndicatorView {
      let view = UIActivityIndicatorView(style: .large) 
         view.translatesAutoresizingMaskIntoConstraints = false
         view.tag = activityIndicatorViewTag
      return view
   }
   //
   private var overlayView: UIView {
      let view = UIView()
         view.translatesAutoresizingMaskIntoConstraints = false
         view.backgroundColor = UIColor.black
         view.alpha = 0.5
         view.tag = overlayViewTag
      return view
   }
   //
   public func displayActivityIndicator(shouldDisplay: Bool) {
      shouldDisplay ? setActivityIndicatorView() : removeActivityIndicatorView()
   }
   //
   private func setActivityIndicatorView() {
      guard !isDisplayingActivityIndicatorOverlay() else { return }
      let overlayView = self.overlayView
      let activityIndicatorView = self.activityIndicatorView
      //add subviews
      overlayView.addSubview(activityIndicatorView)
      addSubview(overlayView)
      //add overlay constraints
      overlayView.heightAnchor.constraint(equalTo: self.heightAnchor).isActive = true
      overlayView.widthAnchor.constraint(equalTo: self.widthAnchor).isActive = true
      //add indicator constraints
      activityIndicatorView.centerXAnchor.constraint(equalTo: overlayView.centerXAnchor).isActive = true
      activityIndicatorView.centerYAnchor.constraint(equalTo: overlayView.centerYAnchor).isActive = true
      //animate indicator
      activityIndicatorView.startAnimating()
   }
   //
   private func removeActivityIndicatorView() {
      let activityIndicator = getActivityIndicatorView()
      guard let overlayView = getOverlayView() else { return }
      UIView.animate(withDuration: 0.2, animations: {
         overlayView.alpha = 0.0
         activityIndicator?.stopAnimating()
      }) { (finished) in
         activityIndicator?.removeFromSuperview()
         overlayView.removeFromSuperview()
      }
   }
   //
   private func isDisplayingActivityIndicatorOverlay() -> Bool {
      getActivityIndicatorView() != nil
         && getOverlayView() != nil
   }
   //
   private func getActivityIndicatorView() -> UIActivityIndicatorView? {
      viewWithTag(activityIndicatorViewTag) as? UIActivityIndicatorView
   }
   //
   private func getOverlayView() -> UIView? {
      viewWithTag(overlayViewTag)
   }
}

@edc0der
Copy link
Author

edc0der commented May 28, 2020

Thank you, @acamill for your response and suggestion, I'll give it a try.

Looking at it, it seems you took the responsibility out of the UIViewController, would it mean that now in order to use it from a ViewController you'd have to write

view.displayActivityIndicator(shouldDisplay: true) ?

Would that also mean the overlay would cover any view that called it? i.e, from a cell from a TableView would it only overlay the cell?

As I said, I'll try it and see how it works. And thank you very much

@acamill
Copy link

acamill commented May 28, 2020

@edc0der yes! I ended up not not using it even before displaying anything in the end, but it made more sense to me to have it at the view level as it’s still UI elements?
And yes I think it will appears on any view and just in that view

@edc0der
Copy link
Author

edc0der commented May 29, 2020

Updated the snippet with @acamill's suggested code. Thank you!

Changes:

  • Added type annotation
  • Separated the display method into display and hide, in order to have single responsibility methods
  • Added UIViewController extension to use the overlay to cover the fullscreen without having to write navigationController?.view.displayAnimatedActivityIndicatorView()

@acamill
Copy link

acamill commented May 30, 2020 via email

@wiles88
Copy link

wiles88 commented Jun 28, 2020

How is this applied to a normal swiftUI page. If I want the page to display the loading icon how do I do that?

Import SwiftUI

struct Welcome: View {
  
    var body: some View {
<< DISPLAY LOADING HERE >>
        Text("Hello World")
    }
}

struct Dashboard_Previews: PreviewProvider {
    static var previews: some View {
        Welcome()
    }
    
}

@ahmdmau
Copy link

ahmdmau commented Aug 6, 2020

Thanks for sharing!

@acamill
Copy link

acamill commented Aug 6, 2020 via email

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment