DispatchGroup notify() doesn't get called - ios

I have to wait for the completion until the user touches the Try Again button to call the same function again. All works fine the first time but when I press it again, the notfy() method doesn't get called. Here is the playground class to simulate that scenario:
import Foundation
class TokenManager{
private var alertAlreadyShown = false
static let shared = TokenManager()
var alreadyEnterGroup = false
let handlerTryAgainGroup = DispatchGroup()
func showRefreshAlert(onTryAgainPressed : #escaping()->Void){
//PREVENTS TO ENTER ON GROUP MUTIPLE TIMES (ASYNC CALLS)
if(!alreadyEnterGroup){
alreadyEnterGroup = true
self.handlerTryAgainGroup.enter()
}
//PREVENTS TO SHOW ALERT MUTIPLE TIMES (ASYNC CALLS)
if(!alertAlreadyShown){
alertAlreadyShown = true
//NOTIFY THE COMPLETION THAT USER TOUCH TRY AGAIN BUTTON
handlerTryAgainGroup.notify(queue: DispatchQueue.main) {
onTryAgainPressed()
}
}else{
//THIS IS JUST A TEST TO SIMULATE THE USER TAPS MUTIPLE TIMES ON BUTTON
TokenManager.shared.tryAgainPressed()
}
}
func tryAgainPressed() {
alreadyEnterGroup = false
handlerTryAgainGroup.leave()
}
}
func showAlert(){
TokenManager.shared.showRefreshAlert {
//CALLS THE SAME FUNCTION IF THE USER TAPS TRY AGAIN
showAlert()
}
}
//SHOW THE ALERT AND PRESS TRY AGAIN FOR THE FIRST TIME
showAlert()
TokenManager.shared.tryAgainPressed()
What is wrong at this case?

Related

Cancel a timer / DispatchQueue.main.asyncAfter in SwiftUI Tap gesture

I am trying to cancel a delayed execution of a function running on the main queue, in a tap gesture, I found a way to create a cancellable DispatchWorkItem, but the issue I have is that it's getting created every time while tapping, and then when I cancel the execution, I actually cancel the new delayed execution and not the first one.
Here is a simpler example with a Timer instead of a DispatchQueue.main.asyncAfter:
.onTapGesture {
isDeleting.toggle()
let timer = Timer.scheduledTimer(withTimeInterval: 2.0, repeats: false) { timer in
completeTask()
}
if !isDeleting {
timer.invalidate()
}
}
completeTask:
private func completeTask() {
tasksViewModel.deleteTask(task: task) // task is declared above this func at the top level of the struct and so is tasksViewModel, etc.
guard let userID = userViewModel.id?.uuidString else { return }
Task {
//do some async stuff
}
}
As you can see if I click it once the timer fires, but if I click it again, another timer fires and straight away invalidates, but the first timer is still running.
So I have to find a way to create only one instance of that timer.
I tried putting it in the top level of the struct and not inside the var body but the issue now is that I can't use completeTask() because it uses variables that are declared at the same scope.
Also, can't use a lazy initialization because it is an immutable struct.
My goal is to eventually let the user cancel a timed task and reactivate it at will on tapping a button/view. Also, the timed task should use variables that are declared at the top level of the struct.
First of all you need to create a strong reference of timer on local context like so:
var timer: Timer?
and then, set the timer value on onTapGesture closure:
.onTapGesture {
isDeleting.toggle()
self.timer = Timer.scheduledTimer(withTimeInterval: 2.0, repeats: false) { timer in
completeTask()
}
if !isDeleting {
timer.invalidate()
}
}
and after that you can invalidate this Timer whenever you need by accessing the local variable timer like this:
func doSomething() {
timer?.invalidate()
}
that is my solution mb can help you
var timer: Timer?
private func produceWorkItem(withDelay: Double = 3) {
scrollItem?.cancel()
timer?.invalidate()
scrollItem = DispatchWorkItem.init { [weak self] in
self?.timer = Timer.scheduledTimer(withTimeInterval: withDelay, repeats: false) { [weak self] _ in
self?.goToNextPage(animated: true, completion: { [weak self] _ in self?.produceWorkItem() })
guard let currentVC = self?.viewControllers?.first,
let index = self?.pages.firstIndex(of: currentVC) else {
return
}
self?.pageControl.currentPage = index
}
}
scrollItem?.perform()
}
for stop use scrollItem?.cancel()
for start call func

Why is UIAccessibility.post(notification: .announcement, argument: "arg") not announced in voice over?

When using Voice Over in iOS, calling UIAccessibility.post(notification:argument:) to announce a field error doesn't actually announce the error.
I have a submit button and, when focusing the button, voice over reads the button title as you would expect. When pressing the button, voice over reads the title again. When the submit button is pressed, I am doing some validation and, when there is a field error, I am trying to announce it by calling:
if UIAccessibility.isVoiceOverRunning {
UIAccessibility.post(notification: .announcement, argument: "my field error")
}
Interestingly enough, if I stop on a breakpoint in the debugger the announcement happens. When I don't stop on a breakpoint, the announcement doesn't happen.
The notification is posting on the main thread and, if is like NotificationCenter.default, I assume that it is handled on the same thread it was posted on. I have tried to dispatch the call to the main queue, even though it is already on the main thread, and that doesn't seem to work either.
The only thing that I can think is that the notification is posted and observed before voice over is finished reading the submit button title and the announcement notification won't interrupt the current voice over.
I would really appreciate any help on this.
This is an admittedly hacky solution, but I was able to prevent the system announcement from pre-empting my own by dispatching to the main thread with a slight delay:
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
UIAccessibility.post(notification: .announcement, argument: "<Your text>")
}
Another work around is to use .screenChanged instead and pass the error label, as:
UIAccessibility.post(notification: .screenChanged, argument: errorLabel)
Your problem may happen because the system needs to take over during the field error appears and, in this case, any customed VoiceOver notification is cancelled.🤯
I wrote an answer about problems with queueing multiple VoiceOver notifications that may help you to understand your current situation.🤓
Your notification works with a breakpoint because you're delaying it and the system works during this time : there's no overlap between your notification and the system work.
A simple solution may be to implement a short delay before sending your notification but the delay depends on the speech rate that's why this is only a temporary workaround. 🙄
Your retry mechanism is smart and could be improved inside a loop of few retries in case of many system takeovers. 👍
I was facing the same issue, so I picked up #brandenesmith idea of the notification queue and wrote a little helper class.
class AccessibilityAnnouncementQueue {
static let shard = AccessibilityAnnouncementQueue()
private var queue: [String] = []
private init() {
NotificationCenter.default.addObserver(self,
selector: #selector(announcementFinished(_:)),
name: UIAccessibility.announcementDidFinishNotification,
object: nil)
}
func post(announcement: String) {
guard UIAccessibility.isVoiceOverRunning else { return }
queue.append(announcement)
postNotification(announcement)
}
private func postNotification(_ message: String) {
let attrMessage: NSAttributedString = NSAttributedString(string: message, attributes: [.accessibilitySpeechQueueAnnouncement: true])
UIAccessibility.post(notification: .announcement, argument: attrMessage)
}
#objc private func announcementFinished(_ sender: Notification) {
guard
let userInfo = sender.userInfo,
let firstQueueItem = queue.first,
let announcement = userInfo[UIAccessibility.announcementStringValueUserInfoKey] as? String,
let success = userInfo[UIAccessibility.announcementWasSuccessfulUserInfoKey] as? Bool,
firstQueueItem == announcement
else { return }
if success {
queue.removeFirst()
} else {
postNotification(firstQueueItem)
}
}
}
I am able to get this to work using a retry mechanism where I register as an observer of the UIAccessibility.announcementDidFinishNotification and then pull the announcement and success status out of the userInfo dictionary.
If the success status is false and the announcement is the same as the one I just sent, I post the notification again. This happens on repeat until the announcement was successful.
There are obviously multiple problems with this approach including having to de-register, what happens if another object manages to post the same announcement (this shouldn't ever happen in practice but in theory it could), having to keep track of the last announcement sent, etc.
The code would look like:
private var _errors: [String] = []
private var _lastAnnouncement: String = ""
init() {
NotificationCenter.default.addObserver(
self,
selector: #selector(announcementFinished(_:)),
name: UIAccessibility.announcementDidFinishNotification,
object: nil
)
}
func showErrors() {
if !_errors.isEmpty {
view.errorLabel.text = _errors.first!
view.errorLabel.isHidden = false
if UIAccessibility.isVoiceOverRunning {
_lastAnnouncement = _errors.first!
UIAccessibility.post(notification: .announcement, argument: _errors.first!)
}
} else {
view.errorLabel.text = ""
view.errorLabel.isHidden = true
}
}
#objc func announcementFinished(_ sender: Notification) {
guard let announcement = sender.userInfo![UIAccessibility.announcementStringValueUserInfoKey] as? String else { return }
guard let success = sender.userInfo![UIAccessibility.announcementWasSuccessfulUserInfoKey] as? Bool else { return }
if !success && announcement == _lastAnnouncement {
_lastAnnouncement = _errors.first!
UIAccessibility.post(notification: .announcement, argument: _errors.first!)
}
}
The problem is that this retry mechanism will always be used because the first call to UIAccessibility.post(notification: .announcement, argument: _errors.first!) always (unless I am stopped on a breakpoint). I still don't know why the first post always fails.
If somebody uses RxSwift, probably following solution will be more suitable:
extension UIAccessibility {
static func announce(_ message: String) -> Completable {
guard !message.isEmpty else { return .empty() }
return Completable.create { subscriber in
let postAnnouncement = {
DispatchQueue.main.async {
UIAccessibility.post(notification: .announcement, argument: message)
}
}
postAnnouncement()
let observable = NotificationCenter.default.rx.notification(UIAccessibility.announcementDidFinishNotification)
return observable.subscribe(onNext: { notification in
guard let userInfo = notification.userInfo,
let announcement = userInfo[UIAccessibility.announcementStringValueUserInfoKey] as? String,
announcement == message,
let success = userInfo[UIAccessibility.announcementWasSuccessfulUserInfoKey] as? Bool else { return }
success ? subscriber(.completed) : postAnnouncement()
})
}
}
}

Alamofire service calls multiple times

Alamofire.request("https://example.com/writecomment.php", method: .post, parameters: parameters).validate().responseJSON { response in
switch response.result {
case .success:
if let json = response.result.value {
var success = 0
if let dictJSON = json as? [String: AnyObject] {
if let successInteger = dictJSON["success"] as? Int {
success = successInteger
if success == 1
{
//succes
}
}
}
}
case .failure(_):
return
}
}
This is connected to an UIButton action
It SOMETIMES triggers service calls multiple times when user tap button.
How to prevent multiple calls?
Example::
var isAPICalling = false
#IBAction func btnTapped(_ sender: UIButton) {
if isAPICalling == false
{
isAPICalling = true
self.APICalling() // Your API calling method
}
}
In APICalling() method set isAPICalling = false when you get response.
OTHER OPTION :
Use ActivityIndicator when you request that time start indicator and after response stop indicator. So you cannot able to tap on button.
You can do one thing without using flag, if you have button outlet then just make button state selected when button click action is performed.
then call API and once you get the response make button state normal. so it will call only once when user click on button.
#IBAction func btnApiAction(_ sender: Any) {
if btnAPI.isSelected == false {
btnAPI.isSelected = true
//Call API here
}else {
//Do nothing here
}
}
if API Call is get failed or if you are getting any error then also change button state to normal so user can click again on button.
Disable UIButton when you call API. And enable back when api call finished.
#IBAction func didTapButton(_ sender: UIButton) {
sender.isEnabled = false
Alamofire.request(/* Params */) { response in
sender.isEnabled = true
// parse response - response.result
}
}
I prefer to add ActivityIndicatorView to the button view, and remove when finished for better UX.

How to stop a Function from Running in Swift 3.0

I have a function that starts playing an animation that is running asynchronously (in the background). This animation is called indefinitely using a completion handler (see below). Is there a way to close this function upon pressing another button?
Here is my code:
func showAnimation() {
UIView.animate(withDuration: 1, animations: {
animate1(imageView: self.Anime, images: self.animation1)
}, completion: { (true) in
DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) {
self.showAnimation() // replay first showAnimation
}
})
}
Then upon pressing another button we closeout the above function
showAnimation().stop();
Thanks
You can add a property to the class to act as a flag indicating whether the animation should be run or not.
var runAnimation = true
func showAnimation() {
if !runAnimation { return }
UIView.animate(withDuration: 1, animations: {
animate1(imageView: self.Anime, images: self.animation1)
}, completion: { (true) in
DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) {
if runAnimation {
self.showAnimation() // replay first showAnimation
}
}
})
}
Then in the button handler to stop the animation you simply do:
runAnimation = false
Note that this does not stop the currently running 1 second animation. This just prevent any more animations.
There are a lot of ways to do this. The simplest is to have a Boolean property (which you should make properly atomic) that you check in your asyncAfter block, and don't just don't call showAnimation() again if it's true.
Another thing you can do, and what I like to do for more complex tasks, is to use OperationQueue instead of DispatchQueue. This allows you to cancel operations, either individually or all at once, or even suspend the whole queue (obviously don't suspend the main queue or call removeAllOperations() on it, though, since there may be other operations in there unrelated to your code).
You can provide a variable outside of your function, then observe its value and handle your task. I can give you a solution:
class SomeClass {
private var shouldStopMyFunction: Bool = false // keep this private
public var deadline: TimeInterval = 0
func stopMyFunction() {
shouldStopMyFunction = true
}
func myFunction(completionHanlder: #escaping (String)->()) {
// -------
var isTaskRunning = true
func checkStop() {
DispatchQueue.global(qos: .background).async {
if self.shouldStopMyFunction, isTaskRunning {
isTaskRunning = false
completionHanlder("myFunction is forced to stop! 😌")
} else {
//print("Checking...")
checkStop()
}
}
}
checkStop()
// --------
// Start your task from here
DispatchQueue.global().async { // an async task for an example.
DispatchQueue.global().asyncAfter(deadline: .now() + self.deadline, execute: {
guard isTaskRunning else { return }
isTaskRunning = false
completionHanlder("My job takes \(self.deadline) seconds to finish")
})
}
}
}
And implement:
let anObject = SomeClass()
anObject.deadline = 5.0 // seconds
anObject.myFunction { result in
print(result)
}
let waitingTimeInterval = 3.0 // 6.0 // see `anObject.deadline`
DispatchQueue.main.asyncAfter(deadline: .now() + waitingTimeInterval) {
anObject.stopMyFunction()
}
Result with waitingTimeInterval = 3.0: myFunction is forced to stop! 😌
Result with waitingTimeInterval = 6.0: My job takes 5.0 seconds to finish

While Loop and Touch Events Swift iOS

I'm trying to create an iOS triggering app that sends a signal if triggered when the app is armed. I've generated the signal to be sent when triggered and a button that changes the app from armed to unarmed.
When I touch the button making the app armed, it then goes into a while loop that runs listening to see if the app has been triggered and sends the signal if triggered. However by doing this I am unable to touch the button that now says to unarm the app. This is what I have so far:
var trigger = AVAudioPlayer()
func playMySound(){
let triggerSoundURL = NSBundle.mainBundle().URLForResource("trigger", withExtension: "wav")!
trigger = try! AVAudioPlayer(contentsOfURL: triggerSoundURL)
trigger.prepareToPlay()
trigger.play()
}
var armed = 0
#IBAction func arm() {
if(armed==0){
armed=1
armButton.setTitle("Armed", forState: .Normal)
triggering();
}
else{
armed=0
armButton.setTitle("Arm", forState: .Normal)
}
}
func triggering() -> Void {
while(armed==1){
playMySound()
arm()
}
}
You are blocking main thread, thats why you can`t touch the button.
Try to change your "triggering" function to something like this:
func triggering() -> Void {
dipsatch_async(dispatch_get_global_queue(QOS_CLASS_BACKGROUND, 0)){
while(armed==1){
playMySound()
arm()
}
}
}

Resources