Search code examples
swift3grand-central-dispatchalamofirexcode8

How to perform sequential request with Alamofire and update a progressHUD at every step in Swift 3


Ok, I am going nuts over this one...

I'm using Alamofire 4.x (Swift 3 and XCode 8.1). I need to fetch and parse several html requests from a site that requires authentication (no json API, unfortunately). The HTML is then parsed with Fuzi and this progress can take some time so I plan to use a ProgressHUD (PKHUD to be exact) to let users know about what is going on. I also need to grab some html that is not behind an authentication.

I created a struct and functions to handle the overall network process and to parse the data.

I managed to perform the requests and grab the data I need but I can't seem to figure out how to make my HUD updates at the right time.

Here is my code so far:

import Alamofire
import Fuzi
import PKHUD

struct MyMSCProvider {

static let baseUrl = "http://mastersswimming.ca"

//I tried with or without a custom queue - same result
static let processingQueue = DispatchQueue(label: "com.colddiver.processing-queue", qos: .utility)

static func fetchData(data: MscRequest) {

    if data.profile || data.log {

        //Authenticate first!
        HUD.show(.labeledProgress(title: "Authenticating", subtitle: ""))

        let requestUrl = "\(baseUrl)/MyMscPage.jsp"
        let parameters = ["locale": "en", "username": data.user.username, "password": data.user.password]

        Alamofire.request(requestUrl, method: .post, parameters: parameters).responseData(
            queue: processingQueue,
            completionHandler:
            { response in


                // Now on the processingQueue you created earlier.
                print("THREAD: \(Thread.current) is main thread: \(Thread.isMainThread)")

                switch response.result {
                case .success:

                    if data.profile {
                        DispatchQueue.main.async {
                            HUD.show(.labeledProgress(title: "Getting Profile", subtitle: ""))
                        }
                        let userProfile = parseProfile(data: response.data!, user: data.user)
                        print(userProfile)
                    }

                    if data.log {
                        DispatchQueue.main.async {
                            HUD.show(.labeledProgress(title: "Getting Log", subtitle: ""))
                        }
                        fetchLog()
                    }

                    if data.records {
                        DispatchQueue.main.async {
                            HUD.show(.labeledProgress(title: "Getting Records", subtitle: ""))
                        }
                        fetchRecords(recordsToFetch: data.recordsToFetch)
                    }

                    if data.times {
                        DispatchQueue.main.async {
                            HUD.show(.labeledProgress(title: "Getting Times", subtitle: ""))
                        }
                        print("Fetching times is not implemented yet")
                    }

                    DispatchQueue.main.async {
                        HUD.flash(.success)
                    }


                case .failure(let error):
                    HUD.flash(.error)
                    print("Alamofire request failed")
                    print(error)
                }
        }
        )


    } else {
        //Just fetch - no need to authenticate first
        if data.records {
            DispatchQueue.main.async {
                HUD.show(.labeledProgress(title: "Getting Records", subtitle: ""))
            }
            fetchRecords(recordsToFetch: data.recordsToFetch)
        }

        if data.times {
            print("Fetching times is not implemented yet")
        }

        DispatchQueue.main.async {
            HUD.flash(.success)
        }
    }

}

static func fetchRecords(recordsToFetch: RecordsToFetch) {

    for province in recordsToFetch.provinces {
        for ageGroup in recordsToFetch.ageGroups {
            for gender in recordsToFetch.genders {

                DispatchQueue.main.async {
                    HUD.show(.labeledProgress(title: "Getting Records", subtitle: "\(province) - \(gender+Helpers.getAgeGroupFromAge(age: Int(ageGroup)!))"))
                }

                let requestUrl = "\(baseUrl)/Records.jsp"
                let parameters = ["locale": "en", "province": province, "age": ageGroup, "gender": gender, "course": "*"]

                Alamofire.request(requestUrl, method: .post, parameters: parameters).responseData(
                    queue: processingQueue,
                    completionHandler: { response in

                        switch response.result {
                        case .success:

                            let recordArray = parseRecords(data: response.data!, province: province, ageGroup: ageGroup, gender: gender)

                        case .failure(let error):
                            DispatchQueue.main.async {
                                HUD.flash(.failure)
                            }
                            print("Alamofire request failed")
                            print(error)
                        }
                }
                )
            }
        }
    }
}

static func fetchLog() {

    let requestUrl = "\(baseUrl)/ViewLog.jsp"

    Alamofire.request(requestUrl).responseData(
        queue: processingQueue,
        completionHandler: { response in

            switch response.result {
            case .success:
                let log = parseLog(data: response.data!)

            case .failure(let error):
                DispatchQueue.main.async {
                    HUD.flash(.failure)
                }
                print("Alamofire request failed")
            }
        }
    )
}

// MARK: - Convenience structs
struct MscRequest {
    let profile: Bool
    let log: Bool
    let times: Bool
    let records: Bool
    let recordsToFetch: RecordsToFetch
    let user: MscUser

    let parentView: UITableViewController
}

Under this setup, I would setup a MscRequest in a TableViewController and launch a series a requests like so:

let myData = MscRequest.init(
  profile: true,
  log: true,
  times: false,
  records: true,
  recordsToFetch: RecordsToFetch.init(
    provinces: ["NB", "CA"],
    ageGroups: ["20", "25", "30", "35", "40"],
    genders: ["M", "F"]),
  user: MscUser.init(
    username: "SomeUserName",
    password: "SomePassword"),
  parentView: self
)

MyMSCProvider.fetchData(data: myData)

With this setup, all the HUD updates are done at the same time (on the main thread) and end up being dismissed while the background fetching and parsing is still going on. Not exactly what I was going for...

I tried various iterations (with or without a custom queue), I also tried the HTML request code straight from Alamofire's manual (which omits the completionHandler part) but I still get the same results...

I also had a look at Grand Central Dispatch tutorials (such as this one: http://www.appcoda.com/grand-central-dispatch/) but I haven't figured out how to apply info when using Alamofire...

Of note, I managed to make this work in Objective-C with manual NSURLRequests back then. I'm modernizing this old application to Swift 3 and thought I should give Alamofire a try.

Can't help to feel like I am missing something obvious... Any tips?


Solution

  • Ok, I found a way to do what I want using a DispatchGroup (Swift 3, Alamofire 4.x)

    func fetchData() {
        let requestGroup =  DispatchGroup()
    
        //Need as many of these statements as you have Alamofire.requests
        requestGroup.enter()
        requestGroup.enter()
        requestGroup.enter()
    
        Alamofire.request("http://httpbin.org/get").responseData { response in
            print("DEBUG: FIRST Request")
            requestGroup.leave()
        }
    
        Alamofire.request("http://httpbin.org/get").responseData { response in
             print("DEBUG: SECOND Request")
             requestGroup.leave()
        }
    
        Alamofire.request("http://httpbin.org/get").responseData { response in
             print("DEBUG: THIRD Request")
             requestGroup.leave()
        }
    
        //This only gets executed once all the above are done
        requestGroup.notify(queue: DispatchQueue.main, execute: {
            // Hide HUD, refresh data, etc.
             print("DEBUG: all Done")
        })
    
    }