Search code examples
iosswiftasynchronousalamofirecompletionhandler

How to display in a View a returned value from a completion handler in Swift?


I have a class that has these two methods:

private func send(method: String, path: String, code: Array<Int>, headers: HTTPHeaders, completionHandler: @escaping (Int) -> Void) {
        let url: String = "\(self.credentials.url)/\(path)"

        AF.request(url, method: HTTPMethod(rawValue: method), headers: headers)
            .authenticate(with: self.request_credentials)
            .response { response in
                let status_code: Int = response.response!.statusCode
                
                completionHandler(status_code as Int)
            }
}

And

func list_files(path: String) {
        let headers: HTTPHeaders = [
            "Depth": "1"
        ]
        
        send(method: "PROPFIND", path: path, code: [207, 301], headers: headers) { status_code in
            self.status_code = String(status_code)
        }
}

So both of these functions use completion handlers because of the fact that Alamofire uses them in its process of making HTTP requests.
I understood that I have to use completion handlers this way to handle this async data.

My current issue is that now I have to display this data (let's say self.status_code) in a view but I have no idea how to do this. When I display it like this :

struct ContentView: View {
    var body: some View {
        
        let auth = Authentication(username: "****",
                                  domain: "****",
                                  password: "****",
                                  port: ****,
                                  proto: "****",
                                  path:"****")
        
        let commands = Commands(credentials: auth.get_credentials())
        
        let _ = commands.list_files(path: "/")
        
        Text(commands.status_code)
            .padding()
    }
}

It will display the initialization value of status_code (which is 0) instead of the updated value by list_files method. I know this is because of the asynchronous behavior of these completion handlers and the value is not yet updated when I display it.

But here's the question: how can I manage to properly display this updated value to the user?

I'm probably doing lots of things wrong here and I don't mind receiving a completely different solution since I'm willing to follow the best practices.

Thank you.


Solution

  • Below is a Playground with a complete example. I'll walk through some of the important things to note.

    First I simplified the "send" method since I don't have all the types and things from your original example. This send will wait 3 seconds then call the completion handler with whatever message you give it.

    Inside the view, when the button is pushed, we call "send". Then, in the completion handler you'll notice:

    DispatchQueue.main.async {
        message = msg
    }
    

    I don't know what thread the Timer in send is going to use to call my completion handler. But UI updates need to happen on the main thread, so the DispatchQueue.main... construct will ensure that the UI update (setting message to msg) will happen on the main thread.

    import UIKit
    import SwiftUI
    import PlaygroundSupport
    
    func send(message: String, completionHandler: @escaping (String) -> Void) {
        Timer.scheduledTimer(withTimeInterval: 3.0, repeats: false) { timer in
            completionHandler(message)
            timer.invalidate()
        }
    }
    
    struct ContentView: View {
        @State var message : String = ""
    
        var body: some View {
            print("Building view")
            return VStack {
                TextField("blah", text: $message)
                Button("Push Me", action: {
                    send(message: "Message Received") { msg in
                        DispatchQueue.main.async {
                            message = msg
                        }
                    }
                })
            }.frame(width: 320, height: 480, alignment: .center)
        }
    }
    
    
    let myView = ContentView()
    let host = UIHostingController(rootView: myView)
    PlaygroundSupport.PlaygroundPage.current.liveView = host