So I have a function called grabUserLoc which grabs the user location.
#objc func grabUserLoc(){
LocationService.getUserLocation { (location) in
guard let currentLocation = location else { return }
print("Latitude: \(currentLocation.coordinate.latitude)")
print("Longitude: \(currentLocation.coordinate.longitude)")
}
}
The function uses this service method, that contains this function.
struct LocationService {
static func getUserLocation(completion: #escaping (CLLocation?) -> Void){
print("Atttempting to get user location")
//May need location manager function
Location.getLocation(accuracy: .city, frequency:.continuous , success: { (_, location) -> (Void) in
//print("Latitide: \(location.coordinate.latitude)")
//print("Longitude: \(location.coordinate.longitude)")
return completion(location)
}) { (request, last, error) -> (Void) in
request.cancel()
print("Location monitoring failed due to an error \(error)")
return completion(nil)
}
}
}
Now I have print statements in the grabUserLoc function to make sure things are working properly which they are because the user location is being grabbed. However, the print statement as well as the completion block is being run twice so it is running the print statement two times. Did I do anything wrong here as far as my implementation?
Btw I am using thrid party location pod called swiftlocation
https://github.com/malcommac/SwiftLocation
Try to stop location update before completion as it may update location twice that usually happen when you implement didLocationUpdate delegate method of CLLocationManager
Location.getLocation(accuracy: .city, frequency:.continuous , success: { (_, location) -> (Void) in
//print("Latitide: \(location.coordinate.latitude)")
//print("Longitude: \(location.coordinate.longitude)")
//Stop location Update here after first call
return completion(location)
}) { (request, last, error) -> (Void) in
request.cancel()
print("Location monitoring failed due to an error \(error)")
return completion(nil)
}
OR surround function with once Bool
#objc func grabUserLoc(){
var once = true
LocationService.getUserLocation { (location) in
guard let currentLocation = location else {
return
}
if(once)
{
print("Latitude: \(currentLocation.coordinate.latitude)")
print("Longitude: \(currentLocation.coordinate.longitude)")
once = false;
}
}
}
You seem to be using some third-party framework to get location so I don't know exactly what your code is doing, but this line:
Location.getLocation(accuracy: .city, frequency:.continuous , success:
has a frequency parameter. Maybe that should be set to something other than .continuous to just get a single location?
If you want to use the iOS Core Location framework directly, getting a single location is just a call to requestLocation() in CLLocationManager.
Related
I am struggling to trigger the logic responsible for changing the view at the right time. Let me explain.
I have a view model that contains a function called createNewUserVM(). This function triggers another function named requestNewUser() which sits in a struct called Webservices.
func createNewUserVM() -> String {
Webservices().requestNewUser(with: User(firstName: firstName, lastName: lastName, email: email, password: password)) { serverResponse in
guard let serverResponse = serverResponse else {
return "failure"
}
return serverResponse.response
}
}
Now that's what's happening in the Webservices' struct:
struct Webservices {
func requestNewUser(with user: User, completion: #escaping (Response?) -> String) -> String {
//code that creates the desired request based on the server's URL
//...
URLSession.shared.dataTask(with: request) { data, response, error in
guard let data = data, error == nil else {
DispatchQueue.main.async {
serverResponse = completion(nil)
}
return
}
let decodedResponse = try? JSONDecoder().decode(Response.self, from: data)
DispatchQueue.main.async {
serverResponse = completion(decodedResponse)
}
}.resume()
return serverResponse //last line that gets executed before the if statement
}
}
So as you can see, the escaping closure (whose code is in the view model) returns serverResponse.response (which can be either "success" or "failure"), which is then stored in the variable named serverResponse. Then, requestNewUser() returns that value. Finally, the createNewUserVM() function returns the returned String, at which point this whole logic ends.
In order to move to the next view, the idea was to simply check the returned value like so:
serverResponse = self.signupViewModel.createNewUserVM()
if serverResponse == "success" {
//move to the next view
}
However, after having written a few print statements, I found out that the if statement gets triggered way too early, around the time the escaping closure returns the value, which happens before the view model returns it. I attempted to fix the problem by using some DispatchQueue logic but nothing worked. I also tried to implement a while loop like so:
while serverResponse.isEmpty {
//fetch the data
}
//at this point, serverResponse is not empty
//move to the next view
It was to account for the async nature of the code.
I also tried was to pass the EnvironmentObject that handles the logic behind what view's displayed directly to the view model, but still without success.
As matt has pointed out, you seem to have mixed up synchronous and asynchronous flows in your code. But I believe the main issue stems from the fact that you believe URLSession.shared.dataTask executes synchronously. It actually executes asynchronously. Because of this, iOS won't wait until your server response is received to execute the rest of your code.
To resolve this, you need to carefully read and convert the problematic sections into asynchronous code. Since the answer is not trivial in your case, I will try my best to help you convert your code to be properly asynchronous.
1. Lets start with the Webservices struct
When you call the dataTask method, what happens is iOS creates a URLSessionDataTask and returns it to you. You call resume() on it, and it starts executing on a different thread asynchronously.
Because it executes asynchronously, iOS doesn't wait for it to return to continue executing the rest of your code. As soon as the resume() method returns, the requestNewUser method also returns. By the time your App receives the JSON response the requestNewUser has returned long ago.
So what you need to do to pass your response back correctly, is to pass it through the "completion" function type in an asynchronous manner. We also don't need that function to return anything - it can process the response and carry on the rest of the work.
So this method signature:
func requestNewUser(with user: User, completion: #escaping (Response?) -> String) -> String {
becomes this:
func requestNewUser(with user: User, completion: #escaping (Response?) -> Void) {
And the changes to the requestNewUser looks like this:
func requestNewUser(with user: User, completion: #escaping (Response?) -> Void) {
//code that creates the desired request based on the server's URL
//...
URLSession.shared.dataTask(with: request) { data, response, error in
guard let data = data, error == nil else {
DispatchQueue.main.async {
completion(nil)
}
return
}
let decodedResponse = try? JSONDecoder().decode(Response.self, from: data)
DispatchQueue.main.async {
completion(decodedResponse)
}
}.resume()
}
2. View Model Changes
The requestNewUser method now doesn't return anything. So we need to accommodate that change in our the rest of the code. Let's convert our createNewUserVM method from synchronous to asynchronous. We should also ask the calling code for a function that would receive the result from our Webservice class.
So your createNewUserVM changes from this:
func createNewUserVM() -> String {
Webservices().requestNewUser(with: User(firstName: firstName, lastName: lastName, email: email, password: password)) { serverResponse in
guard let serverResponse = serverResponse else {
return "failure"
}
return serverResponse.response
}
}
to this:
func createNewUserVM(_ callback: #escaping (_ response: String?) -> Void) {
Webservices().requestNewUser(with: User(firstName: firstName, lastName: lastName, email: email, password: password)) { serverResponse in
guard let serverResponse = serverResponse else {
callback("failure")
return
}
callback(serverResponse.response)
}
}
3. Moving to the next view
Now that createNewUserVM is also asynchronous, we also need to change how we call it from our controller.
So that code changes from this:
serverResponse = self.signupViewModel.createNewUserVM()
if serverResponse == "success" {
//move to the next view
}
To this:
self.signupViewModel.createNewUserVM{ [weak self] (serverResponse) in
guard let `self` = self else { return }
if serverResponse == "success" {
// move to the next view
// self.present something...
}
}
Conclusion
I hope the answer gives you an idea of why your code didn't work, and how you can convert any existing code of that sort to execute properly in an asynchronous fashion.
This can be achieve using DispatchGroup and BlockOperation together like below:
func functionWillEscapeAfter(time: DispatchTime, completion: #escaping (Bool) -> Void) {
DispatchQueue.main.asyncAfter(deadline: time) {
completion(false) // change the value to reflect changes.
}
}
func createNewUserAfterGettingResponse() {
let group = DispatchGroup()
let firstOperation = BlockOperation()
firstOperation.addExecutionBlock {
group.enter()
print("Wait until async block returns")
functionWillEscapeAfter(time: .now() + 5) { isSuccess in
print("Returned value after specified seconds...")
if isSuccess {
group.leave()
// and firstoperation will be complete
} else {
firstOperation.cancel() // means first operation is cancelled and we can check later if cancelled don't execute next operation
group.leave()
}
}
group.wait() //Waits until async closure returns something
} // first operation ends
let secondOperation = BlockOperation()
secondOperation.addExecutionBlock {
// Now before executing check if previous operation was cancelled we don't need to execute this operation.
if !firstOperation.isCancelled { // First operation was successful.
// move to next view
moveToNextView()
} else { // First operation was successful.
// do something else.
print("Don't move to next block")
}
}
// now second operation depends upon the first operation so add dependency
secondOperation.addDependency(firstOperation)
//run operation in queue
let operationQueue = OperationQueue()
operationQueue.addOperations([firstOperation, secondOperation], waitUntilFinished: false)
}
func moveToNextView() {
// move view
print("Move to next block")
}
createNewUserAfterGettingResponse() // Call this in playground to execute all above code.
Note: Read comments for understanding. I have run this in swift playground and working fine. copy past code in playground and have fun!!!
Currently making an app in which a view controller takes in the addressString and then geocodes the address to retrieve the latitude and longitude of said address as seen below inside of a #objc method.
#objc func addLocationTouchUpInside() {
let addressString = "\(streetAddressField.text!), \(cityField.text!), \(stateField.text!)"
var location = Location(id: Location.setid(), locationName: nil, addressString: addressString, latitude: nil, longitude: nil, HoHid: nil)
// This geocoder instance is a let property initialized with the class
geocoder.geocodeAddressString(location.addressString) { placemark, error in // Completion handler starts here...
print("WHY")
if error != nil {
print("error: \(error!)")
}
if let placemark = placemark?[0] {
if let addresslocation = placemark.location {
location.latitude = addresslocation.coordinate.latitude
location.longitude = addresslocation.coordinate.longitude
}
}
print("3: \(location.latitude)")
}
do {
try AppDatabase.shared.dbwriter.write({ db in
try location.insert(db)
})
self.currentUser?.locationid = location.id
try AppDatabase.shared.dbwriter.write({ db in
if var user = try User.fetchOne(db, key: self.currentUser?.id) {
user = self.currentUser!
try user.update(db)
}
})
} catch {
fatalError("\(error)")
}
let locationInputViewController = LocationInputViewController()
locationInputViewController.setUser(user: self.currentUser!)
locationInputViewController.setLocation(location: location)
locationInputViewController.setHoH()
if let navigationController = self.navigationController {
navigationController.pushViewController(locationInputViewController, animated: true)
}
}
The issue I am having is that any code within the completion handler (of type CLGeocodeCompletionHandler) does not seem to execute. Originally I had assumed that location simply wasnt being mutated for some reason. I then learned that nothing within the completion handler executes.
This question may seem like a duplicate to this post however it seems a clear answer was not quite found. Via some comments on said post I had set both set a custom location for the simulator and used my personal iphone and saw no difference.
EDIT: I have updated the question with the entire function as requested in the comments. Second the completion handler I have been referring to would be that of CLGeocodeCompletionHandler. Defined as
typealias CLGeocodeCompletionHandler = ([CLPlacemark]?, Error?) -> Void
where
func geocodeAddressString(_ addressString: String, completionHandler: #escaping CLGeocodeCompletionHandler)
in the documentation. I have also commented the line where the completion handler starts.
Finally in only ONE of my runs was this strange error outputted:
#NullIsland Received a latitude or longitude from getLocationForBundleID that was exactly zero
Let's say we have this pseudocode representing a network request call and show/hide an activity indicator, using RxSwift:
func performRequest() {
isLoading.accept(true)
self.network.executeRequest()
.subscribe(onNext: {
self.isLoading.accept(false)
}, onError: {
self.isLoading.accept(false)
})
}
The function executeRequest returns either an Observable or Single.
I am not feeling comfortable with having to write twice the same code, for onNext/onSuccess and onError, basically doing the same.
I am looking for suggestions to minimize/improve turning off the activity indicator, like for example handling all events of the request in a single statement and avoid using the subscribe function. Or maybe there are other suggestions?
I use ActivityIndicator from RxSwift Example app, which makes it really convenient, especially if your loading multiple things in parallel as it maintains a count of running subscriptions and emit false only when this count is equal to 0:
let isLoading = ActivityIndicator()
func performRequests() {
self.network
.executeFirstRequest()
.trackActivity(isLoading)
.subscribe {
// ...
}
self.network
.executeSecondRequest()
.trackActivity(isLoading)
.subscribe {
// ...
}
}
You can use another method to subscribe, which passes Event in case of Observer or SingleEvent in case of Single:
subscribe(on: (Event<T>) -> Void)
subscribe(observer: (SingleEvent<T>) -> Void)
Observer Example:
func performRequest() {
isLoading.accept(true)
self.network.executeRequest().subscribe {
switch $0 {
case let .error(error):
print(error)
case let .next:
print("good")
case .completed:
print("also good")
}
isLoading.accept(false)
}
}
Single Example:
func performRequest() {
isLoading.accept(true)
self.network.executeRequest().subscribe {
switch $0 {
case let .error(error):
print(error)
case let .next:
print("good")
}
isLoading.accept(false)
}
}
I have a problem with working set enumerator (Recents tab) in Files app. I implement a working enumerator for Folders, it runs enumerateItems(for observer method when I move to the folder in UI and everything works fine.
And I have a different enumerator for working set too, but it is not created and enumerated anything.
override func enumerator(for containerItemIdentifier: NSFileProviderItemIdentifier) throws -> NSFileProviderEnumerator {
let maybeEnumerator: NSFileProviderEnumerator?
if (containerItemIdentifier == NSFileProviderItemIdentifier.rootContainer) {
let directory = repository.directory(for: containerItemIdentifier)!
maybeEnumerator = FileProviderFolderEnumerator()
} else if (containerItemIdentifier == NSFileProviderItemIdentifier.workingSet) {
maybeEnumerator = FileProviderWorkingSetEnumerator() // not called
} else {
_ = repository.item(for: containerItemIdentifier)
if repository.isDirectory(at: containerItemIdentifier) {
let directory = repository.directory(for: containerItemIdentifier)!
maybeEnumerator = FileProviderFolderEnumerator() // works fine
} else {
maybeEnumerator = FileProviderItemEnumerator()
}
}
guard let enumerator = maybeEnumerator else {
throw NSError(domain: NSCocoaErrorDomain, code: NSFeatureUnsupportedError, userInfo:[:])
}
return enumerator
}
I'm trying to call working set enumerator in startProvidingItem method using signal, but it doesn't work.
override func startProvidingItem(at url: URL, completionHandler: #escaping ((_ error: Error?) -> Void)) {
loadItem {
//when done with loading
NSFileProviderManager.default.signalEnumerator(
for: NSFileProviderItemIdentifier.workingSet,
completionHandler: { error in
if let err = error { print(err) }
}
}
}
Does FileProviderWorkingSetEnumerator initialize automatically when I open Recents tab in Files app? Should I call it somehow directly from FileExtension?
Thank you!
The working set enumerator doesn't work like what you think.
The file provider extension will enumerate working set in background, even before you open Files app.
That's why you need to prepare working set data in offline.
The WWDC 2017 document might help.
Okay, so I'm trying to build an iOS app that relies on Firebase (To work with its android version)
I started with creating a repository for each actor in my app and a general repository to manage them all
Each repository manages the observers of this actor. An example:
Inside the PagesRepository, this is a function that retrieves all the pages from Firebase and returns it inside a completionHandler:
//MARK: Gets the whole pages list
func getPagesList(completionHandler: #escaping (_ pages: [Page]?, _ error: NSError?) -> Void) {
func displayError(error: String) {
print(error)
completionHandler(nil, self.getErrorFromString(error))
}
pagesRef.observe(DataEventType.value) { pagesSnapshot in
guard pagesSnapshot.exists() else {
displayError(error: "Pages snapshot doesn't exist")
return
}
var pagesList = [Page]()
for pageSnapshot in pagesSnapshot.children {
pagesList.append(Page(snapshot: pageSnapshot as! DataSnapshot))
}
completionHandler(pagesList, nil)
}
}
And then I call it from the ViewController like this:
repository.getPagesList { (pages, error) in
guard error == nil else {
return
}
//Do processing
}
I know this may be a lot to take in, but my problem is that every time I call the function, it creates a new observer but doesn't cancel the old one... So, the completionHandler is called multiple times with different values
How should I manage this problem?
(Sorry for being complicated and a little unclear, I'm just really lost)
It seems like you only want to observe the value once so I would use this instead:
func getPagesList(completionHandler: #escaping (_ pages: [Page]?, _ error: NSError?) -> Void) {
func displayError(error: String) {
print(error)
completionHandler(nil, self.getErrorFromString(error))
}
pagesRef.observeSingleEvent(of: .value, with: { (pagesSnapshot) in
var pagesList = [Page]()
for pageSnapshot in pagesSnapshot.children {
pagesList.append(Page(snapshot: pageSnapshot as! DataSnapshot))
}
completionHandler(pagesList, nil)
}) { (error) in
// Display error
}
}