In my WalletTableViewController I have two functions, used to calculate the Wallet Value:
A. updateCellValue() Is called by reloadData() with the tableView and uses indexPath.row to fetch a value (price) and an amount (number of coins) corresponding to the cell and make a calculation to get the total value of that coin (amountValue = value * amount). That is then saved with Core Data.
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath) as! WalletTableViewCell
cell.delegate = self
cell.amountTextField.delegate = self
updateCellValue(cell, atRow: indexPath.row)
return cell
}
func updateCellValue(_ walletTableViewCell: WalletTableViewCell, atRow row: Int) {
var newCryptos : [CryptosMO] = []
var doubleAmount = 0.0
if CoreDataHandler.fetchObject() != nil {
newCryptos = CoreDataHandler.fetchObject()!
}
cryptoPrice = cryptos[row].code!
guard let cryptoDoublePrice = CryptoInfo.cryptoPriceDic[cryptoPrice] else { return }
let selectedAmount = newCryptos[row]
guard let amount = selectedAmount.amount else { return }
var currentAmountValue = selectedAmount.amountValue
doubleAmount = Double(amount)!
let calculation = cryptoDoublePrice * doubleAmount
currentAmountValue = String(calculation)
CoreDataHandler.editObject(editObject: selectedAmount, amount: amount, amountValue: currentAmountValue)
updateWalletValue()
}
B. updateWalletValue() Is a function that fetches all the amountValue objects in Core Data and adds them together to calculate the Wallet Value.
func updateWalletValue() {
var items : [CryptosMO] = []
if CoreDataHandler.fetchObject() != nil {
items = CoreDataHandler.fetchObject()!
}
total = items.reduce(0.0, { $0 + Double($1.amountValue)! } )
WalletTableViewController.staticTotal = total
}
In my MainViewController, the Wallet Value is displayed too, but how can I refresh it's value?
func updateMainVCWalletLabel() {
//... what can I do here??
}
This works great for the WalletViewController of course with the TableView and indexPath, but how can I call updateCellValue from the MainViewController to keep the value updated?
The WalletViewController is instantiated and pushed from the MainViewController :
#IBAction func walletButtonTapped(_ sender: Any) {
let walletViewController = self.storyboard?.instantiateViewController(withIdentifier: "walletTableViewController")
self.present(walletViewController!, animated: true)
}
If you want to use a single method in multiple view controllers you should implement that method where you can call that method from anywhere. For example you can use singleton class here.
Create a swift file and name it as your wish (like WalletHelper or WalletManager)
Then you will get a file with the following format
class WalletHelper: NSObject
{
}
Create a shared instance for that class
static let shared = WalletHelper()
Implement the method you want
func getWalletValue() -> Float {
// write your code to get wallet value`
// and return the calculated value
}
Finally call that method like
let walletValue = WalletHelper.shared. getWalletValue()
WalletHelper.swift looks like
import UIKit
class WalletHelper: NSObject
{
static let shared = WalletHelper()
func getWalletValue() -> Float {
// write your code to get wallet value
// and return the calculated value
}
}
Update (old answer below)
To me it is absolutly unclear what you want to achieve: Which value do you want to be updated? The staticTotal?
Seems a litte like an XYProblem to me. As #vadian commented yesterday, please clearly describe where the data is stored, how the controllers are connected, what you want to update when in order to achieve what. You could also provide a MCVE which makes clear what you are asking, instead of adding more and more code snippets.
And, even more interesting: Why do you modify CoreData entries (CoreDataHandler.editObject) when you are in the call stack of tableView(_: cellForRowAt:)? Never ever ever do so! You are in a reading case - reloadData is intended to update the table view to reflect the data changes after the data has been changed. It is not intended to update the data itself. tableView(_: cellForRowAt:) is called many many times, especially when the user scrolls up and down, so you are causing large write impacts (and therefore: performance losses) when you write into the database.
Old Post
You could just call reloadData on the table view, which then will update it's cells.
There are also a few issues with your code:
Why do you call updateWalletValue() that frequently? Every time a cell is being displayed, it will be called, run through the whole database and do some reduce work. You should cache the value and only update it if the data itself is modified
Why do you call fetchObject() twice (in updateWalletValue())?
You should do this:
func updateWalletValue() {
guard let items:[CryptosMO] = CoreDataHandler.fetchObject() else {
WalletTableViewController.staticTotal = 0.0
return
}
total = items.reduce(0.0, { $0 + Double($1.amountValue)! } )
WalletTableViewController.staticTotal = total
}
Related
I'm having difficulty finding the use of NSDiffableDataSourceSnapshot reloadItems(_:):
If the item I ask to reload is not equatable to an item that is already present in the data source, I crash with:
Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'Attempted to reload item identifier that does not exist in the snapshot: ProjectName.ClassName
But if the item is equatable to an item that is already present in the data source, then what's the point of "reloading" it?
You might think the answer to the second point is: well, there might be some other aspect of the item identifier object that is not part of its equatability but does reflect into the cell interface. But what I find is that that's not true; after calling reloadItems, the table view does not reflect the change.
So when I want to change an item, what I end up doing with the snapshot is an insert after the item to be replaced and then a delete of the original item. There is no snapshot replace method, which is what I was hoping reloadItems would turn out to be.
(I did a Stack Overflow search on those terms and found very little — mostly just a couple of questions that puzzled over particular uses of reloadItems, such as How to update a table cell using diffable UITableView. So I'm asking in a more generalized form, what practical use has anyone found for this method?)
Well, there's nothing like having a minimal reproducible example to play with, so here is one.
Make a plain vanilla iOS project with its template ViewController, and add this code to the ViewController.
I'll take it piece by piece. First, we have a struct that will serve as our item identifier. The UUID is the unique part, so equatability and hashability depend upon it alone:
struct UniBool : Hashable {
let uuid : UUID
var bool : Bool
// equatability and hashability agree, only the UUID matters
func hash(into hasher: inout Hasher) {
hasher.combine(uuid)
}
static func ==(lhs:Self, rhs:Self) -> Bool {
lhs.uuid == rhs.uuid
}
}
Next, the (fake) table view and the diffable data source:
let tableView = UITableView(frame: .zero, style: .plain)
var datasource : UITableViewDiffableDataSource<String,UniBool>!
override func viewDidLoad() {
super.viewDidLoad()
self.tableView.register(UITableViewCell.self, forCellReuseIdentifier: "cell")
self.datasource = UITableViewDiffableDataSource<String,UniBool>(tableView: self.tableView) { tv, ip, isOn in
let cell = tv.dequeueReusableCell(withIdentifier: "cell", for: ip)
return cell
}
var snap = NSDiffableDataSourceSnapshot<String,UniBool>()
snap.appendSections(["Dummy"])
snap.appendItems([UniBool(uuid: UUID(), bool: true)])
self.datasource.apply(snap, animatingDifferences: false)
}
So there is just one UniBool in our diffable data source and its bool is true. So now set up a button to call this action method which tries to toggle the bool value by using reloadItems:
#IBAction func testReload() {
if let unibool = self.datasource.itemIdentifier(for: IndexPath(row: 0, section: 0)) {
var snap = self.datasource.snapshot()
var unibool = unibool
unibool.bool = !unibool.bool
snap.reloadItems([unibool]) // this is the key line I'm trying to test!
print("this object's isOn is", unibool.bool)
print("but looking right at the snapshot, isOn is", snap.itemIdentifiers[0].bool)
delay(0.3) {
self.datasource.apply(snap, animatingDifferences: false)
}
}
}
So here's the thing. I said to reloadItems with an item whose UUID is a match, but whose bool is toggled: "this object's isON is false". But when I ask the snapshot, okay, what have you got? it tells me that its sole item identifier's bool is still true.
And that is what I'm asking about. If the snapshot is not going to pick up the new value of bool, what is reloadItems for in the first place?
Obviously I could just substitute a different UniBool, i.e. one with a different UUID. But then I cannot call reloadItems; we crash because that UniBool is not already in the data. I can work around that by calling insert followed by remove, and that is exactly how I do work around it.
But my question is: so what is reloadItems for, if not for this very thing?
(I've filed a bug on the behavior demonstrated in the question, because I don't think it's good behavior. But, as things stand, I think I can provide a guess as to what the idea is intended to be.)
When you tell a snapshot to reload a certain item, it does not read in the data of the item you supply! It simply looks at the item, as a way of identifying what item, already in the data source, you are asking to reload.
(So, if the item you supply is Equatable to but not 100% identical to the item already in the data source, the "difference" between the item you supply and the item already in the data source will not matter at all; the data source will never be told that anything is different.)
When you then apply that snapshot to the data source, the data source tells the table view to reload the corresponding cell. This results in the data source's cell provider function being called again.
OK, so the data source's cell provider function is called, with the usual three parameters — the table view, the index path, and the data from the data source. But we've just said that the data from the data source has not changed. So what is the point of reloading at all?
The answer is, apparently, that the cell provider function is expected to look elsewhere to get (at least some of) the new data to be displayed in the newly dequeued cell. You are expected to have some sort of "backing store" that the cell provider looks at. For example, you might be maintaining a dictionary where the key is the cell identifier type and the value is the extra information that might be reloaded.
This must be legal, because by definition the cell identifier type is Hashable and can therefore serve as a dictionary key, and moreover the cell identifiers must be unique within the data, or the data source would reject the data (by crashing). And the lookup will be instant, because this is a dictionary.
Here's a complete working example you can just copy and paste right into a project. The table portrays three names along with a star that the user can tap to make star be filled or empty, indicating favorite or not-favorite. The names are stored in the diffable data source, but the favorite status is stored in the external backing store.
extension UIResponder {
func next<T:UIResponder>(ofType: T.Type) -> T? {
let r = self.next
if let r = r as? T ?? r?.next(ofType: T.self) {
return r
} else {
return nil
}
}
}
class TableViewController: UITableViewController {
var backingStore = [String:Bool]()
var datasource : UITableViewDiffableDataSource<String,String>!
override func viewDidLoad() {
super.viewDidLoad()
let cellID = "cell"
self.tableView.register(UITableViewCell.self, forCellReuseIdentifier: cellID)
self.datasource = UITableViewDiffableDataSource<String,String>(tableView:self.tableView) {
tableView, indexPath, name in
let cell = tableView.dequeueReusableCell(withIdentifier: cellID, for: indexPath)
var config = cell.defaultContentConfiguration()
config.text = name
cell.contentConfiguration = config
var accImageView = cell.accessoryView as? UIImageView
if accImageView == nil {
let iv = UIImageView()
iv.isUserInteractionEnabled = true
let tap = UITapGestureRecognizer(target: self, action: #selector(self.starTapped))
iv.addGestureRecognizer(tap)
cell.accessoryView = iv
accImageView = iv
}
let starred = self.backingStore[name, default:false]
accImageView?.image = UIImage(systemName: starred ? "star.fill" : "star")
accImageView?.sizeToFit()
return cell
}
var snap = NSDiffableDataSourceSnapshot<String,String>()
snap.appendSections(["Dummy"])
let names = ["Manny", "Moe", "Jack"]
snap.appendItems(names)
self.datasource.apply(snap, animatingDifferences: false)
names.forEach {
self.backingStore[$0] = false
}
}
#objc func starTapped(_ gr:UIGestureRecognizer) {
guard let cell = gr.view?.next(ofType: UITableViewCell.self) else {return}
guard let ip = self.tableView.indexPath(for: cell) else {return}
guard let name = self.datasource.itemIdentifier(for: ip) else {return}
guard let isFavorite = self.backingStore[name] else {return}
self.backingStore[name] = !isFavorite
var snap = self.datasource.snapshot()
snap.reloadItems([name])
self.datasource.apply(snap, animatingDifferences: false)
}
}
Based on your new example code, I agree, it looks like a bug. When you add a reloadItems to a snapshot it correctly triggers the datasource closure to request an updated cell, but the IdentifierType item that is passed to the closure is the original, not the new value that was provided with the reloadItems call.
If I changed your UniBool struct to a class so that it is a reference rather than a value type, then things worked as expected (since there is now a single instance of a UniBool rather than a new one with the same identifier).
It seems at the moment there are a couple of possible work-arounds:
Use a reference rather than a value type for the IdentifierType
Use an additional backing store, such as an array, and access it via indexPath in the datasource closure.
I don't think that either of these are ideal.
Interestingly, after I changed UniBool to a class, I tried creating a new instance of UniBool that had the same uuid as the existing instance and reloading that; The code crashed with an exception stating Invalid item identifier specified for reload; This doesn't sound right to me; Only the hashValue should matter, not the actual object reference. Both the original and the new objects had the same hashValue and == returned true.
Original answer
reloadItems works, but there are two important points:
You must start with the datasource's current snapshot and call reloadItems on that. You can't create a new snapshot.
You can't rely on the item passed to the CellProvider closure for anything other than the identifier - It doesn't represent the most recent data from your backing model (array).
Point 2 means that you need to use the provided indexPath or item.id to obtain your updated object from your model.
I created a simple example that displays the current time in a table row; This is the data source struct:
struct RowData: Hashable {
var id: UUID = UUID()
var name: String
private let possibleColors: [UIColor] = [.yellow,.orange,.cyan]
var timeStamp = Date()
func hash(into hasher: inout Hasher) {
hasher.combine(self.id)
}
static func ==(lhs: RowData, rhs: RowData) -> Bool {
return lhs.id == rhs.id
}
}
Note that despite the hash function only using the id property it is also necessary to override == or you will get a crash with an invalid identifier when you attempt to reload the row.
Each second a random selection of rows are reloaded. When you run the code you see that the time is updated on those randomly selected rows.
This is the code that uses reloadItems:
self.timer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { (timer) in
guard let datasource = self.tableview.dataSource as? UITableViewDiffableDataSource<Section,RowData> else {
return
}
var snapshot = datasource.snapshot()
var rowIdentifers = Set<RowData>()
for _ in 0...Int.random(in: 1...self.arrItems.count) {
let randomIndex = Int.random(in: 0...self.arrItems.count-1)
self.arrItems[randomIndex].timeStamp = Date()
rowIdentifers.insert(self.arrItems[randomIndex])
}
snapshot.reloadItems(Array(rowIdentifers))
datasource.apply(snapshot)
}
I posted the same question, not realising. I got this working by firstly converting my model to classes. Then calling 'applySnapshot' after calling 'reloadItems'.
func toggleSelectedStateForItem(at indexPath: IndexPath, animate: Bool = true) {
let item = dataSource.itemIdentifier(for: indexPath)!
var snapshot = dataSource.snapshot()
item.isSelected = !item.isSelected
snapshot.reloadItems([item])
dataSource.apply(snapshot)
}
I found (via Swift Senpai) that the way you update these diffabledatasource depends on if your model is a class (pass by reference) or struct (pass by value). In the pass by reference you can take the item, update it, then reload the item:
// Model is a class compliant with Hasable and Equatable, name String property
guard let selectedItem = dataSource.itemIdentifier(for: indexPath) else { return}
// modify item
selectedItem.name = "new name"
// update the snapshot
var newSnapShot = dataSource.snapshot()
newSnapshot.reloadItems([selectedItem])
dataSource.apply(newSnapshot)
So the above code will work with a model that is a class (the class needs to explicitly implement hast(into:) and ==(lhs:rhs:)).
On the other hand, a struct requires you to copy the item, update it, then insert the updated item and delete the old item from the snapshot.
// Model is a struct with name String property
guard let selectedItem = dataSource.itemIdentifier(for: indexPath) else { return}
// update the item
var updatedSelectedItem = selectedItem
updatedSelectedItem.name = "new name"
// update snapshot
var newSnapShot = dataSource.snapshot()
newSnapshot.insertItems([updatedSelectedItem], beforeItem: selectedItem)
newSnapshot.deleteItems([selectedItem])
dataSource.apply(newSnapshot)
These worked for me.
Every time different users post something (let's say a color) I get the color they posted, the postID, their userId, the date in secs, and how many times that post was viewed.
A different user can look through a tableView and view different cells with every color that every user posted .
Every time that user who is looking taps didSelectRow to view a detail view of the color I run a Firebase TransactionBlock that increases a views count property to show how many times that particular color/cell was tapped.
For eg if the user scrolls through a tableView and see's a blueCell, a label will be on it that says views: 10 (meaning it was viewed 10 times). If that user presses that blueCell again then the views count will go show views: 11.
The problem is if that user presses that cell repeatedly then they can increase the count on that views label in matter of seconds.
How can I keep track of every object/cell that the user taps and put a timer on it so that they can't update the views count for that particular object for possibly another hour or so? I have the date in secs and postId which are unique to each object.
Basically if the user presses the blueCell at 12pm the views count for the object associated with that particular cell will go up to 11 but if they press it again anytime in between 12pm - 1pm it won't go up. After 1pm if they press it again it the views count for that object will go up to 12?
The model object and the properties I can use to identify each color object:
class ColorClass{
var color: String?
var postID: String?
var userId: String?
var date: NSNumber?
var views: NSNumber? // keeps track of how many the post was viewed
}
TableView's didSelectRow:
// the current user who is pressing the cell
let currentUserID = Auth.auth().currentUser?.uid
var colors = [ColorClass]() // 500 model objects
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return colors.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "ColorsCell", for: indexPath) as! ColorsCell
cell.viewsLabel.text = colors[indexPath.row].views // I separately convert this from a NSNumber to a String
cell.colorLabel.text = colors[indexPath.row].color
return cell
}
// pressing the cell will increase the count on the object's views property
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
guard let indexPath = tableView.indexPathForSelectedRow else { return }
// the userId on the object of the cell that was pressed
guard let userID = colors[indexPath.row].userId else { return }
guard let postID = colors[indexPath.row].postId else { return }
// make sure the current user can't update the views on their own post
if currentUserID != userID{
let viewsRef = databaseRef?.child(userID).child(postID).child("views")
viewsRef?.runTransactionBlock({
(currentData: MutableData) -> TransactionResult in
let newValue: Int
guard let existingValue = (currentData.value as? NSNumber)?.intValue else {
return TransactionResult.abort()
}
newValue = existingValue + 1
currentData.value = NSNumber(value: newValue)
return TransactionResult.success(withValue: currentData)
}, andCompletionBlock: {
(error, completion, snap) in
print(snap as Any)
if !completion{
print("The value wasn't able to update")
print(error?.localizedDescription as Any)
}else{
print("The value updated")
}
})
}
}
Just an idea.
I thought about creating another object that would have a currentUserID, postID, and tappedTime properties. Then I would create a singleton. Every time a cell is pressed I’d pass the data into the object then send the object over to an array in the singleton. In there I’d have a currentTime property. First I’d check if the postID is in the array and if so I’d compare the tappedTime to the currentTime + 1 hour to decide if the views count should get increased. I’d have a dispatch asynch timer and after 1 hour it would automatically get purged from the array. I’m not sure how practical it is though.
You could create a typealias consisting of whatever the object is you're populating your cells with and a Date at the top of your view controller, like so:
typealias ColorLastSelected = (colorClass: ColorClass, timeSelected: Date)
Then, create an array to store the ColorLastSelected objects.
var selectedColors: [ColorLastSelected] = []
From there, in didSelectRow, you could do a guard statement to check if an object is contained within the selectedColors array. If not, then do whatever it is you've got to do and at the end, initialize a ColorLastSelected object and append it to the selectedColors array.
In terms of keeping the selectedColors up to date, you could run an update method on a repeating timer to remove ColorLastSelecteds that are over 1 hour old. Alternatively, you could just filter the selectedColors array before the guard statement to remove stuff that's over an hour old. If you're going to be jumping around between view controllers, you may need to create a singleton that "stays alive" or you could persist the selectedColors array somewhere
The idea I had at the bottom of the question worked.
I basically made a ViewsTrackingObject with a property specifically for the postId
I then made a singleton that adds the viewsTrackingObject to an array, checks to see if its in the array, if not add it to the array, then remove it from the array after xxx secs.
For this example I set it to 15 secs inside step 9: .now() + 15 but if I wanted it for an hour I would change it to .now() + 3600.
I find it easier to explain things in steps. There are 0 - 21 steps. I listed the steps as commented out code above each corresponding piece of code starting at the top of the Tracker class with step 0 and it ends the bottom of didSelectRow with step 21
ViewsTrackingObject:
class ViewsTrackingObject{
var postId: String?
}
Singleton Class:
class Tracker{
static let sharedInstance = Tracker()
var viewsObjects = [ViewsTrackingObject]()
var updateCount = false // 0. need to access this inside didSelectRow (step 17 )to find out wether or not to update the number of views. This would set to true in step 3 below
func checkForObjectInArray(object: ViewsTrackingObject){
// 1. check to see if the object is in the array. If it is return true if not return false. Use dot notation to compare the postId on the viewsTrackingObject vs what's inside the array to find out if it exists
let boolVal = viewsObjects.contains(where: {$0.postId == object.postId})
// 2. if the object is NOT inside the array then append to the array and then add it to the function that will remove it from the array in whatever secs you specify from the moment it's added. I specified 15 secs
if !boolVal{
updateCount = true // 3. change this to true which means in didSelectRow in step 18 return TransactionResult.success(withValue: currentData) will run
viewsObjects.append(object) // 4. add it to the above array property
removeObjectFromArray(object) // 5. will remove the viewsTrackingObject passed into the object parameter above in 15 secs from now. Look at step 9
}
}
// 6. this is called above when an object is appended to the array
func removeObjectFromArray(_ object: ViewsTrackingObject){
// 7. even though the object should definitely be inside the array double check. If it's in there return true if not return false
let boolVal = viewsObjects.contains(where: {$0.postId == object.postId})
// 8. if the object is in the array which mean the boolVal is true then proceed to step 9
if boolVal{
// 9. Fire off in 15 secs from now
DispatchQueue.main.asyncAfter(deadline: .now() + 15) {
// 10. find the index of the viewsTrackingObject inside the array
if let index = self.views.index(where: {$0.postId == viewsModel.postId}){
// 11. remove the viewsTrackingObject at the corresponding index from the array
self.viewsObjects.remove(at: index)
print("++++SUCCESS OBJECT REMOVED++++") // in 15 secs these print statements will print to the console
print("----viewsObjects count: \(views.count)")
print("....viewsObjects items: \(views.description)")
}
}
}
}
}
The class that contains the tableView. Declare a property for the Tracker's sharedInstance so everything runs through the Singleton class
// 12. This is declared as a class property and it's used in didSelectRow. Its the Singleton Class
let tracker = Tracker.sharedInstance
let currentUserID = Auth.auth().currentUser?.uid // the current user who is pressing the cell
var colors = [ColorClass]() // 500 model objects
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
guard let indexPath = tableView.indexPathForSelectedRow else { return }
// 14. Get the postId of the colorObject corresponding to the tapped cell
guard let postID = colors[indexPath.row].postId else { return }
guard let userID = colors[indexPath.row].userId else { return } // the userId on the object of the cell that was pressed. This is used as a child in the databaseRef below to update the user's view's property
// make sure the current user can't update the views on their own post
if currentUserID != userID{
// 15. Create a ViewsTrackingObject and set it's postID property to the same postId property from step 14
let viewsTrackingObject = ViewsTrackingObject()
viewsTrackingObject.postId = postID
// 16. using the tracker's shared instance, call the method to find out if the object is currently inside the Singleton's array
tracker.checkForObjectInArray(object: viewsTrackingObject)
let viewsRef = databaseRef?.child(userID).child(postID).child("views")
viewsRef?.runTransactionBlock({
(currentData: MutableData) -> TransactionResult in
let newValue: Int
guard let existingValue = (currentData.value as? NSNumber)?.intValue else {
return TransactionResult.abort()
}
newValue = existingValue + 1
currentData.value = NSNumber(value: newValue)
// 17. check to see if the singleton's updateCount property was set to true in step 3. If is true then proceed to step 18
if self.tracker.updateCount{
// 18. reset singleton's updateCount property back false since it was set to true in step 3
self.tracker.updateCount = false
print("*****Views Updated")
return TransactionResult.success(withValue: currentData)
}
// 19. if the singleton's updateCount property was false to begin with then the views won't get updated in firebase because the transaction will get aborted
print("=====Views NOT Updated")
return TransactionResult.abort()
}, andCompletionBlock: {
(error, completion, snap) in
print(snap as Any)
if !completion{
// 20. If something went wrong reset singleton's updateCount property back false
self.tracker.updateCount = false
print("The value wasn't able to update")
print(error?.localizedDescription as Any)
}else{
// 21. it's unnecessary but to be on the safe side
self.tracker.updateCount = false
print("The value updated")
}
})
}
}
So I'm trying to make an app that lists the times the ISS is scheduled to Pass over the user's location. It works more or less smoothly, with one hiccup.
What I expect: Around 5 different times listed in a UITableView
What I get: 5 times listed in a UITableView with randomly repeated values. Sometimes all 1 value, sometimes the last one is also the second and/or 3rd to last, sometimes 2 values repeat themselves, any number of incorrect combinations. A small portion of tests return correctly.
What bugs me most is that the wrong results are inconsistent, so I can't see a way to brute force a crude solution.
Relevant code:
First the network manager class/delegate
import Foundation
import CoreLocation
//Delegate for thread communication
protocol NetworkManagerDelegate: class {
func didGetPass(lastPass: Bool, pass: Pass)
//Flag last model object to limit tableview reloads
}
//Using struct as manager class is not going to change state
struct NetworkManager {
weak var delegate: NetworkManagerDelegate?
//Set a base URL as far along as possible, will finish with data passed from function
let baseURL = "http://api.open-notify.org/iss-pass.json?"
func getPasses(coordinate: CLLocationCoordinate2D) {
//complete URL
let lat = coordinate.latitude
let lon = coordinate.longitude
let requestURL = URL(string: "\(baseURL)lat=\(lat)&lon=\(lon)")
//begin task
let task = URLSession.shared.dataTask(with: requestURL!) { (data, response, error) in
if error != nil {
print(error as Any)
//Generic report on failure to connect
print("Could not reach API")
} else {
do {
//Get JSON from data to parse
if let resultJSON = try JSONSerialization.jsonObject(with: data!, options: .mutableContainers) as? [String: Any] {
//Get list of passes from JSON
let passes = resultJSON["response"] as! [[String: Int]]
//Set default parameters for delegate method
var testPass: Pass
var lastPass = false
//loop through passes
for pass in passes {
//determine if last pass
if pass == passes.last! {
lastPass = true
}
testPass = Pass()/*This seems terribly inefficient to me
However, attempting to create the object outside the loop and simply modify it
leads to the same exact object being passed multiple times to the main thread, so
the tableview's cells are all identical.*/
testPass.durationInSeconds = pass["duration"] ?? 0
//Convert date to Date object and set to testPass
testPass.riseTime = Date(timeIntervalSince1970: (Double(pass["risetime"] ?? 0)))
//Inform main thread via delegate
DispatchQueue.main.async {
self.delegate?.didGetPass(lastPass: lastPass, pass: testPass)
}
}
}
} catch {
print(error.localizedDescription)
}
}
}
task.resume()
}
}
And on the main thread:
extension TrackingController: UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return passes.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
//Get cell, pass default if nil
guard let cell = tableView.dequeueReusableCell(withIdentifier: "passCell") as? PassCell else {
return PassCell()
}
//Retrieve data from passes, assign to labels
let pass = passes[indexPath.row]
cell.timeLabel.text = "\(dateFormatter.string(from: pass.riseTime))"
cell.durationLabel.text = "Duration: \(pass.durationInSeconds) Seconds"
return cell
}
}
extension TrackingController: NetworkManagerDelegate {
func didGetPass(lastPass: Bool, pass: Pass) {
passes.append(pass)
if lastPass { //reload only once api calls are done
passList.reloadData()
}
}
}
That should be all code that triggers from the moment the Network Manager's one function is called. Can anyone explain this to me? I have manually checked the API, I should not be receiving duplicate outputs here.
Edit: I just tried changing DispatchQueue.main.async to DispatchQueue.main.sync, and while it seems to fix the issue my understanding of multithreading leads me to believe this defies the point of running the call on another thread. That said, my understanding of multithreading isn't very practical, so I'm open to correction on that or to better solutions to my problem.
Ideally, on the main thread, you don't do anything with the data until the last object has been received. So, passing the whole array in one go is a better approach.
You can do this instead:
func didGetPasses(_ passes: Passes) {
passes.append(passes);
passList.reloadData();
}
And call
self.delegate?.didGetPasses(passes)
once the for loop is completed.
PS: You should also consider using closures. It helps in handling logic and makes code more 'in-place' at the call site.
Is
guard let cell = tableView.dequeueReusableCell(withIdentifier: "passCell") as? PassCell else {
return PassCell()
}
correct? seems you are returning an uninitialised PassCell there
I decided on an answer inspired by advice from Kunal Shah. I stopped looping the delegate call and instead put together a collection of Pass objects to send to the main thread.
I didn't want to do this as my previous experience with this led to empty collections being sent for some reason, but it seems to work here. This way I still only reload the tableview once, but also only call the delegate method once.
Update Note: This question does not seem to be a duplicate of How can I fix crash when tap to select row after scrolling the tableview? because my issue is specifically about one of my CoreData objects going nil after the table scrolls. But the cell returns data as long as the app doesnt crash due to the nil from the topic object.
Basically this part fails: let estimatedSkills = topic.topicEstimatedSkill and let topic = self.topics!.object(at: indexPath.row) as! Topic even though at first, a debugprint shows that the object is not nil, after scroll, self.topic becomes nil.
Original:
I hope you can help me because I am a beginner in Swift and apple development in general.
I inherited an ios app that didnt work because the structure of data returned from our server changed. The previous developers cant be reached, so I am trying to make out what to do here. On top of that, in the middle of development, apple forced me to update to swift3 and xcode8 changing the gamerules making everything even more confusing.
So basically I have a tableview that gets data from an object fetched from CoreData. It fills out the cells when initiated, but scrolling makes the data from the object return nil.
The object in question is named Topic
and the code fails at:
//TODO - this sometimes gives nil but shouldnt
if let estimatedSkills = topic.topicEstimatedSkill {
value = estimatedSkills.doubleValue * Double(cell.starViewContainer.subviews.count);
debugPrint("success getting estimated skills!" , indexPath, estimatedSkills)
} else {
debugPrint("Didnt get estimated skills :(")
}
Then when I return to the original cells, the info in those are gone too.
Here's the tableview function:
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: Identifiers.cells.superTrainer.rawValue, for: indexPath) as! SuperTrainerOverViewCell;
cell.backgroundColor = UIColor.clear;
let topic = self.topics!.object(at: indexPath.row) as! Topic;
cell.titleLabel.text = topic.topicName;
var value = 0.0
//TODO - this sometimes gives nil but shouldnt
if let estimatedSkills = topic.topicEstimatedSkill {
value = estimatedSkills.doubleValue * Double(cell.starViewContainer.subviews.count);
debugPrint("success getting estimated skills!" , indexPath, estimatedSkills)
} else {
debugPrint("Didnt get estimated skills :(")
}
let wholeStar = Int(floor(value));
for index in (0..<wholeStar) {
let imageView = cell.starViewContainer.subviews[index] as! UIImageView;
imageView.image = UIImage(named: "icon-star-100");
}
if(wholeStar < cell.starViewContainer.subviews.count) {
let decimal = fmod(value, 1);
var image = UIImage(named: "icon-star-100");
if(decimal < 0.15) {
image = UIImage(named: "icon-star-0");
}else if(decimal >= 0.15 && decimal < 0.4) {
image = UIImage(named: "icon-star-25");
}else if(decimal >= 0.4 && decimal < 0.65) {
image = UIImage(named: "icon-star-50");
}else if(decimal >= 0.65 && decimal < 0.80) {
image = UIImage(named: "icon-star-75");
}
let imageView = cell.starViewContainer.subviews[wholeStar] as! UIImageView;
imageView.image = image;
}
return cell;
}
The class has a topics property which gets set at:
override func viewDidLoad() {
self.navigationItem.title = "SUPERTRAINER".localized;
self.tableView.backgroundColor = UIColor.init(rgba: hexColors.gray.rawValue);
if(Utility.isConnectedToNetwork()) {
self.getSuperTrainerData(student: self.student!);
} else {
self.topics = Topic.getTopics(student: self.student!, context: NetworkService.sharedInstance.coreDataHandler.context!);
Utility.showNoConnectionAlertView();
}
self.tableView.tableFooterView = UIView(frame: CGRect.zero);
}
// MARK: Custom Methods
func getSuperTrainerData(student: Student) {
_ = SwiftSpinner.show("FETCHING_DATA".localized);
Utility.backgroundThread( background: { () -> Void in
self.networkService.getSuperTrainerData(student: student) { (complet, returnDics, errorMessage) -> Void in
if(complet) {
self.removeSuperTrainerData();
self.createSuperTrainerData(dics: returnDics!);
} else {
Utility.showAlertView(title: "LOGIN_FAILED_TITLE".localized, message: errorMessage);
}
}
}) { () -> Void in
self.networkService.coreDataHandler.saveContext();
self.topics = Topic.getTopics(student: self.student!, context:self.networkService.coreDataHandler.context!)!;
SwiftSpinner.hide();
self.tableView.reloadData();
}
}
Anyone have any ideas? :) Im too new at swift to figure out all of this quickly right now, so any help is super appreciated!
I found out eventually that it was actually a threading(concurrence) problem.
Basically, in the getSuperTrainerData function, a bunch of objects were supposed to be created. Given that they returned nill all the time, the ViewController would of course refuse to create the rows.. however, entering the same view twice would have given the app time to store and cache the objects returned from a network call.
I make a call to Utility.backgroundThread which is just a wrapper for dispatchQueue. This means that networkService was placed inside a background thread. But inside networkService there is a call to urlSession. Call to servers create their own background threads, so even though I called my server call throuhg a background thread, it created its own background thread and never returned to the main call.
The solution that I used was to just make a server call, and place the background-object-creation call in the completion handler like so:
self.networkService.getSuperTrainerData(student: student) { (complet, returnDics, errorMessage) -> Void in
if(complet) {
DispatchQueue.global(qos: DispatchQoS.userInitiated.qosClass ).async {
self.removeSuperTrainerData();
self.createSuperTrainerData(dics: returnDics!);
DispatchQueue.main.async( execute: {
print("main queue without completion")
self.networkService.coreDataHandler.saveContext();
self.topics = Topic.getTopics(student: self.student!, context:self.networkService.coreDataHandler.context!)!;
SwiftSpinner.hide();
self.tableView.reloadData();
})
}
} else {
Utility.showAlertView(title: "LOGIN_FAILED_TITLE".localized, message: errorMessage);
}
}
Hope this helps someone :)
Here's what I'm trying to do:
Provide a tableView that allows the user to select a formula name (an enum) that will be set as their default
In this view I'll place a checkmark next to the formula that is the current default
Their chosen default formula is what will determine which formula is used to calculate a 1 rep maximum amount (the theoretical amount the user could lift based on actual lifts of lesser weight)
After calculating the value, the user will be able to save a record of the lifts they did, the formula used to calculate the maximum, and the calculated maximum weight.
That record will be saved in Core Data with one of the entities being Lift which will simply be the formula name
I've created a class that I want to handle the work of providing the current default formula to what ever part of the app needs it as well as set the default when a new one is selected.
Here is my enum of the formula names:
enum CalculationFormula: Int {
case Epley
case Brzychi
case Baechle
case Lander
case Lombardi
case MayhewEtAl
case OConnerEtAl
}
and with help from the SO community, I created this class to handle the management of userDefaults:
class UserDefaultsManager: NSUserDefaults {
let formulas = [CalculationFormula.Baechle, CalculationFormula.Brzychi, CalculationFormula.Epley, CalculationFormula.Lander, CalculationFormula.Lombardi, CalculationFormula.MayhewEtAl, CalculationFormula.OConnerEtAl]
let formulaNameDictionary: [CalculationFormula : String] =
[.Epley : "Epley", .Brzychi: "Brzychi", .Baechle: "Baechle", .Lander: "Lander", .Lombardi: "Lombardi", .MayhewEtAl: "Mayhew Et.Al.", .OConnerEtAl: "O'Conner Et.Al"]
let userDefaults = NSUserDefaults.standardUserDefaults()
func getPreferredFormula() -> CalculationFormula? {
guard NSUserDefaults.standardUserDefaults().dictionaryRepresentation().keys.contains("selectedFormula") else {
print("No value found")
return nil
}
guard let preferredFormula = CalculationFormula(rawValue: NSUserDefaults.standardUserDefaults().integerForKey("selectedFormula")) else {
print("Wrong value found")
return nil
}
return preferredFormula
}
func setPreferredFormula(formula: CalculationFormula) {
NSUserDefaults.standardUserDefaults().setInteger(formula.rawValue, forKey: "selectedFormula")
}
You can see I have an array of the enums in the order I want them displayed in the tableView and a dictionary of the enums so I can get each enum's string representation to display in each cell of the tableView. Here's how I populate the cell text label which works:
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("formulasCell", forIndexPath: indexPath)
let currentFormula = formulas[indexPath.row].formulaName
cell.textLabel?.text = currentFormula
return cell
}
and here's where I'm setting the checkmark anytime a cell in the tableView is selected
func refresh() {
let preferredFormula = defaults.getPreferredFormula()
for index in 0 ... formulas.count {
let indexPath = NSIndexPath(forItem: index, inSection: 0)
if let cell = tableView.cellForRowAtIndexPath(indexPath) {
cell.accessoryType = preferredFormula == index ? .Checkmark : .None
}
}
}
As I mentioned at the beginning, I need to do many different things with this enum but to keep this question focused on one thing, I'll stay with this checkmark example which now doesn't work after creating my UserDefaultsManager class
The problem is obvious - preferredFormula is now an enum and I can't compare that to the index value which is an Int - but the solution is not. I could get the raw value of the enum but the rawValues aren't guaranteed to be in alignment with the cell indexPaths. Some ideas I've had are:
I could probably change the order of the enum cases so their raw values match the order I've put them in my formulas array, but that seems silly and unreliable
I could use the array index values but that seems equally silly and unreliable
If I just use the array, I don't have the string representations of the cases to display in the cells
It seems that using the array and dictionary together is a viable but the best I could come up with is maybe creating another dictionary that maps the enums to Ints but that would have the same issues I just listed.
Any guidance someone could provide would be greatly appreciated.
You seem to have made things a little more complicated than they need to be.
Firstly, you can use a String raw value for your enum and avoid the associated dictionary:
enum CalculationFormula: String {
case Epley = "Epley"
case Brzychi = "Brzychi"
case Baechle = "Baechle"
case Lander = "Lander"
case Lombardi = "Lombardi"
case MayhewEtAl = "Mayhew Et.Al."
case OConnerEtAl = "O'Conner Et.Al"
}
Second, Your UserDefaultsManager doesn't need to subclass NSUserDefaults, it is simply some utility functions. Also, you are doing a lot of checking in getPreferredFormula that you don't need to. I would suggest re-writing that class to use a computed property like so:
class UserDefaultsManager {
static let sharedInstance = UserDefaultsManager()
private var defaultsFormula: CalculationFormula?
var preferredFormula: CalculationFormula? {
get {
if self.defaultsFormula == nil {
let defaults = NSUserDefaults.standardUserDefaults()
if let defaultValue = defaults.objectForKey("selectedFormula") as? String {
self.defaultsFormula = CalculationFormula(rawValue: defaultValue)
}
}
return self.defaultsFormula
}
set {
self.defaultsFormula = newValue
let defaults = NSUserDefaults.standardUserDefaults()
if (self.defaultsFormula == nil) {
defaults.removeObjectForKey("selectedFormula")
} else {
defaults.setObject(self.defaultsFormula!.rawValue, forKey: "selectedFormula")
}
}
}
}
I have made the class a singleton; although this may have an impact on testability it simplifies issues that could arise if the default is changed in multiple places.
The appropriate place to set/clear the check mark is in cellForRowAtIndexPath so that cell reuse is handled appropriately. This code assumes that formulas is an array of CalculationFormula:
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("formulasCell", forIndexPath: indexPath)
let currentFormula = formulas[indexPath.row]
cell.textLabel?.text = currentFormula.rawValue
let preferredFormula = UserDefaultsManager.sharedInstance.preferredFormula
cell.accessoryType = currentForumula == preferredFormula ? .Checkmark : .None
return cell
}