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

Time:05-08

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)}
    }

CodePudding user response:

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!)
        }
    }
}
  • Related