I am trying to run loadViews() after the pullData() completes and I am wondering what the best way of doing this is? I would like to set a 10 sec timeout on it as well so I can display a network error if possible. From what I have read, GCD looks like it is the way to accomplish this but I am confused on the implementation of it. Thanks for any help you can give!
//1
pullData()
//2
loadViews()
What you need is a completion handler with a completion block.
Its really simple to create one:
func firstTask(completion: (success: Bool) -> Void) {
// Do something
// Call completion, when finished, success or faliure
completion(success: true)
}
And use your completion block like this:
firstTask { (success) -> Void in
if success {
// do second task if success
secondTask()
}
}
You can achieve like this :-
func demo(completion: (success: Bool) -> Void) {
// code goes here
completion(success: true)
}
I had a similar situation where I had to init a view once the data is pulled from Parse server. I used the following:
func fetchQuestionBank(complete:()->()){
let userDefault = NSUserDefaults.standardUserDefaults()
let username = userDefault.valueForKey("user_email") as? String
var query = PFQuery(className:"QuestionBank")
query.whereKey("teacher", equalTo: username!)
query.findObjectsInBackgroundWithBlock { (objects:[AnyObject]?, error:NSError?) -> Void in
if error == nil {
if let objects = objects as? [PFObject] {
var questionTitle:String?
var options:NSArray?
for (index, object) in enumerate(objects) {
questionTitle = object["question_title"] as? String
options = object["options"] as? NSArray
var aQuestion = MultipleChoiceQuestion(questionTitle: questionTitle!, options: options!)
aQuestion.questionId = object.objectId!
InstantlyModel.sharedInstance.questionBank.append(aQuestion)
}
complete()
}
}else{
println(" Question Bank Error \(error) ")
}
}
}
And this is you call the method:
self.fetchQuestionBank({ () -> () in
//Once all the data pulled from server. Show Teacher View.
self.teacherViewController = TeacherViewController(nibName: "TeacherViewController", bundle: nil)
self.view.addSubview(self.teacherViewController!.view)
})
function1();
function2();
Use functions!! Once function1() function completed, function2() will execute.
Related
I apologize if this question is simple or the problem is obvious as I am still a beginner in programming.
I am looping over an array and trying to make an async Firestore call. I am using a DispatchGroup in order to wait for all iterations to complete before calling the completion.
However, the Firestore function is not even getting called. I tested with print statements and the result is the loop iterations over the array have gone through with an enter into the DispatchGroup each time and the wait is stuck.
func getUserGlobalPlays(username: String, fixtureIDs: [Int], completion: #escaping (Result<[UserPlays]?, Error>) -> Void) {
let chunkedArray = fixtureIDs.chunked(into: 10)
var plays: [UserPlays] = []
let group = DispatchGroup()
chunkedArray.forEach { ids in
group.enter()
print("entered")
DispatchQueue.global().async { [weak self] in
self?.db.collection("Users").document("\(username)").collection("userPlays").whereField("fixtureID", in: ids).getDocuments { snapshot, error in
guard let snapshot = snapshot, error == nil else {
completion(.failure(error!))
return
}
for document in snapshot.documents {
let fixtureDoc = document.data()
let fixtureIDx = fixtureDoc["fixtureID"] as! Int
let choice = fixtureDoc["userChoice"] as! Int
plays.append(UserPlays(fixtureID: fixtureIDx, userChoice: choice))
}
group.leave()
print("leaving")
}
}
}
group.wait()
print(plays.count)
completion(.success(plays))
}
There are a few things going on with your code I think you should fix. You were dangerously force-unwrapping document data which you should never do. You were spinning up a bunch of Dispatch queues to make the database calls in the background, which is unnecessary and potentially problematic. The database call itself is insignificant and doesn't need to be done in the background. The snapshot return, however, can be done in the background (which this code doesn't do, so you can add that if you wish). And I don't know how you want to handle errors here. If one document gets back an error, your code sends back an error. Is that how you want to handle it?
func getUserGlobalPlays(username: String,
fixtureIDs: [Int],
completion: #escaping (_result: Result<[UserPlays]?, Error>) -> Void) {
let chunkedArray = fixtureIDs.chunked(into: 10)
var plays: [UserPlays] = []
let group = DispatchGroup()
chunkedArray.forEach { id in
group.enter()
db.collection("Users").document("\(username)").collection("userPlays").whereField("fixtureID", in: id).getDocuments { snapshot, error in
if let snapshot = snapshot {
for doc in snapshot.documents {
if let fixtureIDx = doc.get("fixtureIDx") as? Int,
let choice = doc.get("choice") as? Int {
plays.append(UserPlays(fixtureID: fixtureIDx, userChoice: choice))
}
}
} else if let error = error {
print(error)
// There was an error getting this one document. Do you want to terminate
// the entire function and pass back an error (through the completion
// handler)? Or do you want to keep going and parse whatever data you can
// parse?
}
group.leave()
}
}
// This is the completion handler of the Dispatch Group.
group.notify(queue: .main) {
completion(.success(plays))
}
}
I have created a function getFriends that reads a User's friendlist from firestore and puts each friend in a LocalUser object (which is my custom user class) in order to display the friendlist in a tableview. I need the DispatchSemaphore.wait() because I need the for loop to iterate only when the completion handler inside the for loop is called.
When loading the view, the app freezes. I know that the problem is that semaphore.wait() is called in the main thread. However, from reading DispatchQueue-tutorials I still don't understand how to fix this in my case.
Also: do you see any easier ways to implement what I want to do?
This is my call to the function in viewDidLoad():
self.getFriends() { (friends) in
self.foundFriends = friends
self.friendsTable.reloadData()
}
And the function getFriends:
let semaphore = DispatchSemaphore(value: 0)
func getFriends(completion: #escaping ([LocalUser]) -> ()) {
var friendsUID = [String : Any]()
database.collection("friends").document(self.uid).getDocument { (docSnapshot, error) in
if error != nil {
print("Error:", error!)
return
}
friendsUID = (docSnapshot?.data())!
var friends = [LocalUser]()
let friendsIdents = Array(friendsUID.keys)
for (idx,userID) in friendsIdents.enumerated() {
self.getUser(withUID: userID, completion: { (usr) in
var tempUser: LocalUser
tempUser = usr
friends.append(tempUser)
self.semaphore.signal()
})
if idx == friendsIdents.endIndex-1 {
print("friends at for loop completion:", friends.count)
completion(friends)
}
self.semaphore.wait()
}
}
}
friendsUID is a dict with each friend's uid as a key and true as the value. Since I only need the keys, I store them in the array friendsIdents. Function getUser searches the passed uid in firestore and creates the corresponding LocalUser (usr). This finally gets appended in friends array.
You should almost never have a semaphore.wait() on the main thread. Unless you expect to wait for < 10ms.
Instead, consider dispatching your friends list processing to a background thread. The background thread can perform the dispatch to your database/api and wait() without blocking the main thread.
Just make sure to use DispatchQueue.main.async {} from that thread if you need to trigger any UI work.
let semaphore = DispatchSemaphore(value: 0)
func getFriends(completion: #escaping ([LocalUser]) -> ()) {
var friendsUID = [String : Any]()
database.collection("friends").document(self.uid).getDocument { (docSnapshot, error) in
if error != nil {
print("Error:", error!)
return
}
DispatchQueue.global(qos: .userInitiated).async {
friendsUID = (docSnapshot?.data())!
var friends = [LocalUser]()
let friendsIdents = Array(friendsUID.keys)
for (idx,userID) in friendsIdents.enumerated() {
self.getUser(withUID: userID, completion: { (usr) in
var tempUser: LocalUser
tempUser = usr
friends.append(tempUser)
self.semaphore.signal()
})
if idx == friendsIdents.endIndex-1 {
print("friends at for loop completion:", friends.count)
completion(friends)
}
self.semaphore.wait()
}
// Insert here a DispatchQueue.main.async {} if you need something to happen
// on the main queue after you are done processing all entries
}
}
I have the following case. The root controller is UITabViewController. There is a ProfileViewController, in it I make an observer that users started to be friends (and then the screen functions change). ProfileViewController can be opened with 4 tabs out of 5, and so the current user can open the screen with the same user in four places. In previous versions, when ProfileViewController opened in one place, I deleted the observer in deinit and did the deletion just by ref.removeAllObservers(), now when the user case is such, I started using handle and delete observer in viewDidDisappear. I would like to demonstrate the code to find out whether it can be improved and whether I'm doing it right in this situation.
I call this function in viewWillAppear
fileprivate func firObserve(_ isObserve: Bool) {
guard let _user = user else { return }
FIRFriendsDatabaseManager.shared.observeSpecificUserFriendshipStart(observer: self, isObserve: isObserve, userID: _user.id, success: { [weak self] (friendModel) in
}) { (error) in
}
}
This is in the FIRFriendsDatabaseManager
fileprivate var observeSpecificUserFriendshipStartDict = [AnyHashable : UInt]()
func observeSpecificUserFriendshipStart(observer: Any, isObserve: Bool, userID: String, success: ((_ friendModel: FriendModel) -> Void)?, fail: ((_ error: Error) -> Void)?) {
let realmManager = RealmManager()
guard let currentUserID = realmManager.getCurrentUser()?.id else { return }
DispatchQueue.global(qos: .background).async {
let specificUserFriendRef = Database.database().reference().child(MainGateways.friends.description).child(currentUserID).child(SubGateways.userFriends.description).queryOrdered(byChild: "friendID").queryEqual(toValue: userID)
if !isObserve {
guard let observerHashable = observer as? AnyHashable else { return }
if let handle = self.observeSpecificUserFriendshipStartDict[observerHashable] {
self.observeSpecificUserFriendshipStartDict[observerHashable] = nil
specificUserFriendRef.removeObserver(withHandle: handle)
debugPrint("removed handle", handle)
}
return
}
var handle: UInt = 0
handle = specificUserFriendRef.observe(.childAdded, with: { (snapshot) in
if snapshot.value is NSNull {
return
}
guard let dict = snapshot.value as? [String : Any] else { return }
guard let friendModel = Mapper<FriendModel>().map(JSON: dict) else { return }
if friendModel.friendID == userID {
success?(friendModel)
}
}, withCancel: { (error) in
fail?(error)
})
guard let observerHashable = observer as? AnyHashable else { return }
self.observeSpecificUserFriendshipStartDict[observerHashable] = handle
}
}
Concerning your implementation of maintaining a reference to each viewController, I would consider moving the logic to an extension of the viewController itself.
And if you'd like to avoid calling ref.removeAllObservers() like you were previously, and assuming that there is just one of these listeners per viewController. I'd make the listener ref a variable on the view controller.
This way everything is contained to just the viewController. It also is potentially a good candidate for creating a protocol if other types of viewControllers will be doing similar types of management of listeners.
Apologies that I couldn't think of a better way to title this.
Basically I have an app which connects to Parse. I have specified an action, which runs when a text field is changed (i.e when the user types a letter)
within this action I'm calling a PFQuery to Parse, which I then ask to findObjectsInBackgroundWithBlock.
The problem is that if a user were to type another letter before this query has finished running, then 2 queries are now running and the results of both end up populating the tableView.
So my question is simply, if the user were to type in another letter before the first findObjectsInBackgroundWithBlock has finished, how would I cancel the first and run a new one?
I have tried inserting PFQuery.cancel(query) at the start of the action, but the code gets confused as there isn't a query running yet when the action runs for the first time.
My code, incase it may help:
#IBAction func textFieldChanged (sender: AnyObject) {
let query = PFUser.Query()
query!.whereKey("Postcode", containsString: searchField.text)
query?.findObjectsInBackgroundWithBlock({ (objects, error) -> Void in
if let objects = objects {
for object in objects {
self.citiesArray.append(object["city"] as! String)
}
}
})
Many thanks for your patience!
You can try wrapping those requests in NSOperation and adding them to a dedicated (for such search requests) NSOperationQueue and calling cancelAllOperations() when a new character is typed.
In NSOperation's inner Parse block check for self.cancelled and return doing nothing if cancelled. Should work fine.
UPDATE:
class ViewController: UIViewController {
#IBOutlet weak var searchField: UITextField!
var citiesArray = [String]()
lazy var textRequestQueue: NSOperationQueue = {
var queue = NSOperationQueue()
queue.maxConcurrentOperationCount = 1
queue.qualityOfService = NSQualityOfService.UserInteractive
return queue
}()
#IBAction func textFieldChanged(sender: AnyObject) {
textRequestQueue.cancelAllOperations()
let query = PFQuery()
query.whereKey("Postcode", containsString: searchField.text)
textRequestQueue.addOperation(TextRequestOperation(query: query, resultBlock: { (objects, error) -> Void in
if let objects = objects {
for object in objects {
self.citiesArray.append(object["city"] as! String)
}
}
}))
}
}
class TextRequestOperation: NSOperation {
typealias ResultBlock = ((result: String)->())
var _resultBlock: PFArrayResultBlock
var _query: PFQuery
init(query: PFQuery, resultBlock: PFArrayResultBlock) {
self._resultBlock = resultBlock
self._query = query
}
override func main()
{
if self.cancelled { return }
_query.findObjectsInBackgroundWithBlock { (objects, error) -> Void in
if self.cancelled { return }
self._resultBlock(objects, error)
}
}
}
NSOperation is one option. ReativeCocoa is another option that could help you solve this problem quite easily if you know how to use it.
However the easiest way (and hackiest) would prob be to keep some state of the search and use it to only apply the most recent searches results.
var mostRecentSearchQuery: String = ""
#IBAction func textFieldChanged (sender: AnyObject) {
var queryString: String?
if let textField: UITextField = sender as? UITextField {
queryString = textField.text
self.mostRecentSearchQuery = textField.text
}
let query = PFUser.Query()
query!.whereKey("Postcode", containsString: searchField.text)
query?.findObjectsInBackgroundWithBlock({[weak self] (objects, error) -> Void in
if let objects = objects where queryString == self?.mostRecentSearchQuery {
for object in objects {
self.citiesArray.append(object["city"] as! String)
}
}
})
This will only update your results if block used the most recent text typed.
ps. I am assuming the sender passed into the method is the textField which text has changed.
I'm trying to nest 2 PFQuery's inside HandleWatchKitExtensionRequest so that I can get the data passed back to my Watch Extension in my reply. In the code below the first Query (for a PFUser matching the given userName) returns, but I cannot get the second (for a list of the User's people) to return. Is there some limitation I'm missing on making multiple queries within the same block? Is my background task timing out before this long running request can return?
*I edited my code so that nameList is not an optional, but still no return value. While the same block (nested findObjectsInBackGroundBlocks) executes and returns perfectly in my IOS app.
Extending the time of the delay of background task didn't help.
func application(application: UIApplication, handleWatchKitExtensionRequest userInfo: [NSObject : AnyObject]?, reply: (([NSObject : AnyObject]!) -> Void)!) {
var dictionary = userInfo! as NSDictionary
if let currentUserName: AnyObject = dictionary.objectForKey("currentUserName") {
if let currentUserNameAsString = currentUserName as? String {
// Bogus task for keeping app going
var bogusTask = UIApplication.sharedApplication().beginBackgroundTaskWithExpirationHandler { () -> Void in
}
// End bogus task after 2 seconds
delay(2.0, closure: { () -> () in
UIApplication.sharedApplication().endBackgroundTask(bogusTask)
})
// look up current user logged in on phone from name passed from Watch
let query = PFUser.query()
query!.whereKey("username", equalTo: currentUserNameAsString)
query!.findObjectsInBackgroundWithBlock { (objects, error) -> Void in
if let user = objects?.first as? PFUser {
var testName: String?
var nameList: String = []
let query = PFQuery(className: Person.parseClassName())
query.whereKey("user", equalTo: user)
query.findObjectsInBackgroundWithBlock { (objects, error) -> Void in
if let people = objects as? [Person] {
for person in people {
if let name = person.name {
nameList.append(name)
testName = nameList.first
}
}
}
testName = ("test")
}
let testDict = ["success" : testName!]
reply(testDict)
UIApplication.sharedApplication().endBackgroundTask(bogusTask) // End the bogusTask in case the work was faster than 2 seconds
}
}
}
}
}
// Utility function for delay
func delay(delay:Double, closure:()->()) {
dispatch_after(
dispatch_time(
DISPATCH_TIME_NOW,
Int64(delay * Double(NSEC_PER_SEC))
),
dispatch_get_main_queue(),
closure
)
}
It looks like you are calling "reply(testDict)" outside of your nested query.
Since the query is asynchronous the "reply(testDict)" is probably being called before the nested query is finished executing.