swiftswiftuicompletionhandler

How to use the result of a completionHandler for the destination of a NavigationLink in Swift?


I have a function which calls a REST API and returns the result via a completionHandler.

I want to call this function when pressing a NavigationLink but use the result as the object passed into the destination. Can't quite figure out how to do this, or if it's even possible. Here's my current code:

The REST function

   func postProgramme(programmeName: String, programmeDays: Int, programmeDescription: String, completeionHandler: @escaping ProgrammeCompletionHandler) {
        struct PostRoutineData: Codable {
            let programmeName: String
            let programmeDays: Int
            let programmeDescription: String
        }
        
        let postProgrammeData = PostRoutineData(programmeName: programmeName, programmeDays: programmeDays, programmeDescription: programmeDescription)
        
        do {
            let jsonData = try JSONEncoder().encode(postProgrammeData)
            let jsonString = String(data: jsonData, encoding: .utf8)!
            let request = RESTRequest(path: "/workout/programme", body: jsonString.data(using: .utf8))
            Amplify.API.post(request: request) { result in
                switch result {
                case .success(let data):
                    do {
                        //let str = String(decoding: data, as: UTF8.self)
                        let programme = try self.decoder.decode(Programme.self, from: data)
                        completeionHandler(programme)
                    } catch {
                        print("[ERROR] Error within postProgrammes()", error)
                    }
                case .failure(let error):
                    print("[ERROR] Error within postProgrammes()", error)
                }
            }
        } catch {
            print("[ERROR] Error within postProgramme()", error)
        }
    }

The NavigationLink, and my attempt to figure the problem out:

NavigationLink(destination: ProgrammeDetailView(), isActive: $shouldTransit) {
    Text("Create Programme")
         .onTapGesture {
              self.createNewProgramme()
              self.shouldTransit = true
    }
}
func createNewProgramme() -> ProgrammeDetailView {
        sessionManager.postProgramme(programmeName: programmeName, programmeDays: programmeDays, programmeDescription: programmeDescription, completeionHandler: {(programme) -> ProgrammeDetailView in
            return ProgrammeDetailView(programme: programme)}
    }

Solution

  • Assuming it's an iOS app, if you can deploy for iOS 15, you can use the recent async/ await environment, by using an async function and returning a Programme, instead of using a completion handler.

    1. Turn your function into async and return a Programme:
       func postProgramme(programmeName: String, programmeDays: Int, programmeDescription: String) async -> Programme? {
    
            // ...
            
                    case .success(let data):
                        do {
                            let programme = try self.decoder.decode(Programme.self, from: data)
    
                            // No completion handler: return a Programme
                            // completionHandler(programme)
                            return programme
                        } catch {
                            print("[ERROR] Error within postProgrammes()", error)
    
                            // Return nil everywhere else
                            return nil
                        }
    
    1. In your main view, use a @State var of type Programme?, that will be binding to another variable in ProgrammeDetailView.

    The function createNewProgramme() will update the state variable.

    @State private var programme: Programme? = nil
    
    var body: some View {
        NavigationView {
    
            // Pass the binding to ProgrammeDetailView
            NavigationLink(destination: ProgrammeDetailView(programme: $programme, content: { programme in
                      // A customised view
                      Text(programme?.name ?? "")
                }), isActive: $shouldTransit) {
                Text("Create Programme")
                     .onTapGesture {
                         self.createNewProgramme()
                         self.shouldTransit = true
                     }
            }
        }
    
    func createNewProgramme() {
    
        // Task will allow working with async functions
        Task {
            let programme = await sessionManager.postProgramme(programmeName: programmeName, programmeDays: programmeDays, programmeDescription: programmeDescription)
    
            // Back to main thread to update the UI
            DispatchQueue.main.async {
                 self.programme = programme
            }
        }
    }
    
    1. Remember to create the @Binding variable in ProgrammeDetailView. The example below can receive any view as a parameter, but because the REST API will take some time to respond, you need to handle the case where programme == nil:
    struct ProgrammeDetailView<V: View> : View {
    
        // The binding with the parent view
        @Binding var programme: Programme?
    
        // Your customised view that receives a Programme as a parameter
        let content: (Programme)->V
    
        var body: some View {
            if programme == nil {
                 ProgressView()
            } else {
                 content(programme!)
            }
        }
    }