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.
Related
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.
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.
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.
I'm building an app which it communicates with socket periodically.
As long as the socket is open, then data will be transmitting from time to time.
However, my textFields(representing the data) do not update itself unless another view is introduces.
From the image above, initially my app will scans for the QR code as authentication method. Assuming authentication succeeded and the First view is loaded(the view after navigation controller).
The problem is it took quite some time to get the data.
Code that describe how I maneuver the connection.
override func viewDidLoad() {
super.viewDidLoad()
print("First view loaded")
//add observer
NSNotificationCenter.defaultCenter().addObserver(self, selector: "reachabilityStatusChanged", name: "ReachStatusChanged", object: nil)
reachabilityStatusChanged()
}
func reachabilityStatusChanged(){
switch reachabilityStatus{
case NOACCESS:
print("No access")
dispatch_async(dispatch_get_main_queue()){
print("No-access")
self.displayAlertMessage("No internet access, please try again later")
}
default:
dispatch_async(dispatch_get_main_queue()){
self.api.reportStatus()
self.api.importData()
socket.connect()
//data will be sent to text field at this point
}
}
}
override func viewDidAppear(animated: Bool) {
super.viewDidAppear(animated)
print("First view appeared")
self.circleProgressView.progress = progressSliderValue
dispatch_async(dispatch_get_main_queue()){
self.distanceLabel.text = "\(appUserMileage)"
self.mileageLabel.text = "Target: \(appUserTarget) km"
}
}
From the code above as you can tell as the view is loaded(the view after navigation controller), it checks for network access. If network available then it will communicate with socket but by the time data gets in, the view is already loaded and appeared which means no data will be displayed at that time.
Is there a way to update the views periodically? I've done some research but all of them is about background fetching data which isn't suitable for my case.
I don't know where your appUserMileage and appUserTarget variables live, but how about something like this?
var appUserMileage: Int? {
didSet {
self.distanceLabel?.text = "\(appUserMileage ?? 0)" //? in case model calls this before outlets are loaded
}
}
var appUserTarget: Int? {
didSet {
self.mileageLabel?.text = "Target: \(appUserTarget ?? 0) km"
}
}
I have an application that pulls information from a Parse database, and displays it in a UITableView. I pull the information from parse in the viewWillAppear function, and i display it in the tableView(cellForRowAtIndexPath) function. Sometimes i receive an error because the array that stores the Parse information has a length of 0, and i try to access information at an index outside of the bounds of the array. I believe this is because the cellForRowAtIndexPath is getting called before the viewWillAppear is finished running. Is this possible or is my error definitely coming from somewhere else?
EDIT: The error does not occur every time, and i cannot find a way to reproduce it
override func viewWillAppear(animated: Bool) {
//begin ignoring events until the information is finished being pulled
UIApplication.sharedApplication().beginIgnoringInteractionEvents()
resultsArray.removeAll(keepCapacity: false)
//run query
let query = PFQuery(className: "Answers")
query.findObjectsInBackgroundWithBlock { (objects, error) -> Void in
if let objects = objects {
//append information to the resultsArray
}
}
self.tableView.reloadData()
}
}
//information is now pulled, so allow interaction
UIApplication.sharedApplication().endIgnoringInteractionEvents()
}
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("cell", forIndexPath: indexPath) as! answerCell
// THIS IS WHERE THE ERROR OCCURS
resultsArray[indexPath.row].imageFile.getDataInBackgroundWithBlock { (data, error) -> Void in
//set image within cell
}
return cell
}
I would suggest that you load your data from Parse into a temporary array and then assign this to your property array right before you call reloadData - this will avoid any potential race conditions and remove the need for the removeAll which is potentially a big part of your problem;
override func viewWillAppear(animated: Bool) {
//begin ignoring events until the information is finished being pulled
UIApplication.sharedApplication().beginIgnoringInteractionEvents()
//run query
let query = PFQuery(className: "Answers")
query.findObjectsInBackgroundWithBlock { (objects, error) -> Void in
var localArray=[SomeType]()
if let objects = objects {
//append information to the localArray
}
}
self.resultsArray=localArray
self.tableView.reloadData()
}
}
//information is now pulled, so allow interaction
UIApplication.sharedApplication().endIgnoringInteractionEvents()
}
Looks like in viewWillAppear you have a background block findObjectsInBackgroundWithBlock that has some work to do in a different thread (AKA off the main thread), that means that viewWillAppear will finish while the block will get a callback.
This explains why cellForRowAtIndexPath is being called after viewWillAppear finishes, because of the callback block.
That means that everything is alright and viewWillAppear actually do finish a legit "run".
You can actually put a breaking point inside the callback method (in viewWillAppear) and a breaking point inside cellForRowAtIndexPath and see when the callback happens while cellForRowAtIndexPath is being called.
If you need a different method from Parse perhaps you should look in their documentation.
Actually if your callback not access to self.tableView, everything will go on as you think as usual. You can have a try.
It happened to me when I access to the view on the screen in init method viewDidLoad method called before init ends.
Anyway, you should know that fact. And you access to your tableView in callback (called before viewWillAppear finishing) which needs cellForRowAtIndexPath.