I am using the google maps distance matrix api and using a compeletion handler to pass my async function calls like this:
func configureRoute(origin:String,destination:String, completionHandler: #escaping (_ duration:Int) -> ()){
let jsonURL = "https://maps.googleapis.com/maps/api/distancematrix/json?units=imperial&origins=place_id:\(origin)&destinations=place_id:\(destination)&key=MYAPI"
guard let url = URL(string: jsonURL ) else {return}
print(jsonURL)
URLSession.shared.dataTask(with: url) { (data, response, err) in
guard let data = data else {return}
do {
let route = try JSONDecoder().decode(Welcome.self, from: data)
// print(self.durations)
completionHandler(route.rows[0].elements[0].duration.value)
}
catch let jsonErr {
}
let dataAsString = String(data: data, encoding: .utf8)
}.resume()
}
and then I try and use that result like so, however my output results are still blank and I am unable to use any of the calls that I have received. I am not that good with compeltion handlers, so if anyone could let me know what I have done wrong?
func majorConfigure() {
permutations(placeID.count, &placeID, origin: startPlaceID, destination: endPlaceID)
for eachArray in finalRoutes {
for i in 0..<(eachArray.count-1) {
configureRoute(origin: eachArray[i], destination: eachArray[i+1]){
duration in
self.durations.append(duration)
}
}
groupedDurations.append(durations)
durations.removeAll()
}
print(groupedDurations)
}
After using Dispatch group this is my updated code:
func majorConfigure(){
permutations(placeID.count, &placeID, origin: startPlaceID, destination: endPlaceID)
let dispatchGroup = DispatchGroup()
for eachArray in finalRoutes{
for i in 0..<(eachArray.count-1){
dispatchGroup.enter()
configureRoute(origin: eachArray[i], destination: eachArray[i+1]){
duration in
self.durations.append(duration)
print(self.groupedDurations)
dispatchGroup.leave()
}
}
dispatchGroup.notify(queue: .main){
self.groupedDurations.append(self.durations)
self.durations.removeAll()
}
}
}
The result when I print groupedDurations is:
[]
[]
[]
[]
[]
[]
give this a try :
func majorConfigure() {
permutations(placeID.count, &placeID, origin: startPlaceID, destination: endPlaceID)
for eachArray in finalRoutes {
for i in 0..<(eachArray.count-1) {
configureRoute(origin: eachArray[i], destination: eachArray[i+1]){
duration in
self.durations.append(duration)
if i == eachArray.count - 1{
groupedDurations.append(durations)
}
if i == eachArray.count - 1 && eachArray == finalRoutes.last! {
print(groupedDurations)
durations.removeAll()
}
}
}
}
}
EDITED ANSWER
You can achieve this in this manner :
var indexFinalRoutes = 0
var indexEachArray = 0
func loopFinalRoutes(){
self.indexEachArray = 0
let eachArray = finalRoutes[indexFinalRoutes]
loopEachArray(eachArray: eachArray) { (success) in
self.indexFinalRoutes = self.indexFinalRoutes + 1
if self.indexFinalRoutes < self.finalRoutes.count - 1{
self.loopFinalRoutes()
}else{
self.indexFinalRoutes = 0
print(self.groupedDurations)
}
}
}
func loopEachArray(eachArray : [String] ,
completion: #escaping (_ success:Bool) -> Void){
if indexEachArray + 1 < eachArray.count - 1 {
configureRoute(origin: eachArray[indexEachArray], destination: eachArray[indexEachArray+1]){
duration in
self.durations.append(duration)
self.indexEachArray = self.indexEachArray + 1
let nextEachArray = self.finalRoutes[self.indexFinalRoutes]
self.loopEachArray(eachArray: nextEachArray, completion: completion)
}
}else{
groupedDurations.append(durations)
durations.removeAll()
completion(true)
}
}
call loopFinalRoutes functuin from your majorConfigure function and you will have the output.
Related
I'm making a history of today app in IOS.
the code is as follows:
import Foundation
import UIKit
final class HistoryAPICaller {
static let shared = HistoryAPICaller()
struct Constants
{
static let topHeadLinesURL = URL(string:"http://history.muffinlabs.com/date/6/3")
static let searchUrlString = "http://history.muffinlabs.com/date/6/3"
}
private init() {}
public func getTopStories(completion: #escaping (Result<[Events], Error>) -> Void) {
guard let html = Constants.topHeadLinesURL else {
return
}
let task = URLSession.shared.dataTask(with: html) { data, _, error in
if let error = error {
completion(.failure(error))
}
else if let data = data {
do {
let result = try JSONDecoder().decode(HistoryAPIResponse.self, from: data)
print("Articles: \(result.data.Events.count)")
completion(.success(result.data.Events))
}
catch {
completion(.failure(error))
}
}
}
task.resume()
}
public func search(with query: String, completion: #escaping (Result<[Events], Error>) -> Void) {
guard !query.trimmingCharacters(in: .whitespaces).isEmpty else {
return
}
let urltring = Constants.searchUrlString + query
guard let html = URL(string:urltring) else {
return
}
let task = URLSession.shared.dataTask(with: html) { data, _, error in
if let error = error {
completion ( .failure(error))
}
else if let data = data {
do {
let result = try JSONDecoder().decode(HistoryAPIResponse.self, from: data)
print("Articles: \(result.data.Events.count)")
completion(.success(result.data.Events))
}
catch {
completion (.failure(error))
}
}
}
task.resume()
}
}
struct HistoryAPIResponse: Codable {
let data: HistoryDataAPIResponse }
struct HistoryDataAPIResponse: Codable {
let Events: [Events]
}
struct Events: Codable {
let year: String
let text: String
let html: String
let no_year_html: String
let links: [Links]
}
struct Links: Codable {
let link: String
}
Now I want to add the current date function to the code, so instead of constantly showing the historic event of June 3rd
"http://history.muffinlabs.com/date/6/3" ,
it will be something like
"http://history.muffinlabs.com/date/(currentMonth)/(currentDay)"
Any assistance will be greatly appreciated.
You can use the dateComponents method to retrieve the current day and month, define this function is your HistoryAPICaller class:
func getSearchUrlString() -> String
{
let dayAndMonth = Calendar.current.dateComponents([.day, .month], from: Date())
guard let day = dayAndMonth.day, let month = dayAndMonth.month else { return "" }
return "http://history.muffinlabs.com/date/\(month)/\(day)"
}
Then edit your search method to retrieve the URL from it:
public func search(with query: String, completion: #escaping (Result<[Events], Error>) -> Void) {
guard !query.trimmingCharacters(in: .whitespaces).isEmpty else {
return
}
let urltring = getSearchUrlString() + query
guard let html = URL(string:urltring) else {
return
}
...
I'm very new to coding and I'm stuck on what to do. I'm trying to get the user's geocoordinates from an address, use the coordinates to figure out some values then go to a different view controller where some code will be run to display the values that I figured out. The problem is it finds the users coordinates, Then goes to the next view controller where it doesn't have the calculated data needed to display it then tries to calculate the values needed from the first controller. How do I get this code to run in order?
My Code
#IBAction func BSearch(_ sender: UIButton) {
getCoordinate(addressString: AdressInput) { coordinate, error in
if error != nil {
// Error
return
} else {
user_lat = String(format: "%f", coordinate.latitude)
user_long = String(format: "%f", coordinate.longitude) // Program gets this first
self.getData(savedLat: user_lat, savedLong: user_long) // Lastly goes here
}
}
performSegue(withIdentifier: "TimeNavigation", sender: self) // Goes here second
}
The Function
func getCoordinate(addressString: String, completionHandler: #escaping (CLLocationCoordinate2D, NSError?) -> Void ) {
let geocoder = CLGeocoder()
geocoder.geocodeAddressString(addressString) { (placemarks, error) in
if error == nil {
if let placemark = placemarks?[0] {
let location = placemark.location!
completionHandler(location.coordinate, nil)
return
}
}
completionHandler(kCLLocationCoordinate2DInvalid, error as NSError?)
}
}
getData Function
func getData(savedLat: String, savedLong: String) {
guard let url = URL(string: "http://127.0.0.1:5000/api/lat/\(savedLat)/long/\(savedLong)") else{return}
URLSession.shared.dataTask(with: url) { (data, response, err) in
guard let data = data else { return }
var dataAsString = String(data: data, encoding: .utf8)
let splits = dataAsString?.components(separatedBy: "|")
let Counter:Int = splits?.count ?? 0
for n in 0...(Counter-1){
let splits2 = splits?[n].components(separatedBy: ",")
for x in 0...9 {
dataArray[n][x] = String(splits2?[x] ?? "nil")
}
}
}.resume()
}
Write it inside the closure because your performSegue execute before the closure result ... so write it inside the closure but on main thread
Update you getData function
typealias CompletionHandler = (_ success:Bool) -> Void
func getData(savedLat:String,savedLong:String, completionBlock:#escaping CompletionHandler){
guard let url = URL(string: "http://127.0.0.1:5000/api/lat/\(savedLat)/long/\(savedLong)") else{return}
URLSession.shared.dataTask(with: url) { (data, response, err) in
guard let data = data else {
completionBlock(false)
return
}
var dataAsString = String(data: data, encoding: .utf8)
let splits = dataAsString?.components(separatedBy: "|")
let Counter:Int = splits?.count ?? 0
for n in 0...(Counter-1){
let splits2 = splits?[n].components(separatedBy: ",")
for x in 0...9 {
dataArray[n][x] = String(splits2?[x] ?? "nil")
}
completionBlock(true)
}
}.resume()
}
And then your BSearch method
#IBAction func BSearch(_ sender: UIButton) {
getCoordinate(addressString: "AdressInput") { coordinate, error in
if error != nil {
// Error
return
}
else {
user_lat = String(format: "%f", coordinate.latitude)
user_long = String(format: "%f", coordinate.longitude) // Program gets this first
self.getData(savedLat: "user_lat", savedLong: "user_long", completionBlock: {[weak self] success in
DispatchQueue.main.async {
self?.performSegue(withIdentifier: "TimeNavigation", sender: self)
}
}) // Lastly goes here
}
}
}
You are calling your performSegue outside the scope of getCordinate function, which is why its getting called on click of the button and not waiting for the completion handler to finish.
Just move it inside and it will work fine.
#IBAction func BSearch(_ sender: UIButton) {
getCoordinate(addressString: AdressInput) { coordinate, error in
if error != nil {
// Error
return
}
else {
user_lat = String(format: "%f", coordinate.latitude)
user_long = String(format: "%f", coordinate.longitude) // Program gets this first
self.getData(savedLat: user_lat, savedLong: user_long) // Lastly goes here
DispatchQueue.main.async { //when performing UI related task, it should be on main thread
self.performSegue(withIdentifier: "TimeNavigation", sender: self)
}
}
}
}
I want to run 2 pieces of asynchronous code in one function and escape them. I want first to download the Reciter information and then download with these information the images that is associated with the Reciter. I'm using Firestore. I tried to work with DispatchQueue and DispatchGroup but I couldn't figure something out. I hope someone can help me :)
func getReciters(completion: #escaping (Bool) -> Void) {
var reciters = [Reciter]()
self.BASE_URL.collection(REF_RECITERS).getDocuments { (snapchot, error) in
if let error = error {
debugPrint(error)
completion(false)
// TODO ADD UIALTERCONTROLLER MESSAGE
return
}
guard let snapchot = snapchot else { debugPrint("NO SNAPSHOT"); completion(false); return }
for reciter in snapchot.documents {
let data = reciter.data()
let reciterName = data[REF_RECITER_NAME] as? String ?? "ERROR"
let numberOfSurahs = data[REF_NUMBER_OF_SURAHS] as? Int ?? 0
// **HERE I WANT TO DOWNLOAD THE IMAGES**
self.downloadImage(forDocumentID: reciter.documentID, completion: { (image) in
let reciter = Reciter(name: reciterName, image: nil, surahCount: numberOfSurahs, documentID: reciter.documentID)
reciters.append(reciter)
})
}
}
UserDefaults.standard.saveReciters(reciters)
completion(true)
}
You need DispatchGroup.
In the scope of the function declare an instance of DispatchGroup.
In the loop before the asynchronous block call enter.
In the loop inside the completion handler of the asynchronous block call leave.
After the loop call notify, the closure will be executed after all asynchronous tasks have finished.
func getReciters(completion: #escaping (Bool) -> Void) {
var reciters = [Reciter]()
self.BASE_URL.collection(REF_RECITERS).getDocuments { (snapchot, error) in
if let error = error {
debugPrint(error)
completion(false)
// TODO ADD UIALTERCONTROLLER MESSAGE
return
}
guard let snapchot = snapchot else { debugPrint("NO SNAPSHOT"); completion(false); return }
let group = DispatchGroup()
for reciter in snapchot.documents {
let data = reciter.data()
let reciterName = data[REF_RECITER_NAME] as? String ?? "ERROR"
let numberOfSurahs = data[REF_NUMBER_OF_SURAHS] as? Int ?? 0
group.enter()
self.downloadImage(forDocumentID: reciter.documentID, completion: { (image) in
let reciter = Reciter(name: reciterName, image: nil, surahCount: numberOfSurahs, documentID: reciter.documentID)
reciters.append(reciter)
group.leave()
})
}
group.notify(queue: .main) {
UserDefaults.standard.saveReciters(reciters)
completion(true)
}
}
}
I am trying to build methods with completion blocks for nested requests. The issue is that a completion block catches to early for parent requests (meaning that the child requests haven't actually completed yet). So far I haven't found a way for a child request to communicate back to the parent request other than what I've done in my example below (which is to count the amount of child requests have completed and compare it against the expected amount of requests).
The example below is working against a Firestore database. Imagine a user has multiple card games (decks) with each multiple cards. I'm grateful for any help how to build better completion blocks for cases like these:
func fetchCardsCount(uid: String, completion: #escaping (Int) -> ()) {
let db = Firestore.firestore()
var decksCount = Int()
var cardsCount = Int()
db.collection("users").document(uid).collection("decks").getDocuments { (deckSnapshot, err) in
if let err = err {
print("Error fetching decks for user: ", err)
} else {
guard let deckSnapshot = deckSnapshot else { return }
deckSnapshot.documents.forEach({ (deck) in
let dictionary = deck.data() as [String: Any]
let deck = FSDeck(dictionary: dictionary)
db.collection("users").document(uid).collection("decks").document(deck.deckId).collection("cards").getDocuments(completion: { (cardSnapshot, err) in
if let err = err {
print("Error fetching cards for deck: ", err)
} else {
guard let cardSnapshot = cardSnapshot else { return }
decksCount += 1
cardsCount += cardSnapshot.count
if decksCount == deckSnapshot.count {
completion(cardsCount)
}
}
})
})
}
}
}
Here is the solution, using DispatchGroup, found with #meggar's help in the comments:
func fetchCardsCount(uid: String, completion: #escaping (Int) -> ()) {
let db = Firestore.firestore()
var cardsCount = Int()
let group = DispatchGroup()
group.enter()
db.collection("users").document(uid).collection("decks").getDocuments { (deckSnapshot, err) in
if let err = err {
print("Error fetching decks for user: ", err)
} else {
guard let deckSnapshot = deckSnapshot else { return }
deckSnapshot.documents.forEach({ (deck) in
let dictionary = deck.data() as [String: Any]
let deck = FSDeck(dictionary: dictionary)
group.enter()
db.collection("users").document(uid).collection("decks").document(deck.deckId).collection("cards").getDocuments(completion: { (cardSnapshot, err) in
if let err = err {
print("Error fetching cards for deck: ", err)
} else {
guard let cardSnapshot = cardSnapshot else { return }
cardsCount += cardSnapshot.count
}
group.leave()
})
})
}
group.leave()
}
group.notify(queue: .main) {
completion(cardsCount)
}
}
I have a method that calls three functions that each make a request to Firebase and pass back data in a completion handler. Once the data from 2 of the completion handlers is sent back, I call another method to pass in the data and send back a valid result. Is nesting blocks like this a sign of poor design?
func fetchNearbyUsers(for user: User, completionHandler: usersCompletionHandler?) {
self.fetchAllUsers(completionHandler: { (users: [User]) in
ChatProvider.sharedInstance.fetchAllChatrooms(completionHandler: { (chatrooms: [Chatroom]) in
self.validateNewUsers(currentUser: user, users: users, chatrooms: chatrooms, completionHandler: { (validUsers: [User]) in
guard validUsers.isEmpty == false else {
completionHandler?([])
return
}
completionHandler?(validUsers)
})
})
})
}
// Other Methods
func fetchAllUsers(completionHandler: usersCompletionHandler?) {
var users: [User] = []
let group = DispatchGroup()
let serialQueue = DispatchQueue(label: "serialQueue")
DatabaseProvider.sharedInstance.usersDatabaseReference.observe(.value, with: {
snapshot in
guard let data = snapshot.value as? [String: AnyObject] else { return }
for (_, value) in data {
guard let values = value as? [String: AnyObject] else { return }
guard let newUser = User(data: values) else { return }
group.enter()
newUser.fetchProfileImage(completionHandler: { (image) in
serialQueue.async {
newUser.profileImage = image
users.append(newUser)
group.leave()
}
})
}
group.notify(queue: .main, execute: {
completionHandler?(users)
})
})
}
func fetchAllChatrooms (completionHandler: chatroomsHandler?) {
var chatrooms = [Chatroom]()
let group = DispatchGroup()
let serialQueue = DispatchQueue(label: "serialQueue")
DatabaseProvider.sharedInstance.chatroomsDatabaseReference.observe(.value, with: { snapshot in
guard let data = snapshot.value as? [String: AnyObject] else { return }
for (_, value) in data {
guard let value = value as? [String: AnyObject] else { return }
guard let newChatroom = Chatroom(data: value) else { return }
group.enter()
self.fetchChatroomUsers(chatroom: newChatroom, completionHandler: { (chatroom) in
serialQueue.async {
chatrooms.append(newChatroom)
group.leave()
}
})
}
group.notify(queue: .main, execute: {
completionHandler?(Array(Set<Chatroom>(chatrooms)))
})
})
}
private func validateNewUsers(currentUser: User, users: [User], chatrooms: [Chatroom], completionHandler: usersCompletionHandler?) {
let chatPartners = self.chatPartners(currentUser: currentUser, chatrooms: chatrooms)
let validUsers = users
.filter { currentUser.id != $0.id }
.filter { currentUser.distanceFromUser(atLocation: $0.coordinates) <= 8046.72 }
.filter { !chatPartners.contains($0.id) }
completionHandler?(validUsers)
}
private func chatPartners (currentUser: User, chatrooms: [Chatroom]) -> Set<String> {
var results = [String]()
for chatroom in chatrooms {
if currentUser.id == chatroom.firstUserId {
results.append(chatroom.secondUserId)
} else if currentUser.id == chatroom.secondUserId {
results.append(chatroom.firstUserId)
}
}
return Set(results)
}