I'm sorry the title is not clearer, but it's hard to resume the problem in one sentence.
I'm using the Facebook SDK to retrieve user information. My intent is to instantiate a class I wrote myself called Profile, which contains several user information, including the binary for the avatar picture (NSData).
To do this, I first use FBRequestConnection.startWithGraphPath and then, in the completion handler, I create a Profile instance and request the avatar binary data, using NSURLConnection.sendAsynchronousRequest:queue:completionHandler:
As soon as I get the complete profile, I notify a delegate with didReceiveProfile:. Now, the delegate is LoginViewController, which is a subclass of UIViewController. This method then saves the profile data to disk and then does this:
var app: UIApplication = UIApplication.sharedApplication();
var delegate: AppDelegate = app.delegate as AppDelegate;
delegate.application(app, didReceiveProfile: profile);
So the idea is to notify the AppDelegate that a Profile is now available, so whatever view controller that follows LoginViewController can now be displayed. Here's the code for that:
var storyboard: UIStoryboard = UIStoryboard(name: MainStoryboardIdentifier, bundle: nil);
var profileVC: ProfileViewController! = storyboard.instantiateViewControllerWithIdentifier(ProfileViewControllerIdentifier) as ProfileViewController;
NSLog("Everything OK");
self.navigationController!.pushViewController(profileVC, animated: false);
I do get the log "Everything OK", but the view doesn't show.
A few more details.
When the application first loads, it checks whether the profile has been saved to disk. If it has, it loads the profile from the file and calls didReceiveProfile: which instantly jumps to the second screen. So, the same method is called and it works. It can't be the code.
Also, notice that NSURLConnection.sendAsynchronousRequest:queue:completionHandler: is called inside the FBRequestConnection.startWithGraphPath's completion handler. If I call the delegate's didReceiveProfile: method before sendAsynchronousRequest (that is, in Facebook's completion handler), then it works and the view is properly shown, but I do not have the avatar binary. But if I call it inside sendAsynchronousRequest's completion handler, then the view doesn't show. It just gets back to the login view.
I am new to iOS development, but by my experience it seems like the completion handler is being called in a different thread? Being asynchronous and all.
So, how do I get the view to show?
Thank you.
Ok, I realised I left out something really important. As soon as I did, I also realised what the problem was. I was calling sendAsynchronousRequest like this:
var request = NSURLRequest(URL: url);
let queue = NSOperationQueue();
NSURLConnection.sendAsynchronousRequest(request, queue: queue, completionHandler:{ /* RESPONSE CODE */ });
Obviously the queue is not right. It should be:
let queue = NSOperationQueue.mainQueue();
That solved it.
Related
I have a table view that initially starts empty. I am trying to update it once an asynchronous method is called Networking().fetchRecipies(ingredients: searchText). However, after this method is called, the recipieTableView remains unpopulated. What is causeing this error? Below is the code I am running that should be run everytime the text in the text field changes. I know this is working because I am also printing the results of the API call
#IBOutlet weak var recipieTableView: UITableView!
var recipies = LocalData.recipies.recipieList
var filteredRecipies = [Recipie]()
override func viewDidLoad() {
filteredRecipies = recipies
super.viewDidLoad()
}
func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) {
Networking().fetchRecipies(ingredients: searchText)
self.recipieTableView.reloadData()
DispatchQueue.main.async {
self.recipieTableView.reloadData()
self.recipieTableView.beginUpdates()
self.recipieTableView.endUpdates()
}
}
However, after this method is called, the recipieTableView remains unpopulated. What is causeing this error
What's causing the error is that when you say "after" you should actually be saying "before"! Your code does not run in the order it is written. It runs in this order:
Networking().fetchRecipies(ingredients: searchText) // 2
self.recipieTableView.reloadData() // 1...
// ...
In other words, all your calls to reloadData are happening before you fetch the recipes.
You need to write fetchRecipes such that it reloads the table view after the fetching has finished.
You say the method Networking().fetchRecipies(ingredients:) is called asynchronously, which basically means your code steps into another thread and simultaneously continues to execute to the end of the method your asynchronous function was called from.
So the line self.recipieTableView.reloadData() is called when your data is not yet populated.
After that you call DispatchQueue.main.async which is also unnecessary. Since you're already on the main thread (because that's the convention for searchBar(_:textDidChange:), as for the most of UIKit methods), the only thing your method does is runs the contents of the dispatched block of code some time later (but chances are still much sooner before your fetch request completes).
Read more on how concurrency works on iOS in the official guide. If you're a beginner, it would be just enough to understand the difference between serial and concurrent queues and sync and async dispatches.
Back to your problem. I suggest that you examine the API of your Networking class and check if there's any way to notify you when your data has been actually fetched. Most iOS APIs provide a completion block that is executed with your newly fetched data passed as an argument. For example, it could look like the following:
Networking().fetchRecipies(ingredients: searchText, completion: { fetchedIngredients in
// ... update your data source array
// reload table view
})
Call tableView.updateData() directly from inside such a block if it's called on the main queue, or wrap it inside DispatchQueue.main.async { } if your completion block is invoked from a background queue (refer to the API to find it out).
And, most important, don't forget to update your array (I believe it's either recipies or filteredRecipies) which serves as the source for your table's data BEFORE you call updateData().
I'm writing an app in Swift and I've run into a problem I can't find much help for.
So I've got a ViewController and a class named CameraHandler that uses calls the an ActionSheet which then presents camera or gallery picker and saves resulting images.
What I want to do is: call camera, save the image, and then store relevant information to an object.
CameraHandler.shared.showActionSheet(vc: self, reg: car.registration)
self.car.imagePath = CameraHandler.shared.returnFilePath()
self.storeCar() // completes before CameraHandler can get imagepath
My workaround is using a simple DispatchQueue to wait 10 secs hoping it completes in time.
As matt says in his comment, don't wait. Don't expect the answer to be returned from your function. You need to rewrite your showActionSheet() function to take a completion handler. (That is a block of code {more specifically, a closure} that you pass as a parameter.) When your function gets an answer from the async process it needs to complete (fetching a value from a remote server, prompting the user for something, etc.) then it calls the completion handler.
See my answer to the thread below. I include a sample project that uses a function with a completion handler to fetch data from a remote server.
Swift: Wait for Firebase to load before return a function
Using a closure or block in other programing languages as a completion handler, let take an example.
func returnFilePath(completionHandler: ((String) -> ()) {
// after get file path
completionHandler(path)
}
then you can use it like:
returnFilePath { path in
// do some thing with the path
}
Every firebase client example I see in Swift seems to oversimplify properly loading data from Firebase, and I've now looked through all the docs and a ton of code. I do admit that my application may be a bit of an edge case.
I have a situation where every time a view controller is loaded, I want to auto-post a message to the room "hey im here!" and additionally load what's on the server by a typical observation call.
I would think the flow would be:
1. View controller loads
2. Auto-post to room
3. Observe childAdded
Obviously the calls are asynchronous so there's no guarantee the order of things happening. I tried to simplify things by using a complete handler to wait for the autopost to come back but that loads the auto-posted message twice into my tableview.
AutoPoster.sayHi(self.host) { (error) in
let messageQuery = self.messageRef.queryLimited(toLast:25).queryOrdered(byChild: "sentAt")
self.newMessageRefHandle = messageQuery.observe(.childAdded, with: { (snapshot) in
if let dict = snapshot.value as? [String: AnyObject] {
DispatchQueue.main.async {
let m = Message(dict, key: snapshot.key)
if m.mediaType == "text" {
self.messages.append(m)
}
self.collectionView.reloadData()
}
}
})
}
Worth noting that this seems very inefficient for an initial load. I fixed that by using a trick with a timer that will basically only allow the collection view to reload maximum every .25s and will restart the timer every time new data comes in. A bit hacky but I guess the benefits of firebase justify the hack.
I've also tried to observe the value event once for an initial load and then only after that observe childAdded but I think that has issues as well since childAdded is called regardless.
While I'm tempted to post code for all of the loading methods I have tried (and happy to update the question with it), I'd rather not debug what seems to not be working and instead have someone help outline the recommended flow for a situation like this. Again, the goal is simply to auto-post to the room that I joined in the conversation, then load the initial data (my auto-post should be the most recent message), and then listen for incoming new messages.
Instead of
self.newMessageRefHandle = messageQuery.observe(.childAdded, with: { (snapshot) in
try replacing with
let childref = FIRDatabase.database().reference().child("ChildName")
childref.queryOrdered(byChild:"subChildName").observe(.value, with: { snapshot in
I've found so many solutions for progress bar update within the same thread and view controller, however they seemed to be not similar cases as mine.
In my application, the main view controller calls loadIntoCoreData()(implemented in class MyLoadingService) which asynchronously loads data into core data by another thread. This function has to continuously update the loading percentage (which is written in NSUserDefaults.standardUserDefaults()) to the main thread so that it could be shown on the progress bar in main view controller. I had ever used a while loop in MainViewController to continuously fetch the current percentage value, like below:
class MainViewController {
override func viewDidLoad() {
MyLoadingService.loadIntoCoreData() { result in
NSUserDefaults.standardUserDefaults().setBool(false, forKey: "isLoading")
// do something to update the view
}
self.performSelectorInBackground("updateLoadingProgress", withObject: nil)
}
func updatingLoadingProgress() {
let prefs = NSUserDefaults.standardUserDefaults()
prefs.setBool(true, forKey: "isLoading")
// here I use a while loop to listen to the progress value
while(prefs.boolForKey("isLoading")) {
// update progress bar on main thread
self.performSelectorOnMainThread("showLoadingProcess", withObject: nil, waitUntilDone: true)
}
prefs.setValue(Float(0), forKey: "loadingProcess")
}
func showLoadingProcess() {
let prefs = NSUserDefaults.standardUserDefaults()
if let percentage = prefs.valueForKey("loadingProcess") {
self.progressView.setProgress(percentage.floatValue, animated: true)
}
}
}
And in the class of function loadIntoCoreData:
class MyLoadingService {
let context = (UIApplication.sharedApplication()delegate as! AppDelegate).managedObjectContext!
func loadIntoCoreData(source: [MyModel]) {
var counter = 0
for s in source {
//load into core data using the class context
NSOperationQueue.mainQueue.addOperationWithBlock({
// updating the value of "loadingProcess" in NSUserDefaults.standardUserDefaults()
// and synchronize it on main queue
})
counter++
}
}
}
The above code can successfully run the progress bar, however it often encounter BAD_ACCESS or some other exceptions(like "Cannot update object that was never inserted") due to the conflicts on core data context (thought it seems that managedObjectContext isn't touched by the main thread). Therefore, instead of using a while loop listening on the main thread, I consider using NSOperationQueue.performSelectorOnMainThread to acknowledge the main thread after each entry. Therefore I put my view controller as an argument sender into loadCoreData and call performSelectorOnMainThread("updateProgressBar", withObject: sender, waitUntilDone: true) but failed with error "unrecognized selector sent to class 'XXXXXXXX'". So I would like to ask if is it possible to update an UI object between threads? Or, how to modify my previous solution so that the core data context conflicts could be solved? Any solutions are appreciated.
class MyLoadingService {
func loadIntoCoreData(sender: MainViewController, source: [MyModel]) {
var counter = 0
for s in source {
//load into core data using the class context
NSOperationQueue.mainQueue.addOperationWithBlock({
// updating the value of "loadingProcess" in NSUserDefaults.standardUserDefaults()
// and synchronize it on main queue
})
NSOperationQueue.performSelectorOnMainThread("updateProgressBar", withObject: sender, waitUntilDone: true)
counter++
}
}
func updateProgressBar(sender: MainViewController) {
sender.progressView.setProgress(percentage, animated: true)
}
}
class MainViewController {
override func viewDidLoad() {
MyLoadingService.loadIntoCoreData(self) { result in
// do something to update the view
}
}
}
First, you are abusing NSUserDefaults in horrible ways. The documentation describes it as this...
The NSUserDefaults class provides a programmatic interface for
interacting with the defaults system. The defaults system allows an
application to customize its behavior to match a user’s preferences.
For example, you can allow users to determine what units of
measurement your application displays or how often documents are
automatically saved. Applications record such preferences by assigning
values to a set of parameters in a user’s defaults database. The
parameters are referred to as defaults since they’re commonly used to
determine an application’s default state at startup or the way it acts
by default.
You are using it to store a global variable.
Furthermore, you are completely abusing the user's CPU in your loop where you continuously are checking the value in the user defaults, and clipping off a selector to the main thread. "Abuse of the CPU" doesn't even come close to describing what this code is doing.
You should use NSProgress for reporting progress. There is a WWDC 2015 presentation dedicated exclusively to using NSProgress.
On to your core data usage.
Unfortunately, since you intentionally redacted all of the core data code, it's impossible to say what is going wrong.
However, based on what I see, you are probably trying to use that managed object context from your app delegate (which is probably still created with the deprecated confinement policy) from a background thread, which is a cardinal sin of the highest order as far as core data is concerned.
If you want to import data as a long running operation, use a private context, and execute the operations in the background. Use NSProgress to communicate progress to anyone wanting to listen.
EDIT
Thanks for the advice on my core data context usage. I digged into all
the contexts in my code and re-organized the contexts inside, the
conflict problem does not happen anymore. As for NSProgress , it's a
pity that the WWDC presentation focus on the feature on iOS 9 (while
my app must compact on iOS 8 devices). However, even though I use
NSProgress, I should still tell the main thread how many data the core
data (on another thread) already has, right? How does the thread on
NSProgress know the loading progress on my core data thread? –
whitney13625
You can still use NSProgress for iOS8, then only real difference is that you can't explicitly add children, but the implicit way still works, and that video explains it as well.
You really should watch the whole video and forget about the iOS9 part, except to know that you must add children implicitly instead of explicitly.
Also, this pre-iOS9 blog post should clear up any questions you have about it.
I'm using a UIActivityIndicatorView to show the user that something is going on while I validate the login details. the code below shows what I'm doing:
func confirmLogin(sender: UIButton) {
ViewController.gVariables.gUser = userNameText.text
ViewController.gVariables.gPwd = passwordText.text
ViewController.gVariables.gLoggedIn = ""
indicatorView.startAnimating()
passwordText.text = ""
GetRemoteData.getValidationFromServer( { (svrData) -> Void in
dispatch_async(dispatch_get_main_queue()) {
self.indicatorView.stopAnimating()
self.indicatorView.hidden = true
MenuViewController.processLogin()
}
})
}
The user name and password are passed to the getValidationFromServer method in my GetRemoteData class which handles a lot of other web service activity. I set my indicator to start animating before calling the method but the following code should stop the indicator but it makes no difference as it is on another thread
What can I do to turn my indicator off?
The completion block of getValidationFromServer is dispatching the stopping of the activity indicator view back to the main queue. That is the correct way to do it. Your indicator view should be stopping when the code dispatched back to the main queue runs.
If the indicator view isn't stopping, there are only a few logical reasons why that might be happening:
You might have some other code elsewhere that is starting the indicatorView again (e.g. in processLogin).
You might have something that is blocking the main thread, preventing this code that was dispatched to the main thread from running.
Perhaps your completion block isn't getting called at all.
I would suggest putting a breakpoint or log message where you call stopAnimating and see if you're getting to that point at all. From there, you can diagnose the source of the problem. But the code provided in the question is not the source of the problem.