Search code examples
swiftswiftuiviewmodelcompletionhandler

SwiftUI - How to call function with completion from view model


I have a form that gets a zip code and passes it to a method. The method looks up the city and state and returns the information as a tuple. The method also has a completion handler so that the rest of the form data isn't saved until the city and state are found.

I moved the function to a view model to keep things in order, but now I'm confused. First of all, the completion handler has to be called before the function returns, even though that seems exactly the opposite of what I want. I want to get the city and state values and then I want to save them through the handler. The second half of my confusion is in the calling of the function. If I assign a variable like cityState to receive the tuple from my method, the saving happens in a closure which I can't get to! (See Settings View code below.)

I could move everything back to the view where it was working just fine. But I'm trying to understand how MVVM is supposed to work.

Settings View Model

func getCityStateFromPostalCode(zip: String, completion: @escaping () -> ()) -> (String, String) {
   let geocoder = CLGeocoder()
   var city = ""
   var state = ""
        
   geocoder.geocodeAddressString(zip) { (placemarks, error) in
      if let placemark = placemarks?[0] {
         if placemark.postalCode == zip {
            city = placemark.locality!
            state = placemark.administrativeArea!
         }
      }
      completion()
   }
   return (city, state)
} 

SettingsView

let settingsVM = SettingsViewModel()

let cityState = settingsVM.getCityStateFromPostalCode(zip: companyZip) {
   let newCompany = Company(context: self.moc)
      newCompany.id = UUID()
      newCompany.city = ?? //can't use cityState here.
      newCompany.state = ?? // or here!
}

Solution

  • Return city and state in the completion handler, not in the return:

    func getCityStateFromPostalCode(zip: String, completion: @escaping ((String, String)) -> ()) {
       let geocoder = CLGeocoder()
       var city = ""
       var state = ""
            
       geocoder.geocodeAddressString(zip) { (placemarks, error) in
          if let placemark = placemarks?[0] {
             if placemark.postalCode == zip {
                city = placemark.locality!
                state = placemark.administrativeArea!
             }
          }
          completion((city, state))
       }
    } 
    

    Then you can do:

    settingsVM.getCityStateFromPostalCode(zip: companyZip) { city, state in
       let newCompany = Company(context: self.moc)
       newCompany.id = UUID()
       newCompany.city = city
       newCompany.state = state
    }