I have a simple iOS application written in Swift that loads JSON data downloaded over the Internet and displays it into a UITableView. When I launch the application for the first time, everything works correctly and the table displays the correct information.
In my app, I have a button that triggers a refresh when I would like to reload the data. So I call the displayTable() method when the refresh button is tapped which downloads the data again and then calls the tableView.reloadData() method on the main thread to refresh the data but the reload does not appear to work. I manually change the data on the server and hit refresh but I still have the old data cached in my application. I can exit/kill the application and the old data stays 'stuck' forever unless I delete the app and reinstall.
I've attached the code for my ViewController below. I've spent way too many hours looking at this and I cannot seem to find why the reloadData doesn't work. Any ideas, hints would be greatly appreciated.
thanks
--Vinny
class ViewController: UITableViewController {
var tableData = []
override func viewDidLoad() {
super.viewDidLoad()
}
override func viewWillAppear(animated: Bool) {
println("viewWillAppear was just called")
super.viewWillAppear(true)
self.displayTable()
}
func displayTable() {
getJSONData("http://www.example.com/data.json") { (results, resultError) in
if (resultError == nil) {
if var results = results {
dispatch_async(dispatch_get_main_queue(), {
self.tableData = [] //create out data on subsequent refreshes
self.tableData = results
self.tableView.reloadData() //This doesn't appear to be working!
})
} else {
self.displayErrorPopup()
}
} else {
self.displayErrorPopup()
}
}
}
func displayErrorPopup() {
let alertViewController = UIAlertController(title: "Error", message: "Couldn't connect to API", preferredStyle: .Alert)
let okButton = UIAlertAction(title: "OK", style: .Default, handler: nil)
let cancelButton = UIAlertAction(title: "Cancel", style: .Cancel, handler: nil)
alertViewController.addAction(okButton)
alertViewController.addAction(cancelButton)
self.presentViewController(alertViewController, animated: true, completion: nil)
}
//todo - extract this method into it's own class
func getJSONData(ttAPIURL : String, completion: (resultsArray: NSArray?, resultError: NSError?) -> ()){
let mySession = NSURLSession.sharedSession()
let url: NSURL = NSURL(string: ttAPIURL)!
let networkTask = mySession.dataTaskWithURL(url, completionHandler : {data, response, error -> Void in
var err: NSError?
if (error == nil) {
var theJSON = NSJSONSerialization.JSONObjectWithData(data, options: NSJSONReadingOptions.MutableContainers, error: &err) as NSMutableDictionary
let results : NSArray = theJSON["list"]!["times"] as NSArray
completion(resultsArray: results, resultError: error)
} else {
completion(resultsArray: nil, resultError: error)
}
})
networkTask.resume()
}
override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return tableData.count
}
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("ttCell", forIndexPath: indexPath) as UITableViewCell
let timesEntry = self.tableData[indexPath.row] as NSMutableDictionary
cell.textLabel.text = (timesEntry["routeName"] as String)
return cell
}
override func numberOfSectionsInTableView(tableView: UITableView) -> Int {
return 1
}
#IBAction func refreshButtonTap(sender: AnyObject) {
self.displayTable()
}
}
Ok, so you say that reloadData is not working but that is almost certainly not your problem. To debug this you should:
Make sure reloadData is actually being called (breakpoint or println)
Check if the UITableViewDataSource methods are being called after you call reloadData (breakpoint or println)
Check the results array that you are getting back from the network request (probably println(results))
My guess is that it is failing at number 3 which means it has nothing to do with reloadData. Perhaps the shared NSURLSession has caching enabled? Try setting mySession.URLCache to nil.
Try to replace it
override func viewWillAppear(animated: Bool) {
println("viewWillAppear was just called")
super.viewWillAppear(true)
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0), { () -> Void in
self.displayTable()
})
}
Thanks drewag and Jirom for your answers. Great comments that were very helpful. I just found my problem and its something I should have thought about earlier. The domain which is hosting my dynamic JSON data is being protected by CloudFlare and it appears that my pages rules to disable caching for a certain directory aren't working. So all of the JSON content was being cached and that was the reason my application wouldn't show the new data.
Thanks again guys for your responses.
--Vinny
PS: Per Drewag's suggestion, I added an instance of NSURLSessionConfiguration which defines the behavior and policies to use when downloading data using an NSURLSession object, and set the URLCache property to nil to disable caching.
let config = NSURLSessionConfiguration.defaultSessionConfiguration()
config.URLCache = nil
let mySession = NSURLSession(configuration: config)
Related
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)
My current setup: TabViewController that is connected to two TableViewControllers embedded in navigation controllers. I am able to update the tableview no problem, on app load and when switching back between the different tabs.
My problem comes when I open another tableviewcontroller to edit. When I hit save from this view controller, it updates the data and saves everything just fine however, My tableView will not update no matter what I try unless I switch between the different tabs or close and reopen the app.
I have tried using delegates to trigger the update, I have tried use NSNotificationCenter, and no dice.
Has anybody else had this issue?
Here is my save function:
func saveDose(completion: (_ finished: Bool) -> ()) {
if searchName == "" {
guard let managedContext = appDelegate?.persistentContainer.viewContext else { return }
let dose = Dose(context: managedContext)
let doseNumberString = numberDosesText.text
let doseNumberInt = Int64(doseNumberString!) ?? 0
dose.name = nameText.text
dose.script = scriptText.text
dose.dosage = dosageText.text
// dose.doseInterval = doseInterval
dose.firstDose = datePicker.date
dose.numberDoses = doseNumberInt
dose.doseReminder = remindersSwitch.isOn
do {
try managedContext.save()
print("Data Saved")
completion(true)
} catch {
print("Failed to save data: ", error.localizedDescription)
completion(false)
}
} else {
update(name:searchName, firstDose: searchDate)
completion(true)
}
}
And here is where I call it and load back to my other tableview.
#IBAction func saveBtnPressed(_ sender: UIBarButtonItem) {
saveDose { (done) in
if done {
print("We need to return now")
navigationController?.popViewController(animated: true)
self.dismiss(animated: true, completion: nil)
} else {
print("Try again")
}
}
}
And here is where I reload my tableview when the view appears
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
print("View has been loaded back from other view")
self.fetchData()
self.tableView.reloadData()
print("View will appear")
}
And here is my fetchData and loadData functions
func fetchData() {
loadData { (done) in
if done {
setEmptyView(Array: logArray.count)
}
}
}
func loadData(completion: (_ complete: Bool) -> ()) {
guard let managedContext = appDelegate?.persistentContainer.viewContext else { return }
let request = NSFetchRequest<NSFetchRequestResult>(entityName: "Log")
do {
logArray = try managedContext.fetch(request) as! [Log]
print("Data Fetched No Issues")
completion(true)
} catch {
print("Unable to fetch data: ", error.localizedDescription)
completion(false)
}
}
Again I have tried delegates and have used this code in other applications that have worked fine. Is it something to do with the tab bar controller?
The data is obviously saving fine, I just can't get the tableView to update properly.
It seems like it is not calling the viewWillAppear function after the save. I have tried using delegates as well to force the update of the tableview but nothing has been working.
Basically you need to call tableView.reloadData(). after you have done saving.
Moreover you can use beginUpdates, endUpdates on tableView to perform animations on specific rows. for more information on this you may refer to This Link
Reload your tableview in custom delegates method your problem will be solved.
I'm implementing In-App purchases in an XCode project, and everything works fine except for one error. When the user isn't connected to the internet and he clicks a purchase button, the app crashes. I believe this happens because the in-app purchases haven't been fetched from iTunes and clicking the button can't run the purchase process. This crash also happens when the user clicks the button on the first second that the shop screen loads, because – I think – some time is needed to fetch (or request) the products. Here's what I'm talking about:
override func didMove(to view: SKView) {
...
fetchAvailableProducts()
}
func fetchAvailableProducts() {
// Put here your IAP Products ID's
let productIdentifiers = NSSet(objects:
productID100,
productID250,
productID500,
productIDRemoveAds,
productIDUnlockAll)
productsRequest = SKProductsRequest(productIdentifiers: productIdentifiers as! Set<String>)
productsRequest.delegate = self
productsRequest.start()
}
I'm basing my code on this tutorial.
Is there a way to change my code to make it "crash-proof", so that it first checks if the products can be bought to let you use the button?
I use SwiftyStorkeKit ( https://github.com/bizz84/SwiftyStoreKit ) , which uses a completion handler to fill in the products (will save you from reinventing the wheel as well - and is a great resource to learn from)
As for checking network connection, I use Apple's Reachability ( https://developer.apple.com/library/content/samplecode/Reachability/Introduction/Intro.html ). Here is the relevant parts of an implementation. It also check in situations where the app loses focus. You can also use the network check before any store operation.
class vcon: UIViewController {
#IBOutlet weak var noConnectionView: UIView!
func isNetworkAvailable() -> Bool {
//quick test if network is available
var netTest:Reachability? = Reachability(hostName: "apple.com")!
if netTest?.currentReachabilityStatus == .notReachable {
netTest = nil
return false
}
netTest = nil
return true
}
func displayNoNetworkView() {
//this example pulls from a storyboard to a view I have in front of everything else at all times, and shows the view to block everything else if the network isnt available
let ncvc = UIStoryboard(name: "HelpPrefsInfo", bundle: nil).instantiateViewController(withIdentifier: "noNetworkVC") as! noNetworkVC
ncvc.view.frame = noConnectionView.bounds
ncvc.view.backgroundColor = color03
ncvc.no_connection_imageView.tintColor = color01
ncvc.noInternetConnection_label.textColor = color01
noConnectionView.addSubview(ncvc.view)
}
func hideDataIfNoConnection() {
//the actual code that displays the no net connection view
if !isNetworkAvailable() {
if noConnectionView.isHidden == true {
noConnectionView.alpha = 0
noConnectionView.isHidden = false
self.iapObjects = []
UIView.animate(withDuration: 0.50, animations: {
self.noConnectionView.alpha = 1
}, completion:{(finished : Bool) in
});
}
} else {
if noConnectionView.isHidden == false {
self.collection_view.reloadData()
UIView.animate(withDuration: 0.50, animations: {
self.noConnectionView.alpha = 0
}, completion:{(finished : Bool) in
self.noConnectionView.isHidden = true
self.loadIAPData()
});
}
}
}
override func viewDidLoad() {
super.viewDidLoad()
NotificationCenter.default.addObserver(self, selector: #selector(willEnterForeground), name: .UIApplicationDidBecomeActive, object: nil)
displayNoNetworkView()
loadIAPData()
}
func loadIAPData() {
load the data if the network is available
if isNetworkAvailable() {
helper.requestProductsWithCompletionHandler(completionHandler: { (success, products) -> Void in
if success {
self.iapObjects = products!
self.collection_view.reloadData()
} else {
let alert = UIAlertController(title: "Error", message: "Cannot retrieve products list right now.", preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "OK", style: .default, handler: nil))
self.present(alert, animated: true, completion: nil)
}
})
}
}
func willEnterForeground() {
hideDataIfNoConnection()
}
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
hideDataIfNoConnection()
}
I have a UITableView with about 1000 rows. I also have a timer running every 6 seconds that fetches data from a web service. Each time I call reloadData() there is a blip - my app freezes very noticeably for a brief moment. This is very evident when scrolling.
I tried fetching about 400 rows only and the blip disappears. Any tips how to get rid of this while still fetching the 1000 rows?
var items: [Item] = []
Timer.scheduledTimer(withTimeInterval: 6, repeats: true) { [weak self] _ in
guard let strongSelf = self else { return }
Alamofire.request(urlString, method: method, parameters: params) { response in
// parse the response here and save it in array called itemsFromResponse
OperationQueue.main.addOperation {
strongSelf.items = itemsFromResponse
strongSelf.itemsTableView.reloadData()
}
}
}
UITableViewDataSource code:
extension ItemViewController: UITableViewDataSource, UITableViewDelegate {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return items.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "itemCell", for: indexPath)
cell.textLabel?.text = items[indexPath.row].name
return cell
}
}
The problem is being caused because you are storing the items from the response and then updating the table view from the same OperationQueue, meaning that the UI thread is being blocked while your array is being updated. Using an operation queue in itself is not an optimal way to schedule tasks if you do not need fine grain control over the task (such as cancelling and advanced scheduling, like you don't need here). You should instead be using a DispatchQueue, see here for more.
In order to fix your issue, you should update your array from the background completion handler, then update your table.
Timer.scheduledTimer(withTimeInterval: 6, repeats: true) { [weak self] _ in
guard let strongSelf = self else { return }
Alamofire.request(urlString, method: method, parameters: params) { response in
// parse the response here and save it in array called itemsFromResponse
strongSelf.items = itemsFromResponse
// update the table on the main (UI) thread
DispatchQueue.main.async {
strongSelf.itemsTableView.reloadData()
}
}
}
You should also maybe look into a more efficient way to fetch new data, because reloading the entire dataset every 6 seconds is not very efficient in terms of data or CPU on the user's phone.
The problem is you are reloading data every 6 seconds, so if the data is so big you're reloading 1000 rows every 6 seconds. I recommend you request the data and compare if there's new data so in that case you need to reload data or you simply ask to refresh once. For example:
var items: [Item] = []
Timer.scheduledTimer(withTimeInterval: 6, repeats: true) { [weak self] _ in
guard let strongSelf = self else { return }
Alamofire.request(urlString, method: method, parameters: params) { response in
// parse the response here and save it in array called itemsFromResponse
OperationQueue.main.addOperation {
if(strongSelf.items != itemsFromResponse){
strongSelf.items = itemsFromResponse
strongSelf.itemsTableView.reloadData()
}
}
}
I have a header view for every UITableViewCell. In this header view, I load a picture of an individual via an asynchronous function in the Facebook API. However, because the function is asynchronous, I believe the function is called multiple times over and over again, causing the image to flicker constantly. I would imagine a fix to this issue would be to load the images in viewDidLoad in an array first, then display the array contents in the header view of the UITableViewCell. However, I am having trouble implementing this because of the asynchronous nature of the function: I can't seem to grab every photo, and then continue on with my program. Here is my attempt:
//Function to get a user's profile picture
func getProfilePicture(completion: (result: Bool, image: UIImage?) -> Void){
// Get user profile pic
let url = NSURL(string: "https://graph.facebook.com/1234567890/picture?type=large")
let urlRequest = NSURLRequest(URL: url!)
//Asynchronous request to display image
NSURLConnection.sendAsynchronousRequest(urlRequest, queue: NSOperationQueue.mainQueue()) { (response:NSURLResponse!, data:NSData!, error:NSError!) -> Void in
if error != nil{
println("Error: \(error)")
}
// Display the image
let image = UIImage(data: data)
if(image != nil){
completion(result: true, image: image)
}
}
}
override func viewDidLoad() {
self.getProfilePicture { (result, image) -> Void in
if(result == true){
println("Loading Photo")
self.creatorImages.append(image!)
}
else{
println("False")
}
}
}
func tableView(tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
//Show section header cell with image
var cellIdentifier = "SectionHeaderCell"
var headerView = tableView.dequeueReusableCellWithIdentifier(cellIdentifier) as! SectionHeaderCell
headerView.headerImage.image = self.creatorImages[section]
headerView.headerImage.clipsToBounds = true
headerView.headerImage.layer.cornerRadius = headerView.headerImage.frame.size.width / 2
return headerView
}
As seen by the program above, I the global array that I created called self.creatorImages which holds the array of images I grab from the Facebook API is always empty and I need to "wait" for all the pictures to populate the array before actually using it. I'm not sure how to accomplish this because I did try a completion handler in my getProfilePicture function but that didn't seem to help and that is one way I have learned to deal with asynchronous functions. Any other ideas? Thanks!
I had the same problem but mine was in Objective-C
Well, the structure is not that different, what i did was adding condition with:
headerView.headerImage.image
Here's an improved solution that i think suits your implementation..
since you placed self.getProfilePicture inside viewDidLoad it will only be called once section==0 will only contain an image,
the code below will request for addition image if self.creatorImages's index is out of range/bounds
func tableView(tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
//Show section header cell with image
var cellIdentifier = "SectionHeaderCell"
var headerView = tableView.dequeueReusableCellWithIdentifier(cellIdentifier) as! SectionHeaderCell
if (section < self.creatorImages.count) // validate self.creatorImages index to prevent 'Array index out of range' error
{
if (headerView.headerImage.image == nil) // prevents the blinks
{
headerView.headerImage.image = self.creatorImages[section];
}
}
else // requests for additional image at section
{
// this will be called more than expected because of tableView.reloadData()
println("Loading Photo")
self.getProfilePicture { (result, image) -> Void in
if(result == true) {
//simply appending will do the work but i suggest something like:
if (self.creatorImages.count <= section)
{
self.creatorImages.append(image!)
tableView.reloadData()
println("self.creatorImages.count \(self.creatorImages.count)")
}
//that will prevent appending excessively to data source
}
else{
println("Error loading image")
}
}
}
headerView.headerImage.clipsToBounds = true
headerView.headerImage.layer.cornerRadius = headerView.headerImage.frame.size.width / 2
return headerView
}
You sure have different implementation from what i have in mind, but codes in edit history is not in vain, right?.. hahahaha.. ;)
Hope i've helped you.. Cheers!