How to limit a process between LongPressGesture-began and LongPressGesture-ended - swift

The requirement is simple: I want to start an activity (e.g. drawing circles on the screen) when the user touches the screen and holds down the finger. This activity shall stop immediately when the finger is lifted again. So I used the UILongPressGestureRecognizer. This produces a .began and .ended event. But the problem is that when an activity is launched based on the .began event, the .ended event will not happen as long as the activity is ongoing. My intention for the .ended event is that it shall stop the launched activity. In this way the activity execution would be limited to the time between finger touch down and lift.
I enhanced my sample code to even 2 independent UILongPressGestureRecognizers which can work simultaneously using the UIGestureRecognizerDelegate construct. The simultaneous function of the two recognizers works. However, when one of them launches the activity also the second recognizer does not receive events until the activity has ended.
How can this trivial request be realized?
Here is my sample code. The activity is simplified to 10 print statements. 1 per second. When the touch finger is lifted after 2 seconds, the loop will continue for the remaining 8 seconds.
import UIKit
class ViewController: UIViewController {
var fingerDown = false
#IBOutlet var MyLongPress1: UILongPressGestureRecognizer!
#IBOutlet var MyLongPress2: UILongPressGestureRecognizer!
override func viewDidLoad() {
super.viewDidLoad()
}
#IBAction func handleLongPress1(_ gesture: UILongPressGestureRecognizer) {
if (gesture as AnyObject).state == .began {
print("longPress1 began")
fingerDown = true
myActivity()
} else if (gesture as AnyObject).state == .ended {
print("longPress1 ended")
fingerDown = false
}
}
#IBAction func handleLongPress2(_ gesture: UILongPressGestureRecognizer) {
if (gesture as AnyObject).state == .began {
print("longPress2 began")
fingerDown = true
} else if (gesture as AnyObject).state == .ended {
print("longPress2 ended")
fingerDown = false
}
}
func myActivity() {
var count = 0
while fingerDown && count < 10 {
sleep(1)
count += 1
print("in myActivity \(count)")
}
}
}
extension ViewController: UIGestureRecognizerDelegate {
func gestureRecognizer(
_ gestureRecognizer: UIGestureRecognizer,
shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer
) -> Bool {
return true
}
}
Any good advice would be appreciated!

You are executing synchronous code in your function.
Instead, what you should do is create an asynchronous, cancellable operation, that starts on .began event, and cancel it on .ended event. Code for this would be too complicated to write and put here in an answer. You can instead refer a concurrently / asynchronous code tutorial like this one to properly implement what you are trying to do.
A much simpler, but dirtier solution that will still work is to simply call your function asynchronously, so it doesn't block the main thread:
DispatchQueue.global().async {
self.myActivity()
}
Having an async call like this in your code is ok, but there are other problems with the code which make it not very clean.
e.g. your code depends on an additional fingerDown state instead of actual state of gesture; use of sleep etc..

Related

Key down event handler running multiple times in Swift

I am trying to do something when a given key is pressed in a macOS App. First, I ran into a problem where the keyDown event was detected multiple times on each press, therefore executing the handler multiple times. As per a suggestion, I added code to check whether the event is a repeat and it seemed to work at the time. However, this solution seems to work only some of the time, other times the event is getting detected multiple times. Also, I can't seem find a pattern in situations when it works and when it doesn't.What might be the problem and how could I fix it.
Code:
override func viewDidLoad() {
super.viewDidLoad()
NSEvent.addLocalMonitorForEvents(matching: .keyDown, handler: checkKeyDown(event:))
}
func checkKeyDown(event: NSEvent) -> NSEvent{
if event.isARepeat == false{
if event.keyCode == 36{
print("Hello World!")
}
}
return event
}
Removing the event monitor when the window closes seems to have fixed this issue.
var numKeyDown : Any?
override func viewDidLoad() {
super.viewDidLoad()
numKeyDown = NSEvent.addLocalMonitorForEvents(matching: .keyDown, handler: checkKeyDown(event:))
}
override func viewWillDisappear(){
if let numMonitor = self.numKeyDown {
NSEvent.removeMonitor(numMonitor)
}
}
func checkKeyDown(event: NSEvent) -> NSEvent{
if event.isARepeat == false{
if event.keyCode == 36{
print("Hello World!")
}
}
return event
}

Async data loading swift

I got a function such as scrollViewDidScroll that can trigger many times. And I need to call function loadMoreDataFromRemoteServerIfNeed only single time. How could I do this more elegantly without using any "flag" variables. Maybe I should use DispathGroup|DispatchWorkItem?
func scrollViewDidScroll(_ scrollView: UIScrollView) {
let yOffset = scrollView.contentOffset.y
if yOffset > offset {
loadMoreDataFromRemoteServerIfNeed()
}
}
func loadMoreDataFromRemoteServerIfNeed() {
DispatchQueue.global(qos: .background).async {
sleep(2)
DispatchQueue.main.async {
// <Insert New Data>
}
}
}
The thing that you are trying to describe — "Do this, but only if you are not told to do it again any time in the next 2 seconds" — has a name. It's called debouncing. This is a well-solved problem in iOS programming, so now that you know its name, you can do a search and find some of the solutions.
While I'm here telling you about this, here's a solution you might not know about. Debouncing is now built in to iOS functionality! Starting in iOS 13, it's part of the Combine framework. I'm now using Combine all over the place: instead of notifications, instead of GCD, instead of Timer objects, etc. It's great!
Here's a Combine-based solution to this type of problem. Instead of a scroll view, suppose we have a button hooked up to an action handler, and we don't want the action handler to do its task unless 2 seconds has elapsed since the last time the user tapped the button:
var pipeline : AnyCancellable?
let pipelineStart = PassthroughSubject<Void,Never>()
#IBAction func doButton(_ sender: Any) {
if self.pipeline == nil {
self.pipeline = pipelineStart
.debounce(for: .seconds(2), scheduler: DispatchQueue.main)
.sink { [weak self] _ in self?.doSomething() }
}
self.pipelineStart.send()
}
func doSomething() {
print("I did it!")
}
I'm sure you can readily see how to adapt that to your own use case:
var pipeline : AnyCancellable?
let pipelineStart = PassthroughSubject<Void,Never>()
func scrollViewDidScroll(_ scrollView: UIScrollView) {
let yOffset = scrollView.contentOffset.y
if yOffset > offset {
if self.pipeline == nil {
self.pipeline = pipelineStart
.debounce(for: .seconds(2), scheduler: DispatchQueue.main)
.sink { [weak self] _ in self?.loadMoreDataFromRemoteServerIfNeed()
}
self.pipelineStart.send()
}
}
func loadMoreDataFromRemoteServerIfNeed() {
// <Insert New Data>
}
You can create a flag from DispatchWorkItem to observe loading state e.g.:
var item: DispatchWorkItem?
func loadMoreDataFromRemoteServerIfNeed() {
assert(Thread.isMainThread)
guard item == nil else { return }
item = DispatchWorkItem {
print("loading items")
Thread.sleep(forTimeInterval: 2)
DispatchQueue.main.async {
item = nil
print("insert items")
}
}
DispatchQueue.global().async(execute: item!)
}
NOTE: to synchronise item var you must change its value on the same thread for instance the main thread.
Yes, you could use DispatchWorkItem, keep a reference to the old one, and cancel prior one if necessary. If you were going to do that, I might consider Operation, too, as that handles cancelation even more gracefully and has other advantages.
But that having been said, given that the work that you are dispatching is immediately sleeping for two seconds, this might suggest a completely different pattern, namely a Timer. You can schedule your timer, invalidating previously scheduled timers, if any:
weak var timer: Timer?
func loadMoreDataFromRemoteServerIfNeed() {
// cancel old timer if any
timer?.invalidate()
// schedule what you want to do in 2 seconds
timer = Timer.scheduledTimer(withTimeInterval: 2, repeats: false) { _ in
// <Insert New Data>
}
}
FWIW, if you ever find yourself sleeping, you should general consider either timers or asyncAfter. This avoids tying up the global queue’s worker thread. Sleeping is an inefficient pattern.
In this case, keeping a weak reference to the prior timer (if any) is probably the best pattern.

How can I make fix this quite complex bug in my app that uses AVSpeechSynthesizer to play a playlist?

(If anyone can think of a better title, please edit!)
I noticed that usually, when I call AVSpeechSynthesizer.stopSpeaking(at: .immediate), the didCancel delegate method gets called. However, if I stop the synthesiser at the last syllable, it will instead call didFinish.
Steps to reproduce:
Create an app with a single button. Hook up the button's IBAction to this method:
let synthesiser = AVSpeechSynthesizer()
#IBAction func click() {
if synthesiser.isSpeaking {
print("Manually stopping")
synthesiser.stopSpeaking(at: .immediate)
} else {
let utterance = AVSpeechUtterance(string: "One Two Three Four Internationalization")
utterance.rate = AVSpeechUtteranceMinimumSpeechRate
utterance.voice = AVSpeechSynthesisVoice(language: "en-US")
synthesiser.delegate = self
synthesiser.speak(utterance)
}
}
Implement these two delegate methods:
func speechSynthesizer(_ synthesizer: AVSpeechSynthesizer, didFinish utterance: AVSpeechUtterance) {
print("Stopped!")
}
func speechSynthesizer(_ synthesizer: AVSpeechSynthesizer, didCancel utterance: AVSpeechUtterance) {
print("Cancelled!")
}
Press the button to start synthesising, press again to stop. If you stop at the middle, it would print:
Manually stopping
Cancelled!
which is expected. If you let it speak the whole line, eventually it will print:
Stopped!
which is also expected. However, if you stop it at the last syllable, then it prints:
Manually stopping
Stopped!
I expected the second line to be "Cancelled!", just like stopping in the middle.
Why this is important to me?
In my app, I have a list of utterances (i.e. a playlist) that the user can play through. The user can press buttons to go to the previous or the next utterance. If an utterance finished playing, then the next utterance is automatically played.
// these are properties of the VC
var playlist: Playlist!
var currentIndex = 0 {
didSet {
updateTextView()
}
}
var currentUtterance: Utterance {
return playlist.items[currentIndex]
}
var isPlaying = false
// plays the previous utterance in the playlist
#objc func previousPressed() {
guard currentIndex > 0 else { return }
previous()
}
// plays the next utterance in the playlist
#objc func nextPressed() {
guard currentIndex < playlist.items.count - 1 else { return }
next()
}
func next() {
currentIndex += 1
playCurrentUtterance()
}
func previous() {
currentIndex -= 1
playCurrentUtterance()
}
func playCurrentUtterance() {
speechSynthesiser.stopSpeaking(at: .immediate)
speechSynthesiser.speak(currentUtterance.avUtterance)
isPlaying = true
}
func speechSynthesizer(_ synthesizer: AVSpeechSynthesizer, didFinish utterance: AVSpeechUtterance) {
if currentIndex >= playlist.items.count - 1 { // this checks whether this is the last utterance
isPlaying = false
} else {
// when one utterance finishes, play the next one
next()
}
}
The bug occurs when the user goes to the next or previous utterance when the synthesiser is speaking the last syllable. Let's say the user goes to the next utterance. speechSynthesiser.stopSpeaking(at: .immediate) causes didFinish to be called, so next is actually called twice (once by nextPressed and once by didFinish)! As a result, currentIndex is incremented twice! playCurrentUtterance is also called twice which means speechSynthesiser.speak is called twice. However, the synthesiser can only speak one utterance at a time, and is still speaking the next utterance (as opposed to the next next utterance).
How can I fix this bug?
Add this property to your viewController:
var manuallyStopping = false
Set it when you call stopSpeaking():
manuallyStopping = synthesizer.stopSpeaking(at: .immediate)
Check it in didFinish() and treat it like didCancel() if it is set, and then set it it false.
Also set manuallyStopping to false in didCancel()

Best strategy in swift to detect keyboad input in NSViewController

I want to detect keyboard input in my NSViewController.
The idea is to have several actions performed if the user presses certain keys followed by ENTER/RETURN.
I have checked if keyDown would be a appropriate way. But I would receive an event any time the user has pressed a key.
I also have though on using an NSTextField, set it to hidden and let it have the focus.
But maybe there are other better solution.
Any ideas?
Thanks
I've finally got a solution that I like.
First it has nothing todo with any hidden UI Elements but rather let the viewcontroller detect keyboard input.
var monitor: Any?
var text = ""
override func viewDidLoad() {
super.viewDidLoad()
self.monitor = NSEvent.addLocalMonitorForEvents(matching: .keyDown, handler: myKeyDownEvent)
}
override func viewWillDisappear() {
//Clean up in case your ViewController can be closed an reopened
if let monitor = self.monitor {
NSEvent.removeMonitor(monitor)
}
}
// Detect each keyboard event
func myKeyDownEvent(event: NSEvent) -> NSEvent {
// keyCode 36 is for detect RETURN/ENTER
if event.keyCode == 36 {
print(text)
text = ""
} else {
text.append( event.characters! )
}
return event
}

Redirect button action to another function in Swift

This might be an easy question, but I am new to Swift and do not know for which terms to google for.
For a menu bar application, I have implemented a custom function called doSomething that works just fine when it is bound to some button:
Class MainViewController:
{
#IBAction func doSomething(sender: NSButton)
{
// Do something when NSButton is pressed
}
}
However, I need to distinguish between left- and right click on the button, which in my case is a NSStatusBarButton. Following the suggestion from this answer, I have written the following into my AppDelegate.swift:
#NSApplicationMain class AppDelegate: NSObject, NSApplicationDelegate {
let statusItem = NSStatusBar.systemStatusBar().statusItemWithLength(-2)
var mainViewController: MainViewController?
func applicationDidFinishLaunching(notification: NSNotification)
{
if let button = statusItem.button
{
button.action = #selector(customClickAction)
button.sendActionOn(Int(NSEventMask.RightMouseDownMask.rawValue | NSEventMask.LeftMouseDownMask.rawValue))
}
}
func customClickAction(sender: NSButton)
{
let event:NSEvent! = NSApp.currentEvent!
if (event.type == NSEventType.RightMouseDown)
{
print("Right mouse button down")
}
else if (event.type == NSEventType.LeftMouseDown)
{
print("Left mouse button down")
mainViewController?.doSomething(_:) // THIS DOES NOT WORK
}
}
}
The above code snippet gives me the error message 'Expression resolves to an unused function' in XCode. I cannot figure out how to properly call the function doSomething from the MainViewController class within the customClickAction function, or equivalently, how to redirect the action of the statusItem.button via customClickAction to doSomething. I apologize if this question might seem too trivial for the Swift experts, but I am really in despair trying to figure this one out.
EDIT:
If the function customClickAction was not existing, I would simply write button.action = #selector(mainViewController?.show(_:)) in applicationDidFinishLaunching to call the function and everything works. However, part of my problem is that doing the same in my custom function would overwrite the binding once the left mouse button has been pressed for the first time.
Here is a question from someone who had the same Expression resolves to an unused function problem. In his/her case the problem was that the functions was called without the () after the function, so stop instead of stop() (if that makes sense).
OK, so now that we know what the compiler is trying to tell us, we can try to figure out what to do to solve it.
In your case the problem is that you need to send a sender parameter to your doSomething method, and that parameter must be of type NSButton as you've declared your self.
#IBAction func doSomething(sender: NSButton)
{
// Do something when NSButton is pressed
}
So, in your case, if you just pass the sender which you get as a parameter to your customClickAction along like so:
func customClickAction(sender: NSButton)
{
let event:NSEvent! = NSApp.currentEvent!
if (event.type == NSEventType.RightMouseDown)
{
print("Right mouse button down")
}
else if (event.type == NSEventType.LeftMouseDown)
{
print("Left mouse button down")
mainViewController?.doSomething(sender)
}
}
Then the compiler seems happy.
Here is the entire AppDelegate
#NSApplicationMain
class AppDelegate: NSObject, NSApplicationDelegate {
let statusItem = NSStatusBar.systemStatusBar().statusItemWithLength(-2)
var mainViewController: MainViewController? = MainViewController() //initialized
func applicationDidFinishLaunching(notification: NSNotification)
{
if let button = statusItem.button
{
button.action = #selector(customClickAction)
button.sendActionOn(Int(NSEventMask.RightMouseDownMask.rawValue | NSEventMask.LeftMouseDownMask.rawValue))
}
}
func customClickAction(sender: NSButton)
{
let event:NSEvent! = NSApp.currentEvent!
if (event.type == NSEventType.RightMouseDown)
{
print("Right mouse button down")
}
else if (event.type == NSEventType.LeftMouseDown)
{
print("Left mouse button down")
mainViewController?.doSomething(sender)
}
}
}
Hope that helps you.
Followup to your edit
You write
If the function customClickAction was not existing, I would simply write button.action = #selector(mainViewController?.show(_:)) in applicationDidFinishLaunching to call the function and everything works.
Its possible that I'm misunderstanding everything here, but why don't you then "just" assign the doSomething method to your button.action and then moves the check for left or right button to that method?
So you'd say:
button.action = #selector(mainViewController?.doSomething(_:))
and your doSomething would look like this:
#IBAction func doSomething(sender: NSButton)
{
// Do something when NSButton is pressed
let event:NSEvent! = NSApp.currentEvent!
if (event.type == NSEventType.RightMouseDown)
{
print("Right mouse button down")
}
else if (event.type == NSEventType.LeftMouseDown)
{
print("Left mouse button down")
}
}
"Left mouse button down" and "Right mouse button down" shows up in my console if I do that.
Swift 3 update
As #ixany writes in the comments:
Is it me or Swift 3? :) Seems that your solution needs to be updated to Swift 3 since I'm not able to adapt it using the current Xcode Beta
I've tried to upgrade the syntax to Swift 3.0 but there seems to be some problems with it.
The first problem is that I could not go directly to the doSomething method of MainViewController when assigning the selector, so I added a "local" step so to speak.
The AppDelegate now looks like this:
import Cocoa
#NSApplicationMain
class AppDelegate: NSObject, NSApplicationDelegate {
#IBOutlet weak var window: NSWindow!
let statusItem = NSStatusBar.system().statusItem(withLength: -2)
var mainViewController: MainViewController? = MainViewController()
func applicationDidFinishLaunching(_ aNotification: Notification) {
if let button = statusItem.button
{
button.action = #selector(AppDelegate.localButtonAction(sender:))
button.sendAction(on: [NSEventMask.leftMouseDown, NSEventMask.rightMouseDown])
}
}
func localButtonAction(sender: NSStatusBarButton) {
mainViewController?.doSomething(sender: sender)
}
}
(Notice the ´localButtonAction´ method, I hope someone else has a prettier way to do this)
And the MainViewController looks like this:
import Cocoa
class MainViewController: NSViewController {
override func viewDidLoad() {
super.viewDidLoad()
// Do view setup here.
}
#IBAction func doSomething(sender: NSStatusBarButton) {
// Do something when NSButton is pressed
let event:NSEvent! = NSApp.currentEvent!
if (event.type == NSEventType.rightMouseDown)
{
print("Right mouse button down")
}
else if (event.type == NSEventType.leftMouseDown)
{
print("Left mouse button down")
}
}
}
The problem with this is that the event is never of type rightMouseDown as far as I can see. I'm probably doing something wrong and I hope that someone else can make this work, but now it compiles at least and is Swift 3.0 syntax (using Xcode 8.0 - beta 6 :))
Hope that helps you #ixany