GCD Cancel and restart queue when button pressed again - ios

Simply, I have a button that adds to a queue, or I believe that's how it works - I've only just started to use GCD. If I press the button twice in my app, it runs these two processes in parallel and the progress bar races back and forth between them, I'd like to stop all the other instances of the queue and execute a new one if that makes sense, I've not got to grips with the lingo...
#IBAction func makePi(_ sender: UIButton) {
piProgress.progress = 0.0
let queue1 = DispatchQueue(label: "com.appcoda.queue1", qos: DispatchQoS.userInitiated)
queue1.async {
let currentAmount = Int(self.randNum.value)
var gcdCounter = 0
for n in 1...currentAmount {
if self.gcd(self.random(max: Int(self.randRange.value)), self.random(max: Int(self.randRange.value))) == 1{
gcdCounter += 1
}
if n % 1000 == 0 {
DispatchQueue.main.async {
self.piProgress.setProgress(Float(Double(n)/Double(currentAmount)), animated: true)
}
} else if n == currentAmount {
DispatchQueue.main.async {
self.piProgress.setProgress(1.0, animated: true)
}
}
}
let piFinal = sqrt(Double((6/Double((Double(gcdCounter)/Double(currentAmount))))))
let roundedAccuracy: Double = ((piFinal-Double.pi)/Double.pi*100).roundTo(places: 6)
DispatchQueue.main.async {
self.piLabel.text = String(piFinal)
self.piAccuracy.text = "Accuracy: \(roundedAccuracy)%"
}
}
}
I just need all the other queue1.async ones to stop...
P.s. If I've done anything obviously wrong, please help ^^ Thanks.

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!

GCD: Making Sequential Download Calls; ONE BY ONE

SCENARIO
The app downloads user subscriptions one by one. This call will be made in multiple places (in completion block after another network call and from a button press from a UIAlertController). The logic is to download all the subscriptions and once one subscription download is down it goes to the next until all have been downloaded and then our SVProgressHUD dismisses. The code works great when we build and run from Xcode. But when we build the IPA and send to our customer, this logic creates some sort of a stall, and the SVProgressHUD alert keeps spinning:
This is a big problem because our application is focused around downloading content from subscriptions.
Why is this logic stalling after I Archive and build an IPA from it, but not when I build and run from Xcode?
Code Below:
// Making the call
DispatchQueue.main.async {
DGWebService().syncUserSubscribedContent {
DispatchQueue.main.async {
self.finishLogin()
}
}
}
// Sequentially going through each subscription and downloading them
func syncUserSubscribedContent(completion: #escaping Constants.WebService.ContentCompletion) {
let subscriptions = MPTUser.sharedUser.getSubscriptionsForDownload()
DispatchQueue.global().async {
if subscriptions.count > 0 {
var index:Int = 0
var t = subscriptions.count
var downloading: Bool = false
while t != 0 {
if downloading == false {
downloading = true
if index < 0 {
index = 0
}
if index > subscriptions.count - 1 {
index = subscriptions.count - 1
}
if index <= subscriptions.count {
let subscription = subscriptions[index]
if subscription.didDownloadContent == false {
if let subscriptionID = subscription.subscriptionID {
DispatchQueue.main.async {
SVProgressHUD.show(withStatus: "Downloading Documents\nfor\n\(subscription.functionalGroupName!)\n\(index+1) of \(subscriptions.count)")
}
self.getUserSubscribedContent(subscriptionID: subscriptionID, completion: { (success) in
subscription.didDownloadContent = true
index += 1
t -= 1
downloading = false
})
}
else {
index += 1
t -= 1
downloading = false
}
}
}
else {
index += 1
t -= 1
downloading = false
}
}
}
}
completion()
}
}
self.getUserSubscribedContent is a function that downloads the content and sends a completion back in the block.
If someone could help me out here it would be much appreciated.
You can try using a DispatchGroup. Here's a rough (and untested) example:
DispatchQueue.global().async {
let subscriptions = MPTUser.sharedUser.getSubscriptionsForDownload()
let group = DispatchGroup()
var completed = 0
let completion: (Bool) -> Void = {
if $0 {
completed += 1
}
group.leave()
DispatchQueue.main.async {
SVProgressHUD.show(withStatus: "Downloading Documents\nfor\n\(subscription.functionalGroupName!)\n\(completed) of \(subscriptions.count)")
}
}
for subscription in subscriptions {
self.getUserSubscribedContent(subscriptionID: subscription.subscriptionID, completion: completion)
group.enter()
}
// However long you want to wait (in seconds) before timing out
_ = group.wait(timeout: .now() + 30)
}

For loop in swift seems to randomly choose increment

I have a for loop that executes animations and then removes them on completion, and I am trying to call another method upon completion. Here is my code:
func animateMatch(completion: () -> ()) {
var movingShape = level.shapeAtColumn(pointsContainingShapeArray[0].Column, row: pointsContainingShapeArray[0].Row)
let destinationShape = level.shapeAtColumn(pointsContainingShapeArray[0].Column, row: pointsContainingShapeArray[0].Row)
for everyShape in 1..<pointsContainingShapeArray.count {
movingShape = level.shapeAtColumn(pointsContainingShapeArray[everyShape].Column, row: pointsContainingShapeArray[everyShape].Row)
let Duration: NSTimeInterval = 1
let moveShapes = SKAction.moveTo((destinationShape?.sprite!.position)!,duration: Duration)
moveShapes.timingMode = .EaseOut
movingShape?.sprite?.runAction(moveShapes, completion: {
movingShape?.sprite?.removeFromParent()
print("Removed shape \(everyShape)")
if everyShape == pointsContainingShapeArray.count {
completion()
}
})
}
}
Basically the idea is that every shape in the array after the first position moves to the position of the first one and then removes it from the scene. This works fine, but my completion was getting called at random times every time. So finally I added the print statement in there. Here was the output:
Removed shape 3
Removed shape 1
Removed shape 4
Removed shape 2
At this point I got so frustrated I decided it was time for stack overflow. I can't possibly figure out what's going on here. Thanks for any help!
Here is the code that calls this method:
func handleSwipe(move: Move) {
view.userInteractionEnabled = false
level.performMove(move)
scene.animateMove(move) {
self.view.userInteractionEnabled = true
if hasMatch {
self.view.userInteractionEnabled = false
self.scene.animateMatch() {
self.scene.addNewSpritesForShapes(pointsContainingShapeArray)
hasMatch = false
self.view!.userInteractionEnabled = true
}
}
}
}
The issue isn't the for loop, it's the asynchronous nature of the run action.
When you call moveShapes action, it runs asynchronously before calling the completion back on the original thread. This can happen in any order. You can see this yourself by calling your print synchronously:
for everyShape in 1..<pointsContainingShapeArray.count {
print("Synchronous loop: \(everyShape)")
}
I think you'd be better off using a dispatch_group_t for your final completion:
func animateMatch(completion: () -> ()) {
var movingShape = level.shapeAtColumn(pointsContainingShapeArray[0].Column, row: pointsContainingShapeArray[0].Row)
let destinationShape = level.shapeAtColumn(pointsContainingShapeArray[0].Column, row: pointsContainingShapeArray[0].Row)
// HERE
let group = dispatch_group_create()
dispatch_group_notify(group, dispatch_get_main_queue(), completion)
//
for everyShape in 1..<pointsContainingShapeArray.count {
// HERE
dispatch_group_enter(group)
//
movingShape = level.shapeAtColumn(pointsContainingShapeArray[everyShape].Column, row: pointsContainingShapeArray[everyShape].Row)
let Duration: NSTimeInterval = 1
let moveShapes = SKAction.moveTo((destinationShape?.sprite!.position)!,duration: Duration)
moveShapes.timingMode = .EaseOut
movingShape?.sprite?.runAction(moveShapes, completion: {
movingShape?.sprite?.removeFromParent()
print("Removed shape \(everyShape)")
// HERE
dispatch_group_leave(group)
//
})
}
}

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