I'm attempting to show a loading spinner when I'm doing some network calls when my app first starts up from being closed. These network calls usually take a very small amount of time because they are GETs on a json string and some processing on them, but if they take longer than usual, I don't want my users trying to maneuver in the app without the data they need being there. So, I'm trying to show a spinner when these calls are going on. But the spinner never shows up. I had this working before I changed a lot of stuff, and now it's not working again, and I can't for the life of me figure out why.
Here's my viewDidLoad() method in my HomeViewController, where this information is pulled from the API and loaded into CoreData.
override func viewDidLoad() {
super.viewDidLoad()
self.showSpinner(onView: self.view)
let teamsByConferenceNetworkManager = TeamsByConferenceNetworkManager()
teamsByConferenceNetworkManager.getTeamsByConference(completion: { (data, error) in
guard let data = data else {
os_log("Could not unwrap teamsByConference data in LoginViewController.viewDidLoad()", type: .debug)
self.removeSpinner()
let _ = UIAlertAction(title: "Network unavailable", style: .cancel, handler: { (alert) in
alert.isEnabled = true
})
return
}
let dataModelManager = DataModelManager.shared
DispatchQueue.main.sync {
dataModelManager.loadTeamNamesByConference(teamNamesByConferenceName: data)
dataModelManager.loadGamesFromCoreData()
}
if let _ = dataModelManager.allGames {
self.removeSpinner()
return
} else {
let gamesNetworkManager = GamesNetworkManager()
gamesNetworkManager.getGames { (data, error) in
guard let data = data else {
os_log("Could not unwrap games data in LoginViewController.viewDidLoad()", type: .debug)
self.removeSpinner()
let _ = UIAlertAction(title: "Network unavailable", style: .cancel, handler: { (alert) in
alert.isEnabled = true
})
return
}
DispatchQueue.main.sync {
dataModelManager.loadGames(gameApiResponses: data)
}
}
}
})
self.removeSpinner()
}
You need to remove this
DispatchQueue.main.sync {
dataModelManager.loadGames(gameApiResponses: data)
}
}
}
})
self.removeSpinner(). <<<<<< this line
}
as the call is asynchronous and you remove the spinner directly after you add it with self.showSpinner(onView: self.view)
Related
So my goal here is to have the firestore query logic work every time instead of it working perfectly one time and not working properly every time after that. Currently, I have a function that determines whether a school user can delete an event or not based off the fact if a student has purchased a ticket for this event already.
If even one student has purchased a ticket for this event, the school user cannot delete this event, having this in place will prevent bugs and app crashes. The logic I use is simple, I do a query snapshot to check if there are actually students under the school ID of the school user, and then I do a second query snapshot to see if there are students that have purchased a ticket for the event already.
override func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? {
let deleteAction = UIContextualAction(style: .destructive, title: "Delete") { (deleted, view, completion) in
guard let user = Auth.auth().currentUser else { return }
let deleteIndex = client.index(withName: IndexName(rawValue: user.uid))
let documentid = self.documentsID[indexPath.row].docID
let algoliaID = self.algoliaObjectID[indexPath.row].algoliaObjectID
self.getTheSchoolsID { (id) in
guard let id = id else { return }
self.db.collection("student_users").whereField("school_id", isEqualTo: id).getDocuments { (querySnapshot, error) in
guard error == nil else { return }
for document in querySnapshot!.documents {
let userUUID = document.documentID
self.db.collection("student_users/\(userUUID)/events_bought").whereField("eventID", isEqualTo: documentid).getDocuments { (querySnapshotTwo, error) in
guard error == nil else { print(error)
return }
guard let query = querySnapshotTwo?.isEmpty else { return }
if query == false {
self.showAlert(title: "Students Have Purchased Tickets For This Event", message: "This event cannot be deleted until all students who have purchased a ticket for this event have completely refunded their purchase. Please be sure to make an announcement that this event will be deleted.")
return
} else {
print(querySnapshotTwo?.count)
let alert = UIAlertController(title: "Delete Event", message: "Are you sure you want to delete this event?", preferredStyle: .alert)
let deleteEvent = UIAlertAction(title: "Delete", style: .destructive) { (deletion) in
let batch = self.db.batch()
let group = DispatchGroup()
group.enter()
deleteIndex.deleteObject(withID: ObjectID(rawValue: algoliaID)) { (result) in
if case .success(_ ) = result {
group.leave()
}
}
group.notify(queue: .main) {
let docRef = self.db.document("school_users/\(user.uid)/events/\(documentid)")
batch.deleteDocument(docRef)
batch.commit { (err) in
guard err == nil else { return }
}
self.eventName.remove(at: indexPath.row)
tableView.reloadData()
}
}
alert.addAction(UIAlertAction(title: "Cancel", style: .cancel))
alert.addAction(deleteEvent)
self.present(alert, animated: true, completion: nil)
}
}
}
}
}
}
deleteAction.backgroundColor = UIColor.systemRed
let config = UISwipeActionsConfiguration(actions: [deleteAction])
config.performsFirstActionWithFullSwipe = false
return config
}
I show two different alerts based off the query result, if the query is empty, the event can be deleted as normal and this is what it would like if you try to delete it.
Now if a student has already purchased a ticket for this event, the school user should be seeing this alert every time:
The issue is that this works perfect the first time, first time as in when I first run the simulator or log out and log back in. Every time after that, it shows the other alert as if no students have purchased a ticket for it, but the purchase is still active which makes me so confused. To keep it brief, the code I am using works, but it doesn't work more than once which I can't figure out for the life of me.
Does anybody know how I can make this firestore query logic work perfect every time?
I have a tableViewController (ArticlesVC) that displays an array of Articles that are fetched from an API. Each of these tableViewCell has a "more action" button that allows the user to save the article for later reading in the SavedVC. Note that only saved articles are written to Realm DB, the displaying of articles in the ArticlesVC are not written to Realm DB.
If the user has saved the article, I would show "Remove saved article", otherwise, I show "Save for later". I query Realm to conduct this check.
However, if I delete a saved article (or all saved articles) from Realm from SavedVC and go back to the ArticlesVC and start swiping the tableView, it crashes with the above error. This crash happens on an intermittent basis, making it pretty hard to pinpoint.
Code
//At ArticlesVC, where articles are fetched via API
func getArticles() {
AF.request(apiUrl).responseJSON { (response) in
//Error checks and decoding ...
self.articles = try decoder.decode(Article.self, from: data)
}
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath) as! ArticleCell
cell.article = articles[indexPath.row]
cell.moreButton.tag = indexPath.row
cell.moreButton.addTarget(self, action: #selector(moreButtonTapped(sender:)), for: .touchUpInside)
return cell
}
#objc func moreButtonTapped(sender: UIButton) {
let buttonTag = sender.tag
let article = articles[buttonTag]
let alert = UIAlertController(title: "More", message: nil, preferredStyle: .actionSheet)
let action = getAction(article: article)
alert.addActions([action, .cancelAction()])
//present alert
}
func getAction(article: Article) -> UIAlertAction {
do {
let realm = try Realm()
let articleInRealm = realm.objects(Article.self).filter("id == %#", article.id)
if articleInRealm.isInvalidated {
return UIAlertAction(title: "Save for later", style: .default, handler: {(_) in
self.saveArticle(article: article)
})
} else {
if articleInRealm.count == 0 {
return UIAlertAction(title: "Save for later", style: .default, handler: {(_) in
self.saveArticle(article: article)
})
} else {
return UIAlertAction(title: "Remove saved article", style: .destructive, handler: {(_) in
self.deleteArticle(article: articleInRealm)
})
}
}
} catch {
Log("Err getting article: \(error.localizedDescription)")
return UIAlertAction(title: "Save for later", style: .default, handler: {(_) in
self.saveArticle(article: article)
})
}
}
func saveArticle(article: Article) {
do {
let realm = try Realm()
try realm.write {
realm.add(article, update: .modified)
}
} catch {
Log("Err saving article: \(error.localizedDescription)")
}
}
func deleteArticle(article: Results<Article>) {
do {
let realm = try Realm()
try realm.write {
realm.delete(article)
}
} catch {
Log("Err saving article: \(error.localizedDescription)")
}
}
//At SavedVC, which is another VC in the tabBarController.
//ArticlesVC is in tab1 and SavedVC is in tab2
var notificationToken: NotificationToken? = nil
var articles: Results<Article>?
override func viewDidLoad() {
super.viewDidLoad()
subscribeToRealmNotifications()
}
fileprivate func subscribeToRealmNotifications() {
let realm = try! Realm()
let results = realm.objects(Article.self)
notificationToken = results.observe { [weak self] (changes: RealmCollectionChange) in
guard let tableView = self?.tableView else { return }
switch changes {
case .initial:
self?.articles = results
tableView.reloadData()
case .update(_, _, _, _):
tableView.reloadData()
case .error(let error):
fatalError("Realm notif: \(error.localizedDescription)")
}
}
}
My guess is that because I have deleted the articles in SavedVC but ArticlesVC is still pointing to those Realm objects which no longer exist and therefore crashed.
I have read multiple SO posts that suggest to do a check on obj.isInvalidated, which I did and have included a condition for it. But this still crashes.
Other attempts includes:
using realm.create() instead of realm.add()
Here's the issue: You've saving an unmanaged object (article) into realm
func getAction(article: Article)
...
self.saveArticle(article: article)
which then makes it a managed object.
If it's deleted from Realm,
realm.delete(article)
it will also be removed from the results object that contains it
articles[indexPath.row]
Which will then make your app crash as it's scrolling as the dataSource and tableView are out of sync.
I have a feeling you don't want it to actually be removed from the master list of articles.
My suggestion is to save any articles you want to read later by their id which will have no effect on the master list of articles as they are added and removed from realm.
In other words your master list of articles stays the same but in Realm, you have a table of id's which are strings for example. So when you want to track what they want to read
self.saveArticleId(article: article.id)
and then when it's time to delete it from their saved reading list
self.deleteArticleBy(articleId: artical.id)
I want the getUserToken function and userLogin function to run before the next line which is the Firebase Authentication. For it to run ansynchronous
#IBAction func loginButtonPressed(_ sender: UIButton) {
self.showSpinner(onView: self.view)
guard var phoneNumber = phoneTextField.getRawPhoneNumber() else { return }
phoneNumber = "+234\(phoneNumber)"
guard var userPhoneNumber = phoneTextField.getRawPhoneNumber() else { return }
userPhoneNumber = "234\(userPhoneNumber)"
guard let userName = nameTextField.text else {return}
print(phoneNumber)
getUserAcessToken()
userLogin()
//Validate Required fields are mnot empty
if nameTextField.text == userName {
//Firebase Manipulation
PhoneAuthProvider.provider().verifyPhoneNumber(phoneNumber, uiDelegate: nil) { (verificationId, error) in
if error == nil {
print(verificationId!)
//UserDefaults Database manipulation for Verification ID
guard let verifyid = verificationId else {return}
self.defaults.set(verifyid, forKey: "verificationId")
self.defaults.synchronize()
self.removeSpinner()
}else {
print("Unable to get secret verification code from Firebase", error?.localizedDescription as Any)
let alert = UIAlertController(title: "Please enter correct email and phone number", message: "\n", preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "OK", style: .default, handler: nil))
self.present(alert, animated: true, completion: nil)
return
}
}
}
let OTPRequestVC = storyboard?.instantiateViewController(withIdentifier: "OTPRequestViewController") as! OTPRequestViewController
OTPRequestVC.userId = userId
OTPRequestVC.userEmailData = userEmail
self.present(OTPRequestVC, animated: true)
}
I want the two functions to run asynchronously before the firebase auth.
Its not a good idea to run the time consuming functions on the main thread.
My suggestions would be.
getUserAcessToken() and userLogin() functions Should have a callback. Which will make those functions run on a different thread (I believe those functions are making api call which is done in the background thread)
You could call userLogin() in the completion handler of getUserAcessToken() and then firebaseAuth in the completion handler of getUserAcessToken().
This will make sure that the UI is not hanged till you make those api calls and the user will know that something is going on in the app and the app is not hanged.
Without reproducing the entire intended functionality, the pattern you want to follow is:
func loginButtonPressed(_ sender: UIButton) {
// Any immediate changes to the UI here
// ...
// Start time consuming task in background
DispatchQueue.global(qos: .userInitiated).async {
getUserAccessToken()
userLogin()
// Make your Firebase call
PhoneAuthProvider.provider().verifyPhoneNumber(phoneNumber, uiDelegate: nil) { (verificationId, error) in
// Any response validation here
// ...
DispatchQueue.main.async {
// Any UI updates here
}
}
}
}
This is my first app and I'm wondering if I have made a mistake with regards to using URLSession.shared dataTask because I am not seeing my app get new data which is frequently updated. I see the new json instantly in my browser when I refresh, but, my app apparently does not see it.
Will it ever get the new json data from my server without uninstalling the app?
There are some some similar question topics, such as How to disable caching from NSURLSessionTask however, I do not want to disable all caching. Instead, I want to know how this default object behaves in this scenario - How long is it going to cache it? If indeed the answer is forever, or until they update or reinstall the app, then I will want to know how to reproduce the normal browser based cache behavior, using if-modified-since header, but that is not my question here.
I call my download() function below gratuitously after the launch sequence.
func download(_ ch: #escaping (_ data: Data?, _ respone: URLResponse?, _ error: Error?) -> (), completionHandler: #escaping (_ sessionError: Error?) -> ()) {
let myFileURL: URL? = getFileURL(filename: self.getFilename(self.jsonTestName))
let myTestURL = URL(string:getURLString(jsonTestName))
let session = URLSession.shared
// now we call dataTask and we see a CH and it calls My CH
let task = session.dataTask(with: myTestURL!) { (data, response, error) // generic CH for dataTask
in
// my special CH
ch(data,response,error) // make sure the file gets written in this ch
}
task.resume() // no such thing as status in async here
}
Within the completion handler which I pass to download, I save the data with this code from "ch":
DispatchQueue.main.async {
let documentController = UIDocumentInteractionController.init(url: myFileURL!)
documentController.delegate = self as UIDocumentInteractionControllerDelegate
}
and then finally, I read the data within that same completion handler from disk as such:
let data = try Data(contentsOf: myFileURL!)
For clarification, my complete calling function from which I call download() with completion handler code.
func get_test(){ // download new tests
let t = testOrganizer
let myFileURL: URL? = t.getFileURL(filename:t.getFilename(t.jsonTestName))
t.download( { (data,response,error)
in
var status: Int! = 0
status = (response as? HTTPURLResponse)?.statusCode
if(status == nil) {
status = 0
}
if(error != nil || (status != 200 && status != 304)) {
let alertController = UIAlertController(title: "Error downloading", message:"Could not download updated test data. HTTP Status: \(status!)", preferredStyle: UIAlertControllerStyle.alert)
alertController.addAction(UIAlertAction(title: "OK", style: UIAlertActionStyle.default,handler: nil))
self.present(alertController, animated: true, completion: nil)
self.p.print("END OF COMPLETION HANDLER")
}
else {
let status = (response as! HTTPURLResponse).statusCode
print("Success: status = ", status)
self.p.print("WRITING FILE IN COMPLETION HANDLER")
do {
try data!.write(to: myFileURL!)
DispatchQueue.main.async {
let documentController = UIDocumentInteractionController.init(url: myFileURL!)
documentController.delegate = self as UIDocumentInteractionControllerDelegate
}
} catch {
// // _ = completionHandler(NSError(domain:"Write failed", code:190, userInfo:nil))
print("error writing file \(myFileURL!) : \(error)")
}
self.myJson = self.testOrganizer.readJson()
self.p.print("END OF COMPLETION HANDLER")
}
}, completionHandler: {
sessionError in
if(sessionError == nil) {
print("Downloaded and saved file successfully")
} else {
let alertController = UIAlertController(title: "get_tests", message:
"Failed to download new tests - " + sessionError.debugDescription, preferredStyle: UIAlertControllerStyle.alert)
alertController.addAction(UIAlertAction(title: "Dismiss", style: UIAlertActionStyle.default,handler: nil))
self.present(alertController, animated: true, completion: nil)
}
})
}
I want to ask question about new UIAlertController. How can I detect the errors on custom class to show alert view to users ? I want to execute my alert view when the switch case goes the default statement. There is my NetworkOperation class which is custom class with closures for download some JSON data from web.
class NetworkOperation {
lazy var config: NSURLSessionConfiguration = NSURLSessionConfiguration.defaultSessionConfiguration()
lazy var session: NSURLSession = NSURLSession(configuration: self.config)
let mainURL: NSURL
typealias JSONWeatherDataDictionary = ([String: AnyObject]?) -> Void
init(url: NSURL) {
mainURL = url
}
func downloadWeatherDataFromWeb(completion: JSONWeatherDataDictionary) {
let request = NSURLRequest(URL: mainURL)
let dataTask = session.dataTaskWithRequest(request) {
(let data, response, error) in
if let httpResponse = response as? NSHTTPURLResponse {
switch httpResponse.statusCode {
case 200:
// HTTP Response success use the weather data !
do {
let weatherDataDictionary = try NSJSONSerialization.JSONObjectWithData(data!, options: .MutableContainers) as? [String: AnyObject]
completion(weatherDataDictionary)
} catch {
print("Data can't get")
}
default:
// I want to execute my alert view in there. showAlertView()
print("Http Response failed with this code: \(httpResponse.statusCode)")
}
} else {
print("HTTP Response convert fail !")
}
}
dataTask.resume()
}
}
If the default case is executed, how can I see my alert view in view controller ? I tried to solve this problem with UIAlertViewDelegate but its deprecated for iOS9 so I want to learn best common way to solve this problem.
Thank you everyone...
If all you would like to do is to present an alert that lets the user know about the error, providing along a cancel button to dismiss the button, you could make use of UIAlertController.
I tested the following code with a HTTP URL, causing it to fail, the alert view pops up displaying the status code; and upon tapping on the "cancel" button, dismisses itself.
func downloadWeatherDataFromWeb(completion: JSONWeatherDataDictionary)
{
let request = NSURLRequest(URL: mainURL)
let dataTask = session.dataTaskWithRequest(request)
{
(let data, response, error) in
if let httpResponse = response as? NSHTTPURLResponse
{
switch httpResponse.statusCode
{
case 200:
do
{
let weatherDataDictionary = try NSJSONSerialization.JSONObjectWithData(data!, options: .MutableContainers) as? [String: AnyObject]
completion(weatherDataDictionary)
}
catch
{
print("Could not retrieve data")
}
default:
print("Http Response failed with the following code: \(httpResponse.statusCode)")
let alert = UIAlertController(title: "HTTP Error", message:String(httpResponse.statusCode), preferredStyle: UIAlertControllerStyle.Alert)
//set up cancel action
let cancelAction: UIAlertAction = UIAlertAction(title: "Cancel", style: .Cancel)
{
action -> Void in
//should you want to carry some other operations upon canceling
}
//add the action
alert.addAction(cancelAction)
dispatch_async(dispatch_get_main_queue(),
{
self.presentViewController(alert, animated:true, completion:
{
//alert controller presented
})
})
}
}
else
{
print("HTTP Response conversion failed!")
}
}
dataTask.resume()
}
Update: in response to your comment
Please add a UIViewController variable:
class NetworkOperation
{
var viewController : UIViewController = UIViewController()
....
And make modification to the default: case above:
dispatch_async(dispatch_get_main_queue(),
{
self.addChildViewController(self.viewController)
self.view.addSubview(self.viewController.view)
self.viewController.didMoveToParentViewController(self)
self.viewController.presentViewController(alert, animated:true,
completion:
{
//alert controller presented
})
})
I just took the liberty of testing it a minute ago; it pops and dismisses.
Another Update:
Then, we could do the following:
dispatch_async(dispatch_get_main_queue(),
{
let topViewController = UIApplication.sharedApplication().keyWindow?.rootViewController
topViewController!.presentViewController(alert, animated:true,
completion:
{
//alert controller presented
})
})