Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to easily display an activity indicator from any ViewController?

Tags:

ios

swift

Writing an app with some network activity I find myself writing the same code for multiple view controllers over and over just to display an activity indicator.

class SomeViewController: UIViewController {
    let indicator: UIActivityIndicatorView = UIActivityIndicatorView(frame: CGRect(x: 0.0, y: 0.0, width: 100.0, height: 100.0))

    override func viewDidLoad() {
        super.viewDidLoad()

        // customize indicator
        self.indicator.layer.cornerRadius = 10
        self.indicator.center = self.view.center
        self.indicator.hidesWhenStopped = true
        self.indicator.activityIndicatorViewStyle = UIActivityIndicatorViewStyle.whiteLarge
        self.indicator.backgroundColor = UIColor(red: 1/255, green: 1/255, blue: 1/255, alpha: 0.5)
    }
    // MARK: - Acitivity Indicator
    func startIndicatingActivity() {
        DispatchQueue.main.async {
            self.view.addSubview(self.indicator)
            self.indicator.startAnimating()
        }
    }

    func stopIndicatingActivity() {
        DispatchQueue.main.async {
            self.indicator.stopAnimating()
        }
    }
}

Within the same SomeViewController class I can then use it as follows:

@IBAction func startHeavyNetworkStuffButtonPressed(_ sender: UIButton) {
    startIndicatingActivity()
    doHeavyNetworkStuff() { success in
        // heavy networking has finished
        stopIndicatingActivity()
    }
}

This works fine as long as I only need to display the activity indicator in a single view controller. However, it's tedious to do it over and over for every view controller that needs this functionality. As I hate writing the same code over and over, I am in search of a solution where I can simply call startIndicatingActivity() (and stopIndicatingActivity() respectively) in any view controller.

0th idea - Extension

My obvious first thought was to write an extension for the UIViewController class. As I need to store an instance of the UIActivityIndicatorView, however, I got the Extensions may not contain stored properties error.

1st idea - Subclassing

Next up: subclassing UIViewController. This would work fine for any simple view controller. However, if I needed the same functionality for a MyCustomTableViewController, I would again need to first subclass from UITableViewController and copy/paste existing code.

My question

Is there an elegant way to call startIndicatingActivity() / stopIndicatingActivity() in any view controller while avoiding to copy/paste large amounts of code? I'm assuming an elegant solution would involve an extension, protocol, or some kind of multiple-inheritance approach.

like image 501
pmlk Avatar asked Apr 19 '18 20:04

pmlk


2 Answers

This SO thread is the solution! Turns out there is a way to solve this with an extension and simulated properties, after all.

Posting the complete solution for the interested reader:

Extending UIViewController

extension UIViewController {
    // see ObjectAssociation<T> class below
    private static let association = ObjectAssociation<UIActivityIndicatorView>()

    var indicator: UIActivityIndicatorView {
        set { UIViewController.association[self] = newValue }
        get {
            if let indicator = UIViewController.association[self] {
                return indicator
            } else {
                UIViewController.association[self] = UIActivityIndicatorView.customIndicator(at: self.view.center)
                return UIViewController.association[self]!
            }
        }
    }

    // MARK: - Acitivity Indicator
    public func startIndicatingActivity() {
        DispatchQueue.main.async {
            self.view.addSubview(self.indicator)
            self.indicator.startAnimating()
            //UIApplication.shared.beginIgnoringInteractionEvents() // if desired
        }
    }

    public func stopIndicatingActivity() {
        DispatchQueue.main.async {
            self.indicator.stopAnimating()
            //UIApplication.shared.endIgnoringInteractionEvents()
        }
    }
}

Borrowing code from said SO thread

// source: https://stackoverflow.com/questions/25426780/how-to-have-stored-properties-in-swift-the-same-way-i-had-on-objective-c
public final class ObjectAssociation<T: AnyObject> {

    private let policy: objc_AssociationPolicy

    /// - Parameter policy: An association policy that will be used when linking objects.
    public init(policy: objc_AssociationPolicy = .OBJC_ASSOCIATION_RETAIN_NONATOMIC) {

        self.policy = policy
    }

    /// Accesses associated object.
    /// - Parameter index: An object whose associated object is to be accessed.
    public subscript(index: AnyObject) -> T? {

        get { return objc_getAssociatedObject(index, Unmanaged.passUnretained(self).toOpaque()) as! T? }
        set { objc_setAssociatedObject(index, Unmanaged.passUnretained(self).toOpaque(), newValue, policy) }
    }
}

For the sake of completeness

I also extended the UIActivityIndicatorView class with a static function with my customizations.

extension UIActivityIndicatorView {
    public static func customIndicator(at center: CGPoint) -> UIActivityIndicatorView {
        let indicator = UIActivityIndicatorView(frame: CGRect(x: 0.0, y: 0.0, width: 100.0, height: 100.0))
        indicator.layer.cornerRadius = 10
        indicator.center = center
        indicator.hidesWhenStopped = true
        indicator.activityIndicatorViewStyle = UIActivityIndicatorViewStyle.whiteLarge
        indicator.backgroundColor = UIColor(red: 1/255, green: 1/255, blue: 1/255, alpha: 0.5)
        return indicator
    }
}
like image 101
pmlk Avatar answered Nov 14 '22 20:11

pmlk


Sample show full loading in any View

extension UIView{
 /**
     ShowLoader:  loading view ..

     - parameter Color:  ActivityIndicator and view loading color .

     */
    func showLoader(_ color:UIColor?){


        let LoaderView  = UIView(frame: CGRect(x: 0, y: 0, width: self.frame.width, height: self.frame.height))
        LoaderView.tag = -888754
        LoaderView.backgroundColor = color
        let Loader = UIActivityIndicatorView(frame: CGRect(x: 0, y: 0, width: 60, height: 30))
        Loader.center = LoaderView.center
        Loader.activityIndicatorViewStyle = .whiteLarge
        Loader.color = Color.primaryColor
        Loader.startAnimating()
        LoaderView.addSubview(Loader)
        self.addSubview(LoaderView)
    }

 /**
     dismissLoader:  hidden loading view  ..     
     */
    func dismissLoader(){


        self.viewWithTag(-888754)?.removeFromSuperview()
    }
}

call this func

in UIViewController

self.view.showLoader(nil) //you can set background color nil or any color
// dismiss
self.view.dismissLoader()

I prefer this method because you can use it any view in button , tableview , cell ...etc

like image 33
a.masri Avatar answered Nov 14 '22 22:11

a.masri