viewWillAppear delay in update table from webservices - ios

Is viewWillAppear the best place in the lifecycle to import my data from a webservice? This relates to a small exchange rate app.
In a tableview from viewwillappear, we go to http://api.fixer.io to update an array called rates, and all of the returned data in a class RatesData. If the Internet connection fails we either use the data we already have, or look to a file on the phone file system.
The time it takes to import the data means that I run cellForRowAt indexPath before my data array is populated; meaning that the data appears after a perceptible delay (I've default cells to load) before being updated with exchange rates.
I will implement coredata next as a better solution but the first time the app runs we would still get this undesired effect.
override func viewWillAppear(_ animated: Bool) {
searchForRates()
importCountriessync()
}
private func searchForRates(){
Request.fetchRates(withurl: APIConstants.eurURL) {[weak self] (newData:RatesData, error:Error?)->Void in
DispatchQueue.main.async {
//update table on the main queue
//returns array of rates
guard (error == nil) else {
print ("did not recieve data - getting from file if not already existing")
if ( self?.rates == nil)
{
self?.searchForFileRates()
}
return
}
self?.rates = newData.rates
let newData = RatesData(base: newData.base, date: Date(), rates: newData.rates)
self?.ratesFullData = newData
self?.tableView.reloadData()
}
}
}
func searchForFileRates(){
print ("file rates")
Request.fetchRates(withfile: "latest.json") { [weak self] (newData: RatesData)->Void in
DispatchQueue.main.async {
//update table on the main queue
//returns array of rates
self?.rates = newData.rates
let newData = RatesData(base: newData.base, date: Date(), rates: newData.rates)
self?.ratesFullData = newData
self?.tableView.reloadData()
}
}
}

Yes viewWillAppear is fine as long as the fetch is asynchronous.
Just remember it will be fired every time the view appears. Example when this view controller is hidden by another modal view controller and the modal view controller is dismissed, viewWillAppear will be called. If you want it to be called only once you could invoke it in viewDidLoad
Summary
viewWillAppear - Invoked every time view appears
viewDidLoad - Invoked once when the view first loads
Choose what meets your needs.

Related

App "freezes" when opening VC and getting data from Firebase

I have run into a problem behaving like this. When opening a new VC which is getting a few pictures from Firebase Storage (in this case 9 pictures, each picture being a few KB) and getting a small number of documents from Firestore, the app sort of freezes for about 2-4 seconds before opening and showing the view. I have a tab bar controller and I'm unable to tap on any other tab bar element until the view has shown up.
I have all my Firebase references and calling the function inside viewWillAppear I have also tried putting everything inside ViewDidLoad and viewDidAppear, but I'm experiencing the same freeze.
Is there a solution for this, am I doing something wrong or do I just have to live with this?
I have the latest Firebase version, I'm using swift 4 and have a 1000mb internet connection.
var db: Firestore!
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(true)
tableView.delegate = self
tableView.dataSource = self
db = Firestore.firestore()
getTasks()
tableView.reloadData()
}
func getTasks() {
db.collection("School").document(school).collection("Projects").document((project?.projectId)!).collection("Tasks").whereField("Completed", isEqualTo: false).addSnapshotListener { (snapshot, error) in
if let error = error {
print(error)
} else {
self.tasks = snapshot!.documents.compactMap({TaskModel(dictonary: $0.data())})
self.tableView.reloadData()
}
}
}
It seems like you are fetching data from main thread(which blocks the UI). use GCD for fetching data async and then reload your table view.
DispatchQueue.global(qos: .background).async {
self.getTasks()
DispatchQueue.main.async {
self.tableView.reloadData()
}
}
See your code fetches all the images before presenting that view that's why it freezes. If you have used firebase you must understand that it downloads all the subnodes present in your main node.
Solution:- One way to solve the freezing thing is to call the 'getTasks()' function in ' viewdidAppear() ' function. So you would be able to access your view while it would simultaneously loads the data. Also You can use activity indicator to indicate that the data is downloading.

Update tableView row from AppDelegate Swift 4

[![enter image description here][1]][1]
Hello. I have a tableview like in the picture above and I'm receiving some silent push notifications. Depending on them I need to reload a specific cell from the tableView. Since I'm getting the notification in the AppDelegate and there at the moment I'm reloading the whole tableView...but personally I don't find this the best solution since I only need to update a specific row.
Any hints please how can I update just a specific cell from appDelegate?
if userInfo["notification_type"] as? String == "update_conversation" {
if let rootVC = (self.window?.rootViewController as? UINavigationController)?.visibleViewController {
if rootVC is VoiceViewController {
let chatRoom = rootVC as! VoiceViewController
chatRoom.getConversations()
// the get Conversations method makes a call to api to get some data then I reload the whole tableView
}
}
func getConversations() {
let reachabilityManager = NetworkReachabilityManager()
if (reachabilityManager?.isReachable)! {
ServerConnection.getAllConversation { (data) in
if let _ = data{
self.conversations = data
self.onlineRecent = self.conversations
GlobalMainQueue.async {
self.mainTableView.reloadData()
}
}
}
}
}
This is my getConversation method which is used in VoiceViewController to populate my tableview
Have the app delegate broadcast an app-specific notification center notification (on the main thread). Have the view controller that contains your table view listen for that notification and update the cell in question as needed. That way you don't contaminate your app delegate. The app delegate should only deal with system level app stuff, not business logic.
You could get your row’s cell using self.mainTableView.cellForRow(at:IndexPath(…), and update it directly.
Or, I’ve found you save a load of time and your view controllers end up more reliable using ALTableViewHelper [commercial - available on Framework Central here]. It’s free to try.
The helper does the most of the work - you describe how the data connects to the UITableView. I’ve put together an example (on GitHub here), which I hope is something like what you’re trying to do.
import ALTableViewHelper
class VoiceViewController {
// #objc for the helper to see the var’s, and dynamic so it sees any changes to them
#obj dynamic var conversations: Any?
#obj dynamic var onlineRequest: Any?
func viewDidLoad() {
super.viewDidLoad()
tableView.setHelperString(“””
section
headertext "Conversation Status"
body
Conversation
$.viewWithTag(1).text <~ conversations[conversations.count-1]["title"]
$.viewWithTag(2).text <~ "At \\(dateFormat.stringFromDate(conversations[conversations.count-1]["update"]))"
UpdateButton
$.viewWithTag(1).isAnimating <~ FakeConversationGetter.singleton.busy
“””, context:self)
}
func getConversations() {
let reachabilityManager = NetworkReachabilityManager()
if (reachabilityManager?.isReachable)! {
ServerConnection.getAllConversation { (data) in
if let _ = data {
// change the data on the main thread as this causes the UI changes
GlobalMainQueue.async {
self.conversations = data
self.onlineRequest = self.conversations
}
}
}
}
}

Swift iOS -How To Reload TableView Outside Of Firebase Observer .childAdded to Filter Out Duplicate Values?

I have a tabBar controller with 2 tabs: tabA which contains ClassA and tabB which contains ClassB. I send data to Firebase Database in tabA/ClassA and I observe the Database in tabB/ClassB where I retrieve it and add it to a tableView. Inside the tableView's cell I show the number of sneakers that are currently inside the database.
I know the difference between .observeSingleEvent( .value) vs .observe( .childAdded). I need live updates because while the data is getting sent in tabA, if I switch to tabB, I want to to see the new data get added to the tableView once tabA/ClassA is finished.
In ClassB I have my observer in viewWillAppear. I put it inside a pullDataFromFirebase() function and every time the view appears the function runs. I also have Notification observer that listens for the data to be sent in tabA/ClassA so that it will update the tableView. The notification event runs pullDataFromFirebase() again
In ClassA, inside the callback of the call to Firebase I have the Notification post to run the pullDataFromFirebase() function in ClassB.
The issue I'm running into is if I'm in tabB while the new data is updating, when it completes, the cell that displays the data has a count and the count is thrown off. I debugged it and the the sneakerModels array that holds the data is sometimes duplicating and triplicating the newly added data.
For example if I am in Class B and there are 2 pairs of sneakers in the database, the pullDataFromFirebase() func will run, and the tableView cell will show "You have 2 pairs of sneakers"
What was happening was if I switched to tabA/ClassA, then added 1 pair of sneakers, while it's updating I switched to tabB/ClassB, the cell would still say "You have 2 pairs of sneakers" but then once it updated the cell would say "You have 5 pairs of sneakers" and 5 cells would appear? If I switched tabs and came back it would correctly show "You have 3 pairs of sneakers" and the correct amount of cells.
That's where the Notification came in. Once I added that if I went through the same process and started with 2 sneakers the cell would say "You have 2 pairs of sneakers", I go to tabA, add another pair, switch back to tabB and still see "You have 2 pairs of sneakers". Once the data was sent the cell would briefly show "You have 5 pairs of sneakers" and show 5 cells, then it would correctly update to "You have 3 pairs of sneakers" and the correct amount of cells (I didn't have to switch tabs).
The Notification seemed to work but there was that brief incorrect moment.
I did some research and the most I could find were some posts that said I need to use a semaphore but apparently from several ppl who left comments below they said semaphores aren't meant to be used asynchronously. I had to update my question to exclude the semaphore reference.
Right now I'm running tableView.reloadData() in the completion handler of pullDataFromFirebase().
How do I reload the tableView outside of the observer once it's finished to prevent the duplicate values?
Model:
class SneakerModel{
var sneakerName:String?
}
tabB/ClassB:
ClassB: UIViewController, UITableViewDataSource, UITableViewDelegate{
var sneakerModels[SneakerModel]
override func viewDidLoad() {
super.viewDidLoad()
NotificationCenter.default.addObserver(self, selector: #selector(pullDataFromFirebase), name: NSNotification.Name(rawValue: "pullFirebaseData"), object: nil)
}
override func viewWillAppear(_ animated: Bool){
super.viewWillAppear(animated)
pullDataFromFirebase()
}
func pullDataFromFirebase(){
sneakerRef?.observe( .childAdded, with: {
(snapshot) in
if let dict = snapshot.value as? [String:Any]{
let sneakerName = dict["sneakerName"] as? String
let sneakerModel = SneakerModel()
sneakerModel.sneakerName = sneakerName
self.sneakerModels.append(sneakerModel)
//firebase runs on main queue
self.tableView.reloadData()
}
})
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return sneakerModels.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "SneakerCell", for: indexPath) as! SneakerCell
let name = sneakerModels[indePath.row]
//I do something else with the sneakerName and how pairs of each I have
cell.sneakerCount = "You have \(sneakerModels.count) pairs of sneakers"
return cell
}
}
}
tabA/ClassA:
ClassA : UIViewController{
#IBAction fileprivate func postTapped(_ sender: UIButton) {
dict = [String:Any]()
dict.updateValue("Adidas", forKey: "sneakerName")
sneakerRef.?.updateChildValues(dict, withCompletionBlock: {
(error, ref) in
//1. show alert everything was successful
//2. post notification to ClassB to update tableView
NotificationCenter.default.post(name: Notification.Name(rawValue: "pullFirebaseData"), object: nil)
}
}
}
In other parts of my app I use a filterDuplicates method that I added as an extension to an Array to filter out duplicate elements. I got it from filter array duplicates:
extension Array {
func filterDuplicates(_ includeElement: #escaping (_ lhs:Element, _ rhs:Element) -> Bool) -> [Element]{
var results = [Element]()
forEach { (element) in
let existingElements = results.filter {
return includeElement(element, $0)
}
if existingElements.count == 0 {
results.append(element)
}
}
return results
}
}
I couldn't find anything particular on SO to my situation so I used the filterDuplicates method which was very convenient.
In my original code I have a date property that I should've added to the question. Any way I'm adding it here and that date property is what I need to use inside the filterDuplicates method to solve my problem:
Model:
class SneakerModel{
var sneakerName:String?
var dateInSecs: NSNumber?
}
Inside tabA/ClassA there is no need to use the Notification inside the Firebase callback however add the dateInSecs to the dict.
tabA/ClassA:
ClassA : UIViewController{
#IBAction fileprivate func postTapped(_ sender: UIButton) {
//you must add this or whichever date formatter your using
let dateInSecs:NSNumber? = Date().timeIntervalSince1970 as NSNumber?
dict = [String:Any]()
dict.updateValue("Adidas", forKey: "sneakerName")
dict.updateValue(dateInSecs!, forKey: "dateInSecs")//you must add this
sneakerRef.?.updateChildValues(dict, withCompletionBlock: {
(error, ref) in
// 1. show alert everything was successful
// 2. no need to use the Notification so I removed it
}
}
}
And in tabB/ClassB inside the completion handler of the Firebase observer in the pullDataFromFirebase() function I used the filterDuplicates method to filter out the duplicate elements that were showing up.
tabB/ClassB:
func pullDataFromFirebase(){
sneakerRef?.observe( .childAdded, with: {
(snapshot) in
if let dict = snapshot.value as? [String:Any]{
let sneakerName = dict["sneakerName"] as? String
let sneakerModel = SneakerModel()
sneakerModel.sneakerName = sneakerName
self.sneakerModels.append(sneakerModel)
// use the filterDuplicates method here
self.sneakerModels = self.sneakerModels.filterDuplicates{$0.dateInSecs == $1.dateInSecs}
self.tableView.reloadData()
}
})
}
Basically the filterDuplicates method loops through the sneakerModels array comparing each element to the dateInSecs and when it finds them it excludes the copies. I then reinitialize the sneakerModels with the results and everything is well.
Also take note that there isn't any need for the Notification observer inside ClassB's viewDidLoad so I removed it.

Displaying download progress in reusable cells

I am trying to display download progress in my collectionview cells. Im currently using the parse progressblock which has an instance of the cell and updates the progress bar.
}, progressBlock: { (percent) in
self.mainQueue.addOperation {
// set the downloadProgess var to update from cellForItemAt
// downloadProgress = (Float(percent) / Float(100))
if let downloadingCell = self.collectionView.cellForItem(at: self.indexPath) as? InnerCollectionCell {
downloadingCell.progressBar.isHidden = false
downloadingCell.contentView.bringSubview(toFront: downloadingCell.progressBar)
downloadingCell.progressBar.setProgress(Float(percent) / Float(100), animated: true)
downloadingCell.setNeedsDisplay()
downloadingCell.setNeedsLayout()
downloadingCell.isUserInteractionEnabled = false
downloadingCell.spinner.isHidden = true
}
}
})
So this works fine, the problem i now have is if i leave this view controller then come back to see how the downloads are going the instance of the cell has been reused and none of the desired UI elements are visible but the progress is still ticking away in the background.
The only place i can think to re-display the UI elements is in cellForItemAt. The problem then is that the progress doesn't update, it just shows the value at the time the cell was reloaded.
How can i go about reusing the instance of the cell that the progress block is using or cleanly displaying ui elements that continue to update?
Presuming that you're dismissing the old view controller with the collection view and presenting a new one, there are two problems here:
You're then trying to update cells in the collection view in the previous view controller; and
You're keeping a strong reference to the old view controller that was dismissed.
If this is the case, the goal is to decouple the progress updates from any particular view controller, collection view, or cell. You also probably want to decouple the item/row number, too, in case you insert/remove any cells at any time. The best way to handle this is notifications:
Define a few constants used when defining the notifications:
private let notificationName = Notification.Name(rawValue: "com.domain.app.downloadProgress")
private let notificationIdentifierKey = "com.domain.app.download.identifier"
private let notificationPercentKey = "com.domain.app.download.percent"
Have your progressBlock post a notification rather than trying to update the UI directly:
let percent: Float = ...
let userInfo: [AnyHashable: Any] = [
notificationIdentifierKey: identifier,
notificationPercentKey: percent
]
NotificationCenter.default.post(name: notificationName, object: nil, userInfo: userInfo)
Please note that there are no reference to self here, which keeps the progress block from hanging on to your view controller.
Define some function that you can use to identify which IndexPath corresponds to the identifier for your download. In my simple example, I'm just going to have an array of download identifiers and use that:
var downloadIdentifiers = [String]()
private func indexPath(for identifier: String) -> IndexPath? {
if let item = downloadIdentifiers.index(of: identifier) {
return IndexPath(item: item, section: 0)
} else {
return nil
}
}
You'd probably have a download identifier as a property of some Download model object, and use that instead, but hopefully it illustrates the idea: Just have some way to identify the appropriate IndexPath for a given download. (By the way, this decoupling the IndexPath from what it was when you first created the download is important, in case you insert/remove any items from your collection view at any point.)
Now, you may ask what should you use for the identifier. You might use the URL's absoluteString. You might use some other unique identifier. But I'd discourage you from relying solely on item/row numbers, because those can change (maybe not now, but perhaps later as you make the app more sophisticated, you might be inserting removing items).
Have your collection view's view controller add itself as an observer of this notification, updating the appropriate progress view:
private var observer: NSObjectProtocol!
override func viewDidLoad() {
super.viewDidLoad()
observer = NotificationCenter.default.addObserver(forName: notificationName, object: nil, queue: .main) { [weak self] notification in
if let identifier = notification.userInfo?[notificationIdentifierKey] as? String,
let percent = notification.userInfo?[notificationPercentKey] as? Float,
let indexPath = self?.indexPath(for: identifier),
let cell = self?.collectionView?.cellForItem(at: indexPath) as? InnerCollectionCell {
cell.progressView.setProgress(percent, animated: true)
}
}
...
}
deinit {
NotificationCenter.default.removeObserver(observer)
}
Please note the [weak self] capture list, to make sure the notification observer doesn't cause a strong reference cycle with the view controller.

Table Refresh doubles number of Array items

I have static data (3 values) coming from CloudKit, and the problem is when I refresh the UITableView, I get 6 values instead of 3 values.
I'm not sure why it doesn't refresh and throw out old data from the Array, but instead it keeps the old data and adds the same data to it Array.
Initial UITableView set up:
func getData() {
cloud.getCloudKit { (game: [Game]) in
var teamColorArray = [String]()
for item in game {
let itemColor = item.teamColor
teamColorArray.append(itemColor)
print("TCA in: \(teamColorArray)")
}
self.teamColorArray = teamColorArray
self.tableView.reloadData()
}
}
Prints: ["CC0033", "FDB927", "E3263A"]
Refresh data when UIRefreshControl pulled:
#IBAction func refreshData(_ sender: Any) {
self.teamColorArray.removeAll()
getData()
self.refreshControl?.endRefreshing()
}
Prints: ["CC0033", "FDB927", "E3263A", "CC0033", "FDB927", "E3263A"]
I think I have it narrowed down to somehow game in the function getData() is incremented to a count of 6 items. I'm not sure why it wouldn't always stay at 3 items if it were pulling all new data from CloudKit, but maybe I'm not understanding that calling a completion handler doubles the values and maybe I need to removeAll inside of that? I'm just really not sure
Does anyone have anything they see I'm doing wrong, or anything they'd do to fix my code?
Thanks!
Might have to do with your async call to cloudkit. I'm not too familiar with refresh control but here is a way to maybe solve your problem and also make your code a little cleaner.
func getData(_ completion: () -> ()) {
teamColorArray.removeAll()
cloud.getCloudKit { [weak self] (game: [Game]) in
guard let unwrappedSelf = self else { return }
var updatedColorArray = [String]()
game.forEach { updatedColorArray.append($0.teamColor) }
unwrappedSelf.teamColorArray = updatedColorArray
completion()
}
}
now when you call getData it would look like this
getData {
DispatchQueue.main.async { [weak self] in
self?.tableView.reloadData()
self?.refreshControl?.endRefreshing()
}
}
you add weak self to remove the possibility of retain cycles
make sure your updating UI from the main thread
Call reloadData and endRefreshing when you know the array has been set properly
I put teamColorArray.removeAll() inside the getData function since it seems like you will need to call it everytime and it saves you from having to add it before the function everytime.

Resources