iOS Swift Interrupt Keyboard Events - ios

I have problem to intercept keyboard events. I have connected my iOS with SteelSeries Free (gamepad controller) which when connected to iOS will be detected as a Bluetooth Keyboard. This is tested when I open Notes, any button presses in the gamepad will write a letter.
I need to intercept this button presses and run my own functions but unfortunately I am unable to do so.
I've been trying to use GCController but apparently it is not detected as Game Controller object. When I print the count, it shows as 0. My code below.
let gameControllers = GCController.controllers() as! [GCController]
println("configureConnectedGameControllers count: \(gameControllers.count)")
So I assumed it is because the gamepad is detected as bluetooth keyboard that is why its not detected as game controller. And so I attempted to use UIKeyCommand instead. Below is my code:
override func viewDidLoad() {
super.viewDidLoad()
var keys = [UIKeyCommand]()
for digit in "abcdefghijklmnopqrstuvwxyz"
{
keys.append(UIKeyCommand(input: String(digit), modifierFlags: .Command, action: Selector("keyPressed:")))
keys.append(UIKeyCommand(input: String(digit), modifierFlags: .Control, action: Selector("keyPressed:")))
keys.append(UIKeyCommand(input: String(digit), modifierFlags: nil, action: "pressKey"))
}
}
override func canBecomeFirstResponder() -> Bool {
return true
}
func keyPressed(command: UIKeyCommand) {
println("another key is pressed") //never gets called
}
func pressKey() {
println("a key is pressed")
}
But even with the above implementation, nothing is printed in the console when i press a button at the gamepad.
This confuses me. So please help me if you know any answer to this. Thanks in advance!

I finally managed to get it working. Below is the code if anyone ever needs it.
var keys = [UIKeyCommand]()
override func viewDidLoad() {
super.viewDidLoad()
//configureGameControllers()
for digit in "abcdefghijklmnopqrstuvwxyz"
{
keys.append(UIKeyCommand(input: String(digit), modifierFlags: nil, action: Selector("keyPressed:")))
}
}
override func canBecomeFirstResponder() -> Bool {
return true
}
override var keyCommands: [AnyObject]? {
get {
return keys
}
}
func keyPressed(command: UIKeyCommand) {
println("user pressed \(command.input)")
}

Related

Why canPerformAction get called again when action of menuItem is called?

Below is my code, I found when click menu "pasteAndGo", two log strings are printed: 1. paste and go show 2.paste and go clicked. My requirement is when the menu is shown, log "paste and go show" is shown. When it is clicked, log "paste and go clicked" is shown.
class MyTextField: UITextField {
private func Init() {
let menuController: UIMenuController = UIMenuController.shared
menuController.isMenuVisible = true
let pasteAndGoMenuItem: UIMenuItem = UIMenuItem(title: "pasteAndGo", action: #selector(pasteAndGo(sender:)))
let myMenuItems: NSArray = [pasteAndGoMenuItem]
menuController.menuItems = myMenuItems as? [UIMenuItem]
}
#objc private func pasteAndGo(sender: UIMenuItem) {
Print("paste and go clicked")
}
override func canPerformAction(_ action: Selector, withSender sender: Any?) -> Bool {
let pasteboard = UIPasteboard.general
if action == #selector(pasteAndGo) {
if pasteboard.url != nil {
Print("paste and go show")
return true
} else {
return false
}
}
return super.canPerformAction(action, withSender: sender)
}
}
Your code works as implemented:
In the instant you press your pasteAndGo menu item, the UIKit framework calls canPerformAction to ask whether it is allowed to execute the action or not. Here, you print "paste and go show"
Since you return true, your action pasteAndGo(sender:) is executed and prints "paste and go clicked"
To react on the menu item being shown, you'll have to register to the notification center with the UIMenuController.willShowMenuNotification, like this:
// create a property
var token: NSObjectProtocol?
// then add observer
self.token = NotificationCenter.default.addObserver(forName: UIMenuController.willShowMenuNotification, object: nil, queue: .main)
{ _ in
print ("paste and go show")
}
and don't forget to unsubscribe (NotificationCenter.default.removeObserver) once your viewcontroller gets dismissed.
if let t = self.token {
NotificationCenter.default.removeObserver(t)
}
Update
You could also do so (without properties) in Init
// in Init
var token = NotificationCenter.default.addObserver(forName: UIMenuController.willShowMenuNotification, object: nil, queue: .main)
{ _ in
print ("paste and go show")
NotificationCenter.default.removeObserver(token)
}

Why RxSwift Subscribe just run once in First launch viewWillAppear?

I write a subscribe in viewWillAppear.
But it also run once in first launch app.
When I push to another viewcontroller, I use dispose().
Then I back in first viewcontroller, my subscribe func in viewWillAppear don't run.
What's wrong with my rx subscribe?
var listSubscribe:Disposable?
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
listSubscribe = chatrooms.notifySubject.subscribe({ json in
print("*1") //just print once in first launch
self.loadContents()
})
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
let controllers = tabBarController?.navigationController?.viewControllers
if (controllers?.count)! > 1 {
listSubscribe?.dispose()
}
}
RxSwift documentation says "Note that you usually do not want to manually call dispose; this is only an educational example. Calling dispose manually is usually a bad code smell."
Normally, you should be doing something like this -
let disposeBag = DisposeBag()
override func viewDidLoad() {
super.viewDidLoad()
whatever.subscribe(onNext: { event in
// do stuff
}).disposed(by: self.disposeBag)
}
As for your question, I believe you don't need to re-subscribe because you subscription will be alive and 'notifySubject' will send you updates whenever there are any.
Maybe you can get some reactive implementation of viewWillAppear and similar functions? And forget about manual disposables handling... For example your UIViewController init will contain something like this:
rx.driverViewState()
.asObservable()
.filter({ $0 == .willAppear })
.take(1) // if you need only first viewWillAppear call
.flatMapLatest({ _ in
// Do what you need
})
And the implementation of driverViewState:
public extension UIViewController {
public enum ViewState {
case unknown, didAppear, didDisappear, willAppear, willDisappear
}
}
public extension Reactive where Base: UIViewController {
private typealias _StateSelector = (Selector, UIViewController.ViewState)
private typealias _State = UIViewController.ViewState
private func observableAppearance(_ selector: Selector, state: _State) -> Observable<UIViewController.ViewState> {
return (base as UIViewController).rx
.methodInvoked(selector)
.map { _ in state }
}
func driverViewState() -> Driver<UIViewController.ViewState> {
let statesAndSelectors: [_StateSelector] = [
(#selector(UIViewController.viewDidAppear(_:)), .didAppear),
(#selector(UIViewController.viewDidDisappear(_:)), .didDisappear),
(#selector(UIViewController.viewWillAppear(_:)), .willAppear),
(#selector(UIViewController.viewWillDisappear(_:)), .willDisappear)
]
let observables = statesAndSelectors
.map({ observableAppearance($0.0, state: $0.1) })
return Observable
.from(observables)
.merge()
.asDriver(onErrorJustReturn: UIViewController.ViewState.unknown)
.startWith(UIViewController.ViewState.unknown)
.distinctUntilChanged()
}
}

UIKeyCommand is not disabled when typing into a text field (Swift)

I made a calculator app (swift), and I set up some UIKeyCommands so that if the user has a bluetooth keyboard, they can type numbers/symbols into the calculator.
They work like so:
UIKeyCommand(input: "4", modifierFlags: [], action: "TypeFourInCalculator:")
func TypeFourInCalculator(sender: UIKeyCommand) {
btn4Press(UIButton)
}
That all worked well, adding a four into the calculator when the user pressed the four key (even though the calculator itself has no text field). However: I also have a couple of standard text fields, and I want the UIKeyCommands to stop when the user goes into one of those text fields (so they can type regularly with the BT keyboard again). Without disabling the UIKeyCommands, typing results in calculator functions and no input into the text field.
So I tried this in an attempt to disable UIKeyCommands when the text field becomes the first responder:
let globalKeyCommands = [UIKeyCommand(input: "4", modifierFlags: [], action: "TypeFourInCalculator:"), UIKeyCommand(input: "5", modifierFlags: [], action: "TypeFiveInCalculator:")]
override var keyCommands: [UIKeyCommand]? {
if powertextfield.isFirstResponder() == false { // if the user isn't typing into that text field
return globalKeyCommands // use key commands
} else { // if the user is typing
return nil // disable those UIKeyCommands
}
This works occasionally but yet often doesn't work. If the user has not typed anything with the BT keyboard yet (i.e. not activating the key commands, I guess) then they can type with the BT keyboard as normal into a text field. But if they have already been typing numbers into the calculator via UIKeyCommand, they can not type into the text field (well, sometimes it works with normal behavior, sometimes it fails like it did before I added that preventative code). Typed text just doesn't appear in that text field and, instead, it just calls the calculator command.
So what can I do to disable these UIKeyCommands when the user starts typing
in a normal text field?
Instead of making keyCommands a computed property, you can use addKeyCommand(_:) and removeKeyCommand(_:) methods for UIViewControllers. Subclass your text field like this:
class PowerTextField: UITextField {
var enableKeyCommands: (Bool->())?
override func becomeFirstResponder() -> Bool {
super.becomeFirstResponder()
enableKeyCommands?(false)
return true
}
override func resignFirstResponder() -> Bool {
super.resignFirstResponder()
enableKeyCommands?(true)
return true
}
}
Then in your UIViewController:
override func viewDidLoad() {
super.viewDidLoad()
// globalKeyCommands is a view controller property
// Add key commands to be on by default
for command in globalKeyCommands {
self.addKeyCommand(command)
}
// Configure text field to callback when
// it should enable key commands
powerTextField.enableKeyCommands = { [weak self] enabled in
guard let commands = self?.globalKeyCommands else {
return
}
if enabled {
for command in globalKeyCommands {
self?.addKeyCommand(command)
}
} else {
for command in globalKeyCommands {
self?.removeKeyCommand(command)
}
}
}
}
Instead of an optional stored procedure that gets configured by the UIViewController, you could setup a delegate protocol for the UITextField that the UIViewController will adopt.
Also, if you need to stop these key commands when a UIAlertController pops up, you can make a subclass of UIAlertController that implements the viewWillAppear(_:) and viewWillDisappear(_:) event methods similar to how you implemented becomeFirstResponder() and resignFirstResponder(), by calling an enableKeyCommands(_:) optional stored procedure that's configured by your main view controller during its viewDidLoad().
As for the explanation of why this is happening, perhaps the most likely explanation is that it's a bug. I'm not sure why this is irregular in your testing though. I think you could try to isolate under what conditions it works or doesn't. It's not obvious why this would only happen for bluetooth keyboards, but there are plenty of edge cases that can creep in when you start introducing wireless technologies.
So I was facing the same problem and found a really good and simple solution.
I made a way to figure out, if the current firstResponder is a text field, or similar.
extension UIResponder {
private weak static var _currentFirstResponder: UIResponder? = nil
public static var isFirstResponderTextField: Bool {
var isTextField = false
if let firstResponder = UIResponder.currentFirstResponder {
isTextField = firstResponder.isKind(of: UITextField.self) || firstResponder.isKind(of: UITextView.self) || firstResponder.isKind(of: UISearchBar.self)
}
return isTextField
}
public static var currentFirstResponder: UIResponder? {
UIResponder._currentFirstResponder = nil
UIApplication.shared.sendAction(#selector(findFirstResponder(sender:)), to: nil, from: nil, for: nil)
return UIResponder._currentFirstResponder
}
#objc internal func findFirstResponder(sender: AnyObject) {
UIResponder._currentFirstResponder = self
}
}
And then simply used this boolean value to determine which UIKeyCommands to return when overriding the keyCommands var:
override var keyCommands: [UIKeyCommand]? {
var commands = [UIKeyCommand]()
if !UIResponder.isFirstResponderTextField {
commands.append(UIKeyCommand(title: "Some title", image: nil, action: #selector(someAction), input: "1", modifierFlags: [], propertyList: nil, alternates: [], discoverabilityTitle: "Some title", attributes: [], state: .on))
}
commands.append(UIKeyCommand(title: "Some other title", image: nil, action: #selector(someOtherAction), input: "2", modifierFlags: [.command], propertyList: nil, alternates: [], discoverabilityTitle: "Some title", attributes: [], state: .on))
return commands
}

iOS Keyboard: textDocumentProxy.documentContextBeforeInput.isEmpty Unexpectedly Returning Nil (when it shouldn't)

I'm working on a custom iOS keyboard – much of it is in place, but I'm having trouble with the delete key and more specifically with deleting whole words.
The issue is that self.textDocumentProxy.documentContextBeforeInput?.isEmpty returns Nil even if there are characters remaining in the text field.
Here's the background:
In case you're not familiar, the way it works on the stock iOS keyboard is that while holding the backspace key, the system deletes a character at a time (for the first ~10 characters). After 10 characters, it starts deleting whole words.
In the case of my code, I'm deleting 10 single characters and then I successfully delete a couple of whole words, then suddenly self.textDocumentProxy.documentContextBeforeInput?.isEmpty returns Nil, even if there are characters remaining in the text field.
I've looked all over documentation and the web and I don't see anyone else with the same issue, so I'm sure I'm missing something obvious, but I'm baffled.
Here are the relevant parts of my class definition:
class KeyboardViewController: UIInputViewController {
//I've removed a bunch of variables that aren't relevant to this question.
var myInputView : UIInputView {
return inputView!
}
private var proxy: UITextDocumentProxy {
return textDocumentProxy
}
override func canBecomeFirstResponder() -> Bool {
return true
}
override func viewDidLoad() {
super.viewDidLoad()
//proxy let proxy = self.textDocumentProxy
NSNotificationCenter.defaultCenter().addObserver(self, selector: Selector("keyboardWillAppear"), name: UIKeyboardWillShowNotification, object: nil)
NSNotificationCenter.defaultCenter().addObserver(self, selector: Selector("keyboardWillHide"), name: UIKeyboardWillHideNotification, object: nil)
self.myInputView.translatesAutoresizingMaskIntoConstraints = true
// Perform custom UI setup here
view.backgroundColor = UIColor(red: 209 / 255, green: 213 / 255, blue: 219 / 255, alpha: 1)
NSNotificationCenter.defaultCenter().addObserver(self, selector: "touchUpInsideLetter:", name: "KeyboardKeyPressedNotification", object: nil)
showQWERTYKeyboard()
}
I setup the listeners and actions for the backspace button as I'm also configuring a bunch of other attributes on the button. I do that in a Switch – the relevant part is here:
case "<<":
isUIButton = true
normalButton.setTitle(buttonString, forState: UIControlState.Normal)
normalButton.addTarget(self, action: "turnBackspaceOff", forControlEvents: UIControlEvents.TouchUpInside)
normalButton.addTarget(self, action: "turnBackspaceOff", forControlEvents: UIControlEvents.TouchUpOutside)
normalButton.addTarget(self, action: "touchDownBackspace", forControlEvents: UIControlEvents.TouchDown)
let handleBackspaceRecognizer : UILongPressGestureRecognizer = UILongPressGestureRecognizer(target: self, action: "handleBackspaceLongPress:")
normalButton.addGestureRecognizer(handleBackspaceRecognizer)
buttonWidth = standardButtonWidth * 1.33
nextX = nextX + 7
Thanks for any thought you can offer.
****Modifying the original post to help shed more light on the issue****
Here are the 4 functions that are designed to create the backspace behavior. They appear to work properly for at least the first two whole words, but then the optional checking that was correctly suggested starts evaluating to nil and it stops deleting.
//Gets called on "Delete Button TouchUpInside" and "Delete Button TouchUpOutside"
func turnBackspaceOff() {
self.backspaceIsPressed = false
keyRepeatTimer.invalidate()
}
//Handles a single tap backspace
func touchDownBackspace() {
(textDocumentProxy as UIKeyInput).deleteBackward()
}
//Handles a long press backspace
func handleBackspaceLongPress(selector : UILongPressGestureRecognizer) {
if selector.state == UIGestureRecognizerState.Began {
self.backspaceIsPressed = true
self.keyRepeatTimer = NSTimer.scheduledTimerWithTimeInterval(0.1, target: self, selector: "backspaceRepeatHandlerFinal", userInfo: nil, repeats: true)
print("handleBackspaceLongPress.Began")
}
else if selector.state == UIGestureRecognizerState.Ended {
self.backspaceIsPressed = false
keyRepeatTimer.invalidate()
numberOfKeyPresses = 0
print("handleBackspaceLongPress.Ended")
}
else {
self.backspaceIsPressed = false
keyRepeatTimer.invalidate()
numberOfKeyPresses = 0
print("handleBackspaceLongPress. Else")
}
}
func backspaceRepeatHandlerFinal() {
if let documentContext = proxy.documentContextBeforeInput as String? {
print(documentContext)
}
print("backspaceRepeatHandlerFinal is called")
if self.backspaceIsPressed {
print("backspace is pressed")
self.numberOfKeyPresses = self.numberOfKeyPresses + 1
if self.numberOfKeyPresses < 10 {
proxy.deleteBackward()
}
else {
if let documentContext = proxy.documentContextBeforeInput as NSString? {
let tokens : [String] = documentContext.componentsSeparatedByString(" ")
var i : Int = Int()
for i = 0; i < String(tokens.last!).characters.count + 1; i++ {
(self.textDocumentProxy as UIKeyInput).deleteBackward()
}
}
else {
print("proxy.documentContextBeforeInput was nil")
self.keyRepeatTimer.invalidate()
self.numberOfKeyPresses = 0
}
}
}
else {
print("In the outer else")
self.keyRepeatTimer.invalidate()
self.numberOfKeyPresses = 0
}
}
Finally, I don't fully understand why, but XCode automatically inserted these two functions below when I created the keyboard extension. I've modified them slightly in an effort to try to get this to work.
override func textWillChange(textInput: UITextInput?) {
// The app is about to change the document's contents. Perform any preparation here.
super.textWillChange(textInput)
}
override func textDidChange(textInput: UITextInput?) {
// The app has just changed the document's contents, the document context has been updated.
var textColor: UIColor
//let proxy = self.textDocumentProxy
if proxy.keyboardAppearance == UIKeyboardAppearance.Dark {
textColor = UIColor.whiteColor()
} else {
textColor = UIColor.blackColor()
}
super.textDidChange(textInput)
}
Let's describe what the line below is doing:
if !((self.textDocumentProxy.documentContextBeforeInput?.isEmpty) == nil) {
First, it takes an object marked as optional (by the ? letter):
let documentContext = self.textDocumentProxy.documentContextBeforeInput
Then, it tries to read it's property called isEmpty:
let isEmpty = documentContext?.isEmpty
and then it evaluates the contition:
if !(isEmpty == nil) {
There are two mistakes. The first one is that you are comparing Bool value with nil. Another one is that you aren't sure that documentContext is not a nil.
So, let's write your code in a more appropriate way:
if let documentContext = self.textDocumentProxy.documentContextBeforeInput { // Make sure that it isn't nil
if documentContext.isEmpty == false { // I guess you need false?
// Do what you want with non-empty document context
}
}

Calling IBAction from another method without parameter

In my program 2 functions (IBAction player.Move(UIButton) and autoMove()) are supposed to be called by turns till all of the fields (UIButtons) has been clicked. For this I've created a function play(). However, I don't know how can I put the IBAction playerMove inside of play() function, because I need no parameter here.
I've found some answers and tried self.playerMove(nil) and self.playerMove(self) but it doesn't work.
import UIKit
class ViewController: UIViewController {
#IBOutlet var cardsArray: Array<UIButton> = []
var randomCard = 0
override func viewDidLoad() {
super.viewDidLoad()
self.play()
// Do any additional setup after loading the view, typically from a nib.
}
func play () {
self.autoMove()
self.playerMove(self) // <----- here is my problem
}
#IBAction func playerMove(sender: UIButton) {
switch (sender) {
case self.cardsArray[0]:
self.cardPressedAll(0)
case self.cardsArray[1]:
self.cardPressedAll(1)
case self.cardsArray[2]:
self.cardPressedAll(2)
case self.cardsArray[3]:
self.cardPressedAll(3)
default: break
}
}
func cardPressedAll (cardNumber: Int) {
self.cardsArray[cardNumber].enabled = false
self.cardsArray[cardNumber].setBackgroundImage(UIImage(named: "cross"), forState: UIControlState.Normal)
self.cardsArray.removeAtIndex(cardNumber)
}
func autoMove (){
self.randomCard = Int(arc4random_uniform(UInt32(self.cardsArray.count)))
self.cardsArray[self.randomCard].enabled = false
self.cardsArray[self.randomCard].setBackgroundImage(UIImage(named: "nought"), forState: UIControlState.Normal)
self.cardsArray.removeAtIndex(self.randomCard)
}
}
Either you have to call playerMove: without a button, in which case you have to declare the sender parameter as an optional. Like:
#IBAction func playerMove(sender: UIButton?) {
UIButton means that you have to pass in a button. nil is not a button, but with UIButton?, that is to say Optional<UIButton>, nil is a valid value meaning the absence of a button.
Or you have to work out which button you want to pass to playerMove: to make it do what you want. Sit down and work out what you want to have happen, and what the code needs to do in order to make that happen.
Try
self.playerMove(UIButton())
Your func playerMove has parameters expecting sender to be of type UIButton, self or nil would be an unexpected object.
Edit:
You could us optional parameters by placing ?. This would allow you to call self.playerMove(nil) if needed.
#IBAction func playerMove(sender: UIButton?) {
if sender != nil {
//handle when button is passed
} else {
//handle when nil is passed
}
}
doSomeTask(UIButton()) in swift 5.0 and onward worked for me

Resources