on NSTimer interval, UITableView.visibleCells wrong response - ios

I am using NSTimer with 1.1 seconds interval, the timer fires properly, but (mostly) on scrolling UITableView.visibleCells does not update the response, the response contains invisible cells.
Below is my Swift code
private func onApiResponse() {
_timer = NSTimer.scheduledTimerWithTimeInterval(1.1, target: self, selector: "myTimer", userInfo: nil, repeats: true)
}
above timer after backend api response
func myTimer() {
for cell in tableView.visibleCells {
if let indexPath = tableView.indexPathForCell(cell) {
// do something
}
}
}

here is the fix that worked in my case.
Instead to get visible cells using tableView.visibleCells, I get a list of indexPaths using tableView.indexPathsForVisibleRows with tableView.cellForRowAtIndexPath, below is the sample code:
if let paths = mode == 0 ? tvList.indexPathsForVisibleRows : tvGallery.indexPathsForVisibleRows {
for path in paths {
if let isNotNil = moProducts[path.row].data where moProducts[path.row].bidValid {
if let cell = (mode == 0 ? tvList.cellForRowAtIndexPath(path) : tvGallery.cellForRowAtIndexPath(path)) as? SubClassCell {
// do something here with the cell
}
}
}
}

Related

UITableView scrolling performance problem

I am currently working as a 5 month junior ios developer.
The project I'm working on is an application that shows the prices of 70 cryptocurrencies realtime with websocket connection.
we used websocket connection, UItableview, UITableViewDiffableDataSource, NSDiffableDataSourceSnapshot while developing the application.
But right now there are problems such as slowdown scrolling or not stop scroling and UI locking while scrolling in the tableview because too much data is processed at the same time.
after i check cpu performance with timer profiler I came to the conclusion that updateDataSource and updateUI functions exhausting the main thread.
func updateDataSource(model: [PairModel]) {
var snapshot = DiffableDataSourceSnapshot()
let diff = model.difference(from: snapshot.itemIdentifiers)
let currentIdentifiers = snapshot.itemIdentifiers
guard let newIdentifiers = currentIdentifiers.applying(diff) else {
return
}
snapshot.appendSections([.first])
snapshot.deleteItems(currentIdentifiers)
snapshot.appendItems(newIdentifiers)
dataSource?.apply(snapshot, animatingDifferences: false, completion: nil)
}
func updateUI(data: SocketData) {
guard let newData = data.data else { return }
guard let current = data.data?.price else { return }
guard let closed = data.data?.lastDayClosePrice else { return }
let dailyChange = ((current - closed)/closed)*100
DispatchQueue.main.async { [self] in
if model.filter({ $0.symbol == newData.pairSymbol }).first != nil {
let index = model.enumerated().first(where: { $0.element.symbol == newData.pairSymbol})
guard let location = index?.offset else { return }
model[location].price = current
model[location].dailyPercent = dailyChange
if calculateLastSignalTime(alertDate: model[location].alertDate) > 0 {
//Do Nothing
} else {
model[location].alertDate = ""
model[location].alertType = ""
}
if let text = allSymbolsView.searchTextField.text {
if text != "" {
filteredModel = model.filter({ $0.name.contains(text) || $0.symbol.contains(text) })
updateDataSource(model: filteredModel)
} else {
filteredModel = model
updateDataSource(model: filteredModel)
}
}
}
delegate?.pricesChange(data: self.model)
}
}
Regards.
ALL of your code is running on the main thread. You have to wrap your entire updateUI function inside a DispatchQueue.global(qos:), and then wrap your dataSource.apply(snapshot) line inside a DispatchQueue.main.async. the dataSource.apply(snapshot) line is the only UI work you're doing in all that code you posted.

Swift UIKit label text doesn't update / view doesn't update

I have a problem:
I have a list of items this is controller A, and when I click on any item I go to controller B (item info), I then execute the ledLightingButton_Tapped function by pressing the corresponding button that activates the LED indicator for the animal.
#IBAction func ledLightingButton_Tapped(_ sender: Any) {
if !GlobalData.shared.led_animals.contains(GlobalData.shared.selectedAnimalId) {
GlobalData.shared.led_animals.append(GlobalData.shared.selectedAnimalId)
}
activateLED(at: GlobalData.shared.selectedAnimalId)
}
func activateLED(at animalId: String) {
ServerSocket.shared?.perform(
op: "ActivateLED",
with: [
"light_duration": "180",
"led_color": "White",
"client_data": "",
"led_animals": [animalId]
]
) { err, data in
guard err == nil else { return }
print(data)
let ledStatus = data[0]["led_request_status"].stringValue
self.ledStatusLabel.text = ledStatus
GlobalData.shared.isActiveLED = true
self.startTimer()
}
}
Upon successful activation, the animal number is added to the array, and the startTimer is called which every 10 seconds requests checkLEDStatus for all animals in the array.
func startTimer() {
timer = Timer.scheduledTimer(timeInterval: 10.0, target: self, selector: #selector(updateCowStatus), userInfo: nil, repeats: true)
}
#objc func updateCowStatus() {
self.checkLEDStatus()
}
func checkLEDStatus() {
ServerSocket.shared?.perform(
op: "CheckStatusLED",
with: [
"light_duration": "180",
"led_color": "White",
"client_data": "",
"led_animals": GlobalData.shared.led_animals
]
) { err, data in
guard err == nil else {
GlobalData.shared.isActiveLED = false
self.stopTimer()
return
}
DispatchQueue.global(qos: .background).async {
for i in 0..<data.count {
if GlobalData.shared.selectedAnimalId == data[i]["animal_id"].stringValue {
let ledStatus = data[i]["led_request_status"].stringValue
if ledStatus.contains("Fail") {
guard let index = GlobalData.shared.led_animals.firstIndex(of: GlobalData.shared.selectedAnimalId) else { return }
GlobalData.shared.led_animals.remove(at: index)
}
DispatchQueue.main.async {
self.ledStatusLabel.text = ledStatus
}
}
}
}
}
}
The current status of the animal is displayed on the label. If you go in the controller A and activate the status + get a result from checkedLEDstatus - it is work for one animal - everything works, but if you go to controller B, activate for animal number 1, go out and open animal number 2 - perform activation, return to animal number 1 - then the label is no longer is updated, it does not display the new value, but I check it from debugging and property self.ledStatuslabel.text contains new value but UI didn't update. self.ledStatuslabel.text show old value.
Please help me, thanks!

Swift: How to stop timers started from a loop?

I got some Swift code to print every character of a specific word ("stackoverflow") and also with a specific delay (1.0s).
To understand my thoughts look at the pseudo code:
print("s")
wait(1s)
print("t")
wait(1s)
print("a")
wait(1s)
print("c")
wait(1s)
print("k")
wait(1s)
...
Ok - below you can find my code written in Swift:
var mystring="stackoverflow"
var counter=0.0
for i in mystring {
Timer.scheduledTimer(
timeInterval: Double(counter),
target: self,
selector: #selector(self.myfunc(_:)),
userInfo: String(i),
repeats: false)
counter=counter+1.0
})
}
func myfunc(_ timer: Timer) {
let value: String? = timer.userInfo! as? String
print ("Value: \(value as String?)")
}
But how is it possible to kill all myfunc-calls after the for loop has finished? How to kill the different Timers that I didn't declared with a variable to avoid the override of the last Timer??
Why not one timer and a few lines additional logic. The code prints the first character of the string when the timer fires and then drops the first character until the string is empty. At the end the timer gets invalidated.
let string = "stackoverflow"
var temp = Substring(string)
Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { timer in
print(temp.prefix(1))
temp = temp.dropFirst()
if temp.isEmpty { timer.invalidate() }
}
or as ticker
let string = "stackoverflow"
var counter = 1
Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { timer in
print(string.prefix(counter))
if counter == string.count { timer.invalidate() }
counter += 1
}
Edit : For those who argue that this is not the best answer it's the best for his current programming technique and to suggest a good one here is a solution in edit part for his main issue with no timers at all
Edit://////
var counter = 0.0
for i in mystring {
DispatchQueue.main.asyncAfter(deadline: .now() + counter) {
print("\(String(i))")
}
counter = counter + 1
}
/////////
declare var
var timerarr = [Timer] = []
then add every created timer to the array
for i in mystring {
let t = Timer.scheduledTimer(
timeInterval: Double(counter),
target: self,
selector: #selector(self.myfunc(_:)),
userInfo: String(i),
repeats: false)
counter=counter+1.0
})
timerarr.append(t)
}
and loop to stop
for i in 0..<timerarr.count {
let tim = timerarr[i]
tim.invalidate()
}
timearr = []

The best way of adding countdown label with timer in UICollectionViewCell which works fine even after cell is being reused

I have a list of items which are presenting to the user in UICollectionView. These items have a countdown label to show the remaining time that the item is available.
I used a Timer in UICollectionViewCell to show the remaining time like:
OperationQueue.main.addOperation {
var remaingTimeInterval = self.calculateRemainigTime(remainingTime: remainingTime)
if remaingTimeInterval > 0 {
self.timerLabel.isHidden = false
self.timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true, block: { [weak self] (_) in
let hours = Int(remaingTimeInterval) / 3600
let minutes = Int(remaingTimeInterval) / 60 % 60
let seconds = Int(remaingTimeInterval) % 60
self?.timerLabel?.text = String(format:"%02i:%02i:%02i", hours, minutes, seconds)
remaingTimeInterval -= 1
})
} else {
self.timer?.invalidate()
self.timerLabel.isHidden = true
}
}
and that's how I calculate the remaining time based on the given Date:
//Calculating remaining time based on the item endDate and currentDAte
func calculateRemainigTime(remainingTime: String) -> Int {
let prizeRemainingTime = Helper.stringToDate(remainingTime)
let prizeRemainingTimeInterval = Int(prizeRemainingTime.timeIntervalSince1970)
let currentTimeInterval = Int(Date().timeIntervalSince1970)
return prizeRemainingTimeInterval - currentTimeInterval
}
Everything works fine till the cell is being reused, after that the countdown numbers are not correct anymore.
Is this a correct way to show the countdown in UICollectionViewCell or there is a better solution.
Can anyone help me to find a way through this?
Move the timer logic to the data model.
Instead of target/action use the block-based API of Timer.
In cellForRow pass the timer callback block to the cell.
When the timer fires the code in the callback block can update the UI in the cell.
Coding from accepted answer guideline, hope you like it.
MyViewController
protocol MyViewControllerDelegate : class {
func updateTimer(_ timeString: String)
}
class MyViewController: UIViewController {
weak var timerDetegate: MyViewControllerDelegate?
var timer = Timer()
var time = 0
fun ViewDidLoad() {
timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true, block: { _ in
self.time += 1
self.timerDetegate?.updateTimer("time \(self.time)")
})
}
...
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableViewdequeueReusableCell(withIdentifier: "MeCell")
vc.timerDetegate = cell
return cell
}
...
}
MyTableViewCell
class MyTableViewCell : UITableViewCell, MyViewControllerDelegate {
...
func updateTimer(_ timeString: String) {
lblTimeLeft.text = timeString
}
}
I had implemented this simpler way (without caring about the performance/latency)
in ..cellForItemAt.. method, I call
cell.localEndTime = yourEndTimeInterval
cell.startTimerProgress()
In collection view cell, I added a method which starts the progress:
var localEndTime: TimeInterval = 0 //set value from model
var timer:Timer?
func timerIsRunning(timer: Timer){
let diff = localEndTime - Date().timeIntervalSince1970
if diff > 0 {
self.showProgress()
return
}
self.stopProgress()
}
func showProgress(){
let endDate = Date(timeIntervalSince1970: localEndTime)
let nowDate = Date()
let components = Calendar.current.dateComponents(Set([.day, .hour, .minute, .second]), from: nowDate, to: endDate)
self?.timerLabel?.text = String(format:"%02i:%02i:%02i", components.hour!, components.minute!, components.second!)
}
func stopProgress(){
self?.timerLabel?.text = String(format:"%02i:%02i:%02i", 0, 0, 0)
self.timer?.invalidate()
self.timer = nil
}
func startTimerProgress() {
timer = Timer.scheduledTimer(timeInterval: 1, target: self, selector: #selector(self.timerIsRunning(timer:)), userInfo: nil, repeats: true)
self.timer?.fire()
}

How to access for loop externally and make it stop in swift

I'm using this function to make the text write letter by letter:
extension SKLabelNode {
func setTextWithTypeAnimation(typedText: String, characterInterval: NSTimeInterval = 0.05) {
text = ""
self.fontName = "PressStart2P"
dispatch_async(dispatch_get_global_queue(QOS_CLASS_USER_INTERACTIVE, 0)) {
for character in typedText.characters {
dispatch_async(dispatch_get_main_queue()) {
self.text = self.text! + String(character)
}
NSThread.sleepForTimeInterval(characterInterval)
}
}
}
And, if the user clicks the screen, I want to make the for loop stop and show the complete text instantly.
I would do something like this:
var ignoreSleeper = false
#IBAction func pressButton(sender: UIButton) {
ignoreSleeper = true
}
extension SKLabelNode {
func setTextWithTypeAnimation(typedText: String, characterInterval: NSTimeInterval = 0.05) {
text = ""
self.fontName = "PressStart2P"
dispatch_async(dispatch_get_global_queue(QOS_CLASS_USER_INTERACTIVE, 0)) {
for character in typedText.characters {
dispatch_async(dispatch_get_main_queue()) {
self.text = self.text! + String(character)
}
if(!ignoreSleeper){
NSThread.sleepForTimeInterval(characterInterval)
}
}
}
}
Edit: like #Breek already mentioned
I'd suggest to implement a tiny NSTimer with a counter for the number of chars to display. Start the Timer with a repeat count of typedText.characters.count and the desired delay and you're good to go (with one thread). Increment the number of chars counter on each timer loop. You can stop this timer at any time with a button press by calling invalidate on the timer.
Example
var timer: NSTimer?
var numberOfCharsToPrint = 1
let text = "Hello, this is a test."
func updateLabel() {
if numberOfCharsToPrint == text.characters.count {
welcomeLabel.text = text
timer?.invalidate()
}
let index = text.startIndex.advancedBy(numberOfCharsToPrint)
welcomeLabel.text = text.substringToIndex(index)
numberOfCharsToPrint++;
}
Then initialize your timer whenever you want the animation to start.
timer = NSTimer.scheduledTimerWithTimeInterval(0.25, target: self, selector: "updateLabel", userInfo: nil, repeats: true)
You can invalidate/stop the timer at any given time with timer?.invalidate().

Resources