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

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.

Related

Method tableView.reloadData() not working

I'm new to develop an iOS app.my story is I have the completion for handle new data from API that notify by socket IO, every time socket on success I want to reload tableview.and the socket work fine. The problem is When I First Login my app for the first time the method tableview.reload not work but when I rerun my project everything is fine.so I want to ask why tableview does not reload for the first time right here?.
Thank you in advance!!
Note1: for tableview.reloadData() not working I mean it doesn't call calls cellForRowAtIndexPath I've already printed and set a breakpoint on the method cellForRowAtIndexPath
Note2: tableview render tableViewCell once and I try to call tableview.reloadData() to render cell again to update UI but it doesn't work.
This is my code
override func viewDidLoad() {
super.viewDidLoad()
....................
ChatSocketManagerOnline.shared.lastMessageOn { (data) in
self.data = data
self.tableView.reloadData()
}
try
override func viewDidLoad() {
super.viewDidLoad()
....................
ChatSocketManagerOnline.shared.lastMessageOn { [weak self] (data) in
DispatchQueue.main.async {
self?.data = data
self?.tableView.reloadData()
}
}
Because you put ChatSocketManagerOnline.shared.lastMessageOn in viewDidLoad() so your Table View will be reloaded data once when you run app.
Please read document https://developer.apple.com/documentation/uikit/uiviewcontroller/1621495-viewdidload
You should use a NotificationCenter to detect when socket success then reload tableview.

viewWillAppear delay in update table from webservices

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.

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.

Downloading Initial App Data

When a user opens the app, it needs to download information stored on a MySQL database, save it to Core Data, then display it in a table view controller. The download and saving of the data works, but not presenting it to the user. The data doesn't present itself the first time the view controller is displayed; it presents itself only after switching to a different view and returning. I have tried putting the code which loads the data in the viewWillAppear function (where I think it belongs) and the viewDidLoad function -- both with the same, previously described outcome. Can someone help spot what I may be doing wrong? Maybe I have the statements executing in the wrong order?
Also, another weird thing I see is when I run it in the debugger or with breakpoints (aka when I give the app more time to load), it works fine. It's only on a normal run when I have these problems.
View Did Load
override func viewDidLoad() {
/*This is where I would put the code to load the data. See the first
part of viewWillAppear too see what the code is.*/
super.viewDidLoad()
NSNotificationCenter.defaultCenter().addObserver(self, selector:"viewWillAppear:", name:
UIApplicationWillEnterForegroundNotification, object: nil)
}
View Will Appear
override func viewWillAppear(animated: Bool) {
if(!defaults.boolForKey("objectivesDownloaded")) {
downloadObjectives()
defaults.setBool(true, forKey: "objectivesDownloaded")
}
defaults.synchronize()
fetchObjectives()
super.viewWillAppear(animated)
}
Fetch Objectives
func fetchObjectives() {
let fetchRequest = NSFetchRequest(entityName: "Objective")
let sortDescriptor = NSSortDescriptor(key: "logIndex", ascending: true)
fetchRequest.sortDescriptors = [sortDescriptor]
var error: NSError?
let fetchedResults = managedContext.executeFetchRequest(fetchRequest, error: &error) as [NSManagedObject]?
if var results = fetchedResults {
objectives = results
} else {
println("Could not fetch \(error), \(error?.userInfo)")
}
tableView.reloadData()
}
you probably aren't telling your table to reload data after you fetch your data. (it would initially load with no data when your view loads)
i.e. in obj-c
[yourTable reloadData]
swift
yourTable.reloadData()
The user has to login before seeing the main view, so I added the download code in the login script after it finds the login successful, but before the view controller is presented. The data now displays on start.
if(loginSucceeded) {
self.downloadObjectives()
self.defaults.setBool(true, forKey: "objectivesDownloaded")
let tabBarController = self.storyboard!.instantiateViewControllerWithIdentifier("tabBarController") as UIViewController
dispatch_async(dispatch_get_main_queue(), {() -> Void in
self.presentViewController(tabBarController, animated: true, completion: nil)
})
}

(Xcode 6 beta / Swift) performSegueWithIdentifier has delay before segue

I'm just learning Ios programming for the first time, with Swift and Xcode 6 beta.
I am making a simple test app that should call an API, and then segue programmatically to a different view to present the information that was retrieved.
The problem is the segue. In my delegate method didReceiveAPIResults, after everything has been successfully retrieved, I have:
println("--> Perform segue")
performSegueWithIdentifier("segueWhenApiDidFinish", sender: nil)
When the app runs, the console outputs --> Perform segue, but then there is about a 5-10 second delay before the app actually segues to the next view. During this time all the UI components are frozen.
I'm a little stuck trying to figure out why the segue doesn't happen immediately, or how to debug this!
Heres The Full View controller:
import UIKit
class ViewController: UIViewController, APIControllerProtocol {
#lazy var api: APIController = APIController(delegate: self)
override func viewDidLoad() {
super.viewDidLoad()
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
}
func didReceiveAPIResults(results: NSDictionary) {
println(results)
println("--> Perform segue")
performSegueWithIdentifier("segueWhenApiDidFinish", sender: nil)
}
#IBAction func getData(sender : AnyObject){
println("--> Get Data from API")
api.getInfoFromAPI()
}
}
And my API controller:
import UIKit
import Foundation
protocol APIControllerProtocol {
func didReceiveAPIResults(results: NSDictionary)
}
class APIController: NSObject {
var delegate: APIControllerProtocol?
init(delegate: APIControllerProtocol?) {
self.delegate = delegate
}
func getInfoFromAPI(){
let session = NSURLSession.sharedSession()
let url = NSURL(string: "https://itunes.apple.com/search?term=Bob+Dylan&media=music&entity=album")
let task = session.dataTaskWithURL(url, completionHandler: {data, response, error -> Void in
if(error) {
println("There was a web request error.")
return
}
var err: NSError?
var jsonResult = NSJSONSerialization.JSONObjectWithData(data, options: NSJSONReadingOptions. MutableContainers, error: &err) as NSDictionary
if(err?) {
println("There was a JSON error.")
return
}
self.delegate?.didReceiveAPIResults(jsonResult)
})
task.resume()
}
}
UPDATE: Got this working based on Ethan's answer. Below is the exact code that ended up getting the desired behavior. I needed assign that to self to have access to self inside the dispatch_async block.
let that = self
if(NSThread.isMainThread()){
self.delegate?.didReceiveAPIResults(jsonResult)
}else
{
dispatch_async(dispatch_get_main_queue()) {
println(that)
that.delegate?.didReceiveAPIResults(jsonResult)
}
}
Interestingly, this code does not work if I remove the println(that) line! (The build fails with could not find member 'didReceiveAPIResults'). This is very curious, if anyone could comment on this...
I believe you are not on the main thread when calling
self.delegate?.didReceiveAPIResults(jsonResult)
If you ever are curious whether you are on the main thread or not, as an exercise, you can do NSThread.isMainThread() returns a bool.
Anyway, if it turns out that you are not on the main thread, you must be! Why? Because background threads are not prioritized and will wait a very long time before you see results, unlike the mainthread, which is high priority for the system. Here is what to do... in getInfoFromAPI replace
self.delegate?.didReceiveAPIResults(jsonResult)
with
dispatch_sync(dispatch_get_main_queue())
{
self.delegate?.didReceiveAPIResults(jsonResult)
}
Here you are using GCD to get the main queue and perform the UI update within the block on the main thread.
But be wear, for if you are already on the main thread, calling dispatch_sync(dispatch_get_main_queue()) will wait FOREVER (aka, freezing your app)... so be aware of that.
I have a delay problem with segue from a UITableView. I have checked and I appear to be on the main thread. I checked "NSThread.isMainThread()" during prepareForSegue. It always returns true.
I found a solution on Apple Developer forums! https://forums.developer.apple.com/thread/5861
This person says it is a bug in iOS 8.
I followed their suggestion to add a line of code to didSelectRowAtIndexPath...... Despatch_async.....
It worked for me, hopefully you too.

Resources