swiftfunctioncore-locationcllocationswift-concurrency

How to Display the async/throw functions return value inside the textView?


This is the function I am calling, which uses the latitude and longitude, converts them into city name and country name, and then returns city name or country name depending on what I want.

import SwiftUI
import CoreLocation


struct junoWeatherEntryView: View {
    
    @ObservedObject var cityVM = allCities()
    @State private var searchTerm = "San Francisco"

    var body: some View {
        VStack{
            search
            Spacer()
            ForEach(cityVM.weather){ item in
                Text("\( try await reverseGeocode(lat: item.lat ,lan:item.lan).locality ?? "Unkown")")
            }
        }
    }
    
    func reverseGeocode(lat: Double, lan: Double) async throws -> CLPlacemark {
        let geoCoder = CLGeocoder()
        let location = CLLocation(latitude: lat, longitude: lan) // <- New York

        return try await withCheckedThrowingContinuation { continuation in
            geoCoder.reverseGeocodeLocation(location) { (placemarks, error) in
                guard
                    error == nil,
                    let placemark = placemarks?.first
                else {
                    continuation.resume(throwing: error ?? CLError(.geocodeFoundNoResult))
                    return
                }

                continuation.resume(returning: placemark)
            }
        }
    }
}

here cityVM is @Published var weather = [WeatherResponse]()

Error i am getting is

1.Cannot pass function of type '(WeatherResponse) async throws -> Text' to parameter expecting synchronous function type
2.Invalid conversion from throwing function of type '(WeatherResponse) async throws -> Text' to non-throwing function type '(Array<WeatherResponse>.Element) -> Text'

I want to show the return value in the TextView how can I achieve this ?


Solution

  • The reverseGeocodeLocation calls its completion handler asynchronously (i.e., later). So, there are two common patterns:

    1. Use traditional completion handler patterns. E.g.:

      func reverseGeocode(lat: Double, lon: Double, completion: @escaping (Result<CLPlacemark, Error>) -> Void) {
          let geocoder = CLGeocoder()
          let location = CLLocation(latitude: lat, longitude: lon) // <- New York
      
          geocoder.reverseGeocodeLocation(location) { (placemarks, error) in
              guard
                  error == nil,
                  let placemark = placemarks?.first
              else {
                  completion(.failure(error ?? CLError(.geocodeFoundNoResult)))
                  return
              }
      
              completion(.success(placemark))
          }
      }
      

      And you would call it like so:

      reverseGeocode(lat: 31, lon: 32) { result in
          switch result {
          case .failure(let error):     print(error)
          case .success(let placemark): print(placemark.locality ?? "Unknown")
          }
      }
      
    2. Use modern Swift concurrency, e.g.,

      func reverseGeocode(lat: Double, lon: Double) async throws -> CLPlacemark {
          let geocoder = CLGeocoder()
          let location = CLLocation(latitude: lat, longitude: lon) // <- New York
      
          guard let placemark = try await geocoder.reverseGeocodeLocation(location).first else {
              throw CLError(.geocodeFoundNoResult)
          }
      
          return placemark
      }
      

      And you would call it like so:

      Task {
          var placemark = try await reverseGeocode(lat: 31, lon: 32)
          print(placemark.locality ?? "Unknown")
      }
      

    Now, in both of those examples, I am returning the entire CLPlacemark. You could change these to return the String of just locality or county based upon your cityName Boolean, but the basic idea would be the same. Use completion handler or async pattern to handle the return of the asynchronously retrieved information.


    In your revised question, you ask for an example of how to use this in SwiftUI. You could, for example, use .task { ... }:

    struct ContentView: View {
        @ObservedObject var viewModel = CityViewModel()
    
        var body: some View {
            VStack {
                Spacer()
                ForEach(viewModel.results) { result in
                    Text(result.name)
                }
                Spacer()
            }
            .padding()
            .task {
                try? await viewModel.search()
            }
        }
    }
    

    And I'd put the business logic in the view model, not the view:

    @MainActor
    class CityViewModel: ObservableObject {
        var coordinates = [
            CLLocationCoordinate2D(latitude: 31, longitude: 32),
            CLLocationCoordinate2D(latitude: 40.7, longitude: -74),
            CLLocationCoordinate2D(latitude: 34.05, longitude: -118.25)
        ]
    
        @Published var results: [CityResult] = []
    
        private let geocoder = CLGeocoder()
    
        func search() async throws {
            for coordinate in coordinates {
                let placemark = try await reverseGeocode(coordinate)
                results.append(CityResult(name: placemark.locality ?? "Not found"))
            }
        }
    
        private func reverseGeocode(_ coordinate: CLLocationCoordinate2D) async throws -> CLPlacemark {
            let location = CLLocation(latitude: coordinate.latitude, longitude: coordinate.longitude)
    
            guard let placemark = try await geocoder.reverseGeocodeLocation(location).first else {
                throw CLError(.geocodeFoundNoResult)
            }
    
            return placemark
        }
    }
    
    struct CityResult: Identifiable {
        var id: String { name }
        let name: String
    }
    

    But don't get lost too in the details here, as your example will undoubtedly vary. The idea is to use .task to start the asynchronous task, which can update some observable property.