Home > Mobile >  Handling asynchronous view controller retrieval in SwiftUI
Handling asynchronous view controller retrieval in SwiftUI

Time:06-01

I have a flow in UIKit where, when I call a function, I retrieve a response from my API, this response is then used to create a UIViewController. Then this view controller is presented full screen:

getResponse() { result in
  switch result {
    case .success(let response):
       let viewController = MyViewController(response: response)
       
       presenter.present(viewController) { success in
         // etc
       }
    case .failure:
      break
  }
}

I want to implement the equivalent of this in SwiftUI using a view modifier to abstract this:

.showMyView(isPresented: $isPresented)

When isPresented is true, it should get the response then present MyView. I have bits in my head of what I think I need to use, but am not sure how to piece them together.

I know that I need to use a UIViewControllerRepresentable to handle the creation of a SwiftUI version of MyViewController:

struct MyView: UIViewControllerRepresentable {
  var response: MyResponse

  func makeUIViewController(context: Context) -> MyViewController {
    return MyViewController(response: response)
  }
}

I'm assuming I can pass the response in. However, how do I handle the asynchronousity of it with respect to the body of the view modifier?

struct ShowMyView: ViewModifier {
  @Binding var isPresented: Bool

  func body(content: Content) -> some View {
    content
      .sheet(isPresented: $isPresented) {
        // something that returns MyView here?
      }
      .onReceive(Just(isPresented)) { _ in
        getMyResponse()
      }
  }

  func getMyResponse() {
    getResponse() { result in
      switch result {
      case .success(let response):
        // Something needs to happen here
      case .failure:
        break
      }
    }
  }
}

I couldn't figure it out. Any help appreciated!

CodePudding user response:

You need internal state, like (typed here, so some typos/errors might needed to be fixed)

struct ShowMyView: ViewModifier {
  @Binding var isPresented: Bool

  @State private var result: MyResponse? = nil

  func body(content: Content) -> some View {
    content
      .sheet(item: $result) {  // MyResponse needed to be Identifiable
         switch $0 {
         case .success(let response):
             MyView(response: response)         
         case .failure:
             // something appropriate here, e.g. ErrorView()
           break
         }
      }
      .onReceive(Just(isPresented)) { _ in
        getMyResponse()
      }
  }

  func getMyResponse() {
    getResponse() { result in
       DispatchQueue.main.async {
          self.result = result   // update UI on main queue
       }
    }
  }
}

CodePudding user response:

I think this is what you want:

struct ShowMyView: ViewModifier {
  @Binding var shouldPresent: Bool

  @State private var response: MyResponse? = nil

  private var isPresentedBinding: Binding<Bool> {
      return Binding(
          get: { shouldPresent && response != nil },
          set: { shouldPresent = $0 }
      )
  }

  func body(content: Content) -> some View {
    content
          .sheet(isPresented: isPresentedBinding) {
              if let response = response {
                  MyView(response: response)
              }
          }
          .onChange(of: shouldPresent) {
              if $0 && response == nil {
                  getMyResponse()
              }
          }
  }

  func getMyResponse() {
    getResponse() { result in
      switch result {
      case .success(let response):
          self.response = response
      case .failure:
        break
      }
    }
  }
}
  • Related