Is it possible to subclass UIEvent to embed some additional information?
I tried but keep getting exceptions and can't find anything about subclassing UIEvent.
class CustomEvent: UIEvent {
let payload: CustomData
override init(payload: CustomData) {
super.init()
self.payload = payload
}
}
017-03-30 08:51:17.504497 StealingTouches[1041:778674] -[StealingTouches.TouchEvent _firstTouchForView:]: unrecognized selector sent to instance 0x170053950
2017-03-30 08:51:17.505470 StealingTouches[1041:778674] *** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[StealingTouches.TouchEvent _firstTouchForView:]: unrecognized selector sent to instance 0x170053950'
*** First throw call stack:
(0x18bf991b8 0x18a9d055c 0x18bfa0268 0x18bf9d270 0x18be9680c 0x191e8494c 0x100026278 0x100026550 0x100026fc4 0x100027214 0x191e7be98 0x191e77328 0x191e47da0 0x19263175c 0x19262b130 0x18bf46b5c 0x18bf464a4 0x18bf440a4 0x18be722b8 0x18d926198 0x191eb27fc 0x191ead534 0x100029418 0x18ae555b8)
libc++abi.dylib: terminating with uncaught exception of type NSException
I have a collection of views nested 3 levels deep inside superviews. I want the deeply nested view to add some custom data to the event, and forward this data up to the outermost view.
See image. The green views should handle the event, do the appropriate calculations, save the data into the event, and forward the data up to the red view. Only the green views respond to events, but only the red view knows what to do with that event.
Green view handles the touches...
class GreenView: UIControl {
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
super.touchesBegan(touches, with: event)
//do some complicated calculation then save in custom event
let customEvent = CustomEvent.init(payload: calculations)
if let nextRespoonder = self.next {
nextRespoonder.touchesBegan(touches, with: customEvent)
}
}
}
Which then gets forwarded up to the yellow view...
class YellowView: UIControl {
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
super.touchesBegan(touches, with: event)
if let nextRespoonder = self.next {
nextRespoonder.touchesBegan(touches, with: event)
}
}
}
And finally the red view can extract the event payload and do what it needs to do...
class RedView: UIControl {
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
super.touchesBegan(touches, with: event)
if let customEvent = event as? CustomEvent {
let payload = customEvent.payload
//do something with the payload
}
}
}
Another option is to store the payload data locally in the green view, and then the only thing the red view needs to do is identify which green view initiated the event. This is fairly simple to do with hit test, but I have over a hundred of these green views and it can get fairly complicated to figure out which one is which based on hit test alone as sometimes green views overlap each other.
I learned that this pattern is the wrong way to handle this. The better thing to do is to use delegation. Assign redView as the delegate for greenView and just pass info up.
protocol GreenViewDelegate {
func onTouchesBegan(greenView: GreenView)
}
class GreenView: UIControl {
var delegate: GreenViewDelegate?
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
super.touchesBegan(touches, with: event)
self.delegate?.onTouchesBegan(greenView: self)
}
}
class RedView: UIControl, GreenViewDelegate {
init() {
greenView.delegate = self
}
func onTochesBegan(greenView: GreenView) {
//extract whatever data you want
}
}
Related
As Swift is my first programming language and also seeing that I have no Objective C experience...
I'm having difficulty understanding #objc in relation to methods.
How do I use the #objc syntax to conform to my methods?
Is there another way to select a method without using the #selector syntax?
Here is the code that I'm having difficulty with(mainly the #objc attempt at the startGame method):
import UIKit
#objc class ViewController: UITableViewController {
var allWords = [String]()
var usedWords = [String]()
override func viewDidLoad() {
super.viewDidLoad()
navigationItem.rightBarButtonItem =
UIBarButtonItem(barButtonSystemItem: .add, target: self, action:
#selector(promptForAnswer))
navigationItem.leftBarButtonItem = UIBarButtonItem(title: "New
Word", style: .plain, target: self, action: #selector(startGame))
if let startWordsURL = Bundle.main.url(forResource: "start",
withExtension: "txt") {
if let startWords = try? String(contentsOf: startWordsURL) {
allWords = startWords.components(separatedBy: "\n")
}
}
if allWords.isEmpty {
allWords = ["silkworm"]
}
#objc func startGame() {
title = allWords.randomElement()
usedWords.removeAll(keepingCapacity: true)
tableView.reloadData()
{
startGame()
}
A few observations:
You do not need #objc in your view controller declaration.
The two action/selector methods should bear #objc qualifier.
I would suggest that you give these two methods descriptive names that clearly indicate that they are called when the user taps on a particular button, e.g.:
#objc func didTapNewWord(_ sender: UIBarButtonItem) {
...
}
#objc func didTapAdd(_ sender: UIBarButtonItem) {
...
}
Note, I also added a parameter to these methods. That makes it entirely unambiguous that they are button handlers. You do not need to do that, but now you can glance at the code and immediately grok what the method is for.
Obviously, you will change the code that adds these target actions accordingly:
navigationItem.rightBarButtonItem = UIBarButtonItem(barButtonSystemItem: .add,
target: self,
action: #selector(didTapAdd(_:)))
navigationItem.leftBarButtonItem = UIBarButtonItem(title: "New Word",
style: .plain,
target: self,
action: #selector(didTapNewWord(_:)))
Be careful with the placement of braces. Swift allows you to declare functions inside functions. So make sure that these selector methods are instance methods of the view controller, and not, for example, private functions declared inside another function (i.e. viewDidLoad).
If you start to lose track of the braces, you can select all the code in this file and press control+i (or in Xcode menus, “Editor” » “Structure” » “Re-Indent”). If you have missing braces somewhere, the re-indentation of the code will make this jump out at you.
So pulling that together, you get something like:
// ViewController.swift
import UIKit
class ViewController: UITableViewController {
var allWords = [String]()
var usedWords = [String]()
override func viewDidLoad() {
super.viewDidLoad()
configureButtons()
fetchData()
}
}
// MARK: - Actions
extension ViewController {
#objc func didTapNewWord(_ sender: UIBarButtonItem) {
startGame()
}
#objc func didTapAdd(_ sender: UIBarButtonItem) {
...
}
}
// MARK: - UITableViewDataSource
extension ViewController {
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
...
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
...
}
}
// MARK: - Private utility methods
private extension ViewController {
func configureButtons() {
navigationItem.rightBarButtonItem = UIBarButtonItem(barButtonSystemItem: .add,
target: self,
action: #selector(didTapAdd(_:)))
navigationItem.leftBarButtonItem = UIBarButtonItem(title: "New Word",
style: .plain,
target: self,
action: #selector(didTapNewWord(_:)))
}
func fetchData() {
guard
let startWordsURL = Bundle.main.url(forResource: "start", withExtension: "txt"),
let startWords = try? String(contentsOf: startWordsURL).components(separatedBy: "\n"),
!startWords.isEmpty
else {
allWords = ["silkworm"]
return
}
allWords = startWords.filter { !$0.isEmpty }
}
func startGame() {
title = allWords.randomElement()
usedWords.removeAll(keepingCapacity: true)
tableView.reloadData()
}
}
A few final observations on my code sample (not directly related to your question, but just to explain why structured it like I did):
I like to put methods into extensions, so that they are in logical groups. This makes it easier to follow what is going on at a glance. You can also collapse/expand these extensions so that while you are editing, you can focus on the relevant code.
The MARK comments just puts nice section headers in the Xcode jump bar, again, making it easier to jump about in one’s code.
I personally don't put anything in the action methods except a call to some method with the “business logic”. This separates the “view” code (the handling of the button) from the business logic. Some day, you may start using view models or presenter objects, so embracing this separation of responsibilities now will make that eventual transition easier. It will also make it easier to write unit tests when you get around to that (e.g. you write unit tests for the "start game" logic, not not the tapping of a button).
I think you have syntax error in the #objc method. It should be:
#objc
func functionName() {
}
for you it will be:
#objc
func startGame() {
title = allWords.randomElement()
usedWords.removeAll(keepingCapacity: true)
tableView.reloadData()
}
I want to generate a menu which will call functions on a class other than that one which generated the menu, and then place that in Finder. However, when I click the menu item, the system error beep plays and my function is never called.
Here's a SSCCE:
import Cocoa
import FinderSync
#objc(FinderSync)
class FinderSync: FIFinderSync {
var observedFolder = URL(fileURLWithPath: "/")
override init() {
super.init()
NSLog("\(FinderSync.self.className()) launched from \(Bundle.main.bundlePath)")
// Set up the directory we are syncing.
FIFinderSyncController.default().directoryURLs = [self.observedFolder]
}
// MARK: - Menu and toolbar item support
override var toolbarItemName: String {
return "Wonderful Test App"
}
override var toolbarItemToolTip: String {
return "This is wonderful"
}
override var toolbarItemImage: NSImage {
return NSImage(named: NSImage.cautionName)!
}
override func menu(for menuKind: FIMenuKind) -> NSMenu {
let menu = NSMenu(title: "")
let menuItem = NSMenuItem(title: "Click me!", action: #selector(SomeOtherClass.remoteAction), keyEquivalent: "")
menuItem.target = SomeOtherClass.shared
menuItem.action = #selector(SomeOtherClass.remoteAction)
menu.addItem(menuItem)
return menu
}
}
#objc(SomeOtherClass)
public class SomeOtherClass: NSObject {
public static let shared = SomeOtherClass()
deinit {
NSLog("Deallocated!")
preconditionFailure("Shared instance should never be deallocated!")
}
#IBAction
#objc(remoteAction:)
public func remoteAction(_ sender: Any?) {
NSLog("Remote!")
}
}
I've verified via the memory debugger that SomeOtherClass.shared is still in memory before, during, and after the menu item is clicked, so it's not being deallocated or anything.
It appears you can't add actions which are within any class other than your FinderSync extension's principal class. Which is about as stupid as everything else about NSMenuItem, so I'm not surprised.
So, you'll have to move the action(s) into your FinderSync class, despite how ugly that might be for organization.
In my Mac App, I listen to key press events and pass them on to the internal client, depending on modifiers and key code.
Currently, I'm facing the problem, that I can't get a hold of the "Ctrl+Tab" event. It seems that the "App" itself tries to handle this, which makes sense for tab based applications. So I disabled the Tabbingmode, but still, the Ctrl+Tab never fires the KeyDown event. Any other combination of key code and modifier seems to pass just fine.
Any suggestions on how to get the key down event fired for Ctrl+Tab?
In my testing, NSView's -keyDown: method does not seem to get called on NSView subclasses for control-tab key events. However, you can intercept them at the application level with an NSApplication subclass:
#interface MyApplication: NSApplication
#end
#implementation MyApplication
- (void)sendEvent:(NSEvent *)event {
if (event.type == NSEventTypeKeyDown &&
[event.charactersIgnoringModifiers isEqualToString:#"\t"] &&
(event.modifierFlags & NSEventModifierFlagDeviceIndependentFlagsMask) == NSEventModifierFlagControl) {
NSLog(#"ctrl-tab");
}
[super sendEvent:event];
}
#end
Use
override func viewDidLoad() {
super.viewDidLoad()
NSEvent.addLocalMonitorForEvents(matching: .keyDown) {
if self.myKeyDown(with: $0) {
return nil
} else {
return $0
}
}
}
and
func myKeyDown(with event: NSEvent) -> Bool {
// handle keyDown only if current window has focus, i.e. is keyWindow
guard let locWindow = self.view.window,
NSApplication.shared.keyWindow === locWindow else {
return false
}
switch event.specialKey {
case NSEvent.SpecialKey.tab:
// your event for tab action
return true
default:
break
}
return false
}
if you need shortcut keys
func myKeyDown(with event: NSEvent) -> Bool {
// handle keyDown only if current window has focus, i.e. is keyWindow
guard let locWindow = self.view.window,
NSApplication.shared.keyWindow === locWindow else {
return false
}
switch event.specialKey {
case NSEvent.SpecialKey.tab:
// your code for tab action
return true
default:
break
}
switch event.modifierFlags.intersection(.deviceIndependentFlagsMask) {
case [.command]:
switch event.charactersIgnoringModifiers! {
case "w":
// your code for cmd+w action (example)
break
default:
break
}
}
return false
}
I know that theoretically it's possible to create multiple instances of the same class with a property that would have a different value for each instance.
The thing is, I can't make it happen.
Each time I'm creating a new instance, it gets the property's value of the other instances, and when I'm changing one value for an instance, it changes the other's too.
So my guess is that I'm doing something wrong (obviously), like accessing the class property value instead of the instance property value... Here's the code.
class CustomUIImageView: UIImageView {
var someParameter: Bool = false // This is the property I want to be different in each version of the instance.
}
class ClassSiege: UIViewController, UIGestureRecognizerDelegate {
var myView: CustomUIImageView! //the instance declaration.
// I use this gesture recognizer to find out the value of the instance I'm tapping on.
func handleTap (sender: UITapGestureRecognizer) {
print("value of someParameter \(self.myView.someParameter)")
}
func handlePan(recognizer: UIPanGestureRecognizer) {
let iv: UIView! = recognizer.view
let translation = recognizer.translationInView(self.view)
iv.center.x += translation.x
iv.center.y += translation.y
recognizer.setTranslation(CGPointZero, inView: self.view)
var centerBoardX = BlackBoard.center.x // 'Blackboard' is a fixed image on the screen.
var centerBoardY = BlackBoard.center.y
var centerRondX = iv.center.x
var centerRondY = iv.center.y
if centerRondY - centerBoardY < 100 {
self.myView.someParameter = true // If the distance between myView and the blackboard is under 100 I want the instance's property to become true.
} else {
self.myView.someParameter = false // On the other hand, if the distance is greater than 100, I want it to be false.
}
}
// When the user pushes a button, it triggers this method that creates a new instance of myView and add it to the screen.
#IBAction func showContent(sender: AnyObject) {
// some code...
// Here I'm creating the instance of the view and I give it the gesture recognizer parameters. I don't think that relevant to the issue, so I'm not adding the code.
}
}
So clearly that's not the good way to do it, but what's wrong, and how can it be solved?
Basing my answer on your related question.
If what you want to achieve is initializing a property with a value that you provide, just add a new parameter to the initializer. If for instance you are using the initializer with a CGRect passed in, then you can implement an initializer like this:
class CustomUIImageView : UIImageView {
let someParameter : Bool
required init(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
init(frame: CGRect, someParameter: Bool) {
self.someParameter = someParameter
super.init(frame: frame)
}
}
I hope that this is what you are looking for - let me know otherwise.
I've found the solution, and if you've been facing the same issu, here's how to deal with it.
The secret is to downcast the recognizer.view to take the parameter of the subclass CustomUIImageView.
here's how :
func handleTap (sender: UITapGestureRecognizer) {
println("value of someParameter \(self.myView.someParameter)") //I use this gesture recognizer to find out the value of the instance I'm tapping on.
}
func handlePan(recognizer:UIPanGestureRecognizer) {
let iv : UIView! = recognizer.view
let translation = recognizer.translationInView(self.view)
iv.center.x += translation.x
iv.center.y += translation.y
recognizer.setTranslation(CGPointZero, inView: self.view)
var centerBoardX = BlackBoard.center.x //blackboard is a fixed image on the screen.
var centerBoardY = BlackBoard.center.y
var centerRondX = iv.center.x
var centerRondY = iv.center.y
var myParameter = recognizer.view as CustomUIImageView //<- this is the key point. Downcasting let you access the custom subclass parameters of the object that is currently moved
if centerRondY - centerBoardY < 100 {
myParameter.someParameter = true //so now I'm really changing the parameter's value inside the object rather than changing a global var like I did before.
} else {
myParameter.someParameter = false
}
}
//when user pushes a button, it triggers this func that creates a new instance of myView and add it to the screen.
#IBAction func showContent(sender: AnyObject) {
some code...
//here I'm creating the instance of the view and I give it the gesture recognizer parameters. I don't think that relevant to the issue, so I'm not adding the code.
}
OK, what I need is pretty straightforward, though I can still find nothing specific.
I want to be able to :
track double-click events
track when the NSTableView is in focus, and the "Return" key is pressed.
How would you go about it?
P.S. I've had a look into NSTableViewDelegate specification, but I can't find anything useful.
For double click you need to do just these :
-(void)awakeFromNib{
[self.tableView setDoubleAction:#selector(thisMethod)];
//And if you wish to take selector dynamically, I guess you know how to do :)
}
-(void)thisMethod{
NSLog(#"double clicked");
}
For the return event, subclass your NSTableView and override keyDown:
Swift 5.x:
override func keyDown(with event: NSEvent) {
if event.characters?.count == 1 {
let character = event.keyCode
switch (character) {
// 36 is return
case UInt16(36):
print("return: \(event)")
default:
print("any other key: \(event)")
}
} else {
super.keyDown(with: event)
}
}
There is a way to handle the Return key without having to manually check for its key code.
I'll show the answer in Swift, but it can be applied in Objective-C as well.
First, override keyDown(with:) in your view controller subclass that controls the table view and call interpretKeyEvents(_:):
override func keyDown(with event: NSEvent) {
interpretKeyEvents([event])
}
Second, in the same view controller subclass, override insertNewLine(_:). This is called when the user presses the Return key:
override func insertNewLine(_ sender: Any?) {
// Add your logic to handle the Return key being pressed
}
Here's an example:
class TableViewController: NSViewController {
#IBOutlet var tableView: NSTableView!
override func keyDown(with event: NSEvent) {
interpretKeyEvents([event])
}
override func insertNewLine(_ sender: Any?) {
guard tableView.selectedRow >= 0 else { return }
print("Pressed Return on row \(tableView.selectedRow)")
}
}