Elliot Sylvén
Elliot Sylvén

Reputation: 59

How to wait for function to end in SwiftUI?

Basically, the first two functions (FetchOriginCoordinates and FetchDestCoordinates) take a street name as an input and return the coordinates for it. The coordinates are then used to find a nearby bus stop in the functions FetchOriginID and FetchDestID. These functions return the bus stop ID which is then used in the last function (FetchTrip). In URL used in that functions has four modifiable parameters: originExtId, destExtId, date, and time. The originExtId and destExtId are the values fetched from FetchOriginID and FetchDestID. The date and time parameters correspond to the variables arrivalTime and travelDate.

I only call the FetchTrip function when the Button in the body is pressed. The first time the function is called the variables: self.originLat, self.originLon, self.destLat, self.destLon, self.originID and self.destID are all nil. The second time self.originID and self.destID are nil. And the third time the function works (returns the correct value).

I'm new to swift so I'm not entirely sure why this is caused. But I do believe it's because the nested function continues running without waiting for the other functions to finish. I, therefore, think it has something to do with Closures, but I'm not fully sure how to apply it. Could someone help me?

@State var arrivalTime = String()
@State var travelDate = String()
@State var originInput = String()
@State var destInput = String()
    
@State var originLat = String()
@State var originLon = String()
@State var destLat = String()
@State var destLon = String()
    
@State var originID = String()
@State var destID = String()
 
@State var destName = String()
@State var destTime = String()
@State var originTime = String()
@State var originName = String()
    
@State var Trips = [String]()
@State var tripIndex = Int()
 
var body: some View {
        VStack {
            List(Trips, id: \.self) { string in
                        Text(string)
                    }
            TextField("From", text: $originInput).padding()
            TextField("To", text: $destInput).padding()
            TextField("00:00", text: $arrivalTime).padding()
            TextField("yyyy-mm-dd", text: $travelDate).padding()
            Button("FetchAPI"){FetchTrip()}.padding()
        }
    }
    
    
    func FetchOriginCoordinates(completion: @escaping ([NominationStructure]) -> ()) {
        let locationUrl = URL(string: "https://nominatim.openstreetmap.org/search?country=Sweden&city=Stockholm&street=\(self.originInput)&format=json")
        print(locationUrl)
        
        URLSession.shared.dataTask(with: locationUrl!) {data, response, error in
            if let data = data {
                do {
                    if let decodedJson = try? JSONDecoder().decode([NominationStructure].self, from: data) {
                        
                        DispatchQueue.main.async {
                            completion (decodedJson)
                        }
                    }
                } catch {
                    print(error)
                }
            }
        }.resume()
    }
    
    func FetchDestCoordinates(completion: @escaping ([NominationStructure]) -> ()) {
        // Same as the function above but using self.destInput instead of self.originInput
    }
    
    
    func FetchOriginID(completion: @escaping (NearbyStopsStructure) -> ()) {
        DispatchQueue.main.async {
            FetchOriginCoordinates { (cors) in
                self.originLat = cors[0].lat
                self.originLon = cors[0].lon
            }
        }
        
        let nearbyStopsKey = "MY API KEY"
        let stopIDUrl = URL(string: "http://api.sl.se/api2/nearbystopsv2.json?key=\(nearbyStopsKey)&originCoordLat=\(self.originLat)&originCoordLong=\(self.originLon)&maxNo=1")
        print(stopIDUrl)
        
        URLSession.shared.dataTask(with: stopIDUrl!) {data, response, error in
            if let data = data {
                do {
                    if let decodedJson = try? JSONDecoder().decode(NearbyStopsStructure.self, from: data) {
                        
                        DispatchQueue.main.async {
                            completion (decodedJson)
                        }
                    }
                } catch {
                    print(error)
                }
            }
        }.resume()
    }
    
    func FetchDestID(completion: @escaping (NearbyStopsStructure) -> ()) {
        // Same as the function above but using self.destLat and self.destLon instead of self.originLat and self.originLon
    }
    
    
    func FetchTrip() {
        DispatchQueue.main.async {
            FetchOriginID { (stops) in
                self.originID = stops.stopLocationOrCoordLocation[0].StopLocation.mainMastExtId
            }
            
            FetchDestID { (stops) in
                self.destID = stops.stopLocationOrCoordLocation[0].StopLocation.mainMastExtId
            }
        }
        
        let tripKey = "MY API KEY"
        let tripUrl = URL(string: "http://api.sl.se/api2/TravelplannerV3_1/trip.json?key=\(tripKey)&originExtId=\(self.originID)&destExtId=\(self.destID)&Date=\(self.travelDate)&Time=\(self.arrivalTime)&searchForArrival=1")
        print(tripUrl)
 
        // Logic here
    }

Upvotes: 1

Views: 905

Answers (1)

lorem ipsum
lorem ipsum

Reputation: 29676

One way to do it is via DispatchGroup but since you have quite a few different steps it might be simpler if you just trigger the next step when you are done with what you are working on.

//Just to condense the information /make it reusable
struct LocationInfo {
    var iD = String()
    var input = String()
    var lat = String()
    var lon = String()
    var name = String()
    var time = String()
}
//One way is to call your functions when you know a step is completed
enum TripFetchStatus: String {
    case start
    case fetchedOriginCoordinates
    case fetchedOriginId
    case fetchedDestinationCoordinates
    case fetchedDestinationId
    case fetchingTrip
    case done
    case stopped
}

class TripViewModel: ObservableObject {
    //Apple frowns upon frezzing a screen so having a way to update the user on what is going on
    //When a step is complete have it do something else
    @Published var fetchStatus: TripFetchStatus = .stopped{
        didSet{
            switch fetchStatus {
            case .start:
                FetchOriginCoordinates { (cors) in
                    self.origin.lat = cors[0].latitude.description
                    self.origin.lon = cors[0].longitude.description
                    self.fetchStatus = .fetchedOriginCoordinates
                }
            case .fetchedOriginCoordinates:
                self.FetchOriginID { (stops) in
                    self.origin.iD = stops
                    self.fetchStatus = .fetchedOriginId
                }
            case .fetchedOriginId:
                FetchDestCoordinates { (cors) in
                    self.dest.lat = cors[0].latitude.description
                    self.dest.lon = cors[0].longitude.description
                    self.fetchStatus = .fetchedDestinationCoordinates
                }
            case .fetchedDestinationCoordinates:
                self.FetchDestID { (stops) in
                    self.dest.iD = stops
                    self.fetchStatus = .fetchedDestinationId
                }
            case .fetchedDestinationId:
                //Once you have everthing in place then go to the next API
                FetchTrip()
            case .fetchingTrip:
                print("almost done")
            case .done:
                print("any other code you need to do")
            case .stopped:
                print("just a filler use this for errors or other things that deserve a halt")
                
            }
        }
    }
    
    @Published var dest: LocationInfo = LocationInfo()
    @Published var origin: LocationInfo = LocationInfo()
    @Published var arrivalTime = String()
    @Published var travelDate = String()
    
    
    
    func FetchTrip() {
        //Timers are to mimic waiting on a URLResponse
        Timer.scheduledTimer(withTimeInterval: 3.0, repeats: false) {_ in
            
            self.fetchStatus = .fetchingTrip
            
            Timer.scheduledTimer(withTimeInterval: 3.0, repeats: false) {_ in
                self.fetchStatus = .done
            }
        }
        
    }
    //Simple version just to replicate put your code within
    private func FetchOriginCoordinates(completion: @escaping ([CLLocationCoordinate2D]) -> ()) {
        //Timers are to mimic waiting on a URLResponse
        Timer.scheduledTimer(withTimeInterval: 3.0, repeats: false) {_ in
            
            completion([CLLocationCoordinate2D()])
        }
    }
    
    private func FetchDestCoordinates(completion: @escaping ([CLLocationCoordinate2D]) -> ()) {
        //Timers are to mimic waiting on a URLResponse
        Timer.scheduledTimer(withTimeInterval: 3.0, repeats: false) {_ in
            completion([CLLocationCoordinate2D()])
        }
    }
    private func FetchOriginID(completion: @escaping (String) -> ()) {
        //Timers are to mimic waiting on a URLResponse
        Timer.scheduledTimer(withTimeInterval: 3.0, repeats: false) {_ in
            completion(UUID().uuidString)
        }
    }
    private func FetchDestID(completion: @escaping (String) -> ()) {
        //Timers are to mimic waiting on a URLResponse
        Timer.scheduledTimer(withTimeInterval: 3.0, repeats: false) {_ in
            completion(UUID().uuidString)
        }
    }
    
}


struct TripView: View {
    //Code that does work should never live in a View you want to be able to reuse it
    @StateObject var vm: TripViewModel = TripViewModel()
    
    @State var Trips = [String]()
    @State var tripIndex = Int()
    
    var body: some View {
        ZStack{
            VStack {
                List(Trips, id: \.self) { string in
                    Text(string)
                }
                TextField("From", text: $vm.origin.input).padding()
                TextField("To", text: $vm.dest.input).padding()
                TextField("00:00", text: $vm.arrivalTime).padding()
                TextField("yyyy-mm-dd", text: $vm.travelDate).padding()
                Button("FetchAPI"){
                    vm.fetchStatus = .start
                    
                }.padding()
            }
            .disabled(vm.fetchStatus != .stopped && vm.fetchStatus != .done)
            if vm.fetchStatus != .stopped && vm.fetchStatus != .done{
                VStack{
                    ProgressView()
                    //You dont have to show the user all this
                    Text(vm.fetchStatus.rawValue)
                    Text("lat = \(vm.origin.lat)  lon = \(vm.origin.lon)")
                    Text("id = \(vm.origin.iD)")
                    Text("lat = \(vm.dest.lat)  lon = \(vm.dest.lon)")
                    Text("id = \(vm.dest.iD)")
                }
            }
        }
    }
}

Upvotes: 2

Related Questions