How to detect touch event across multiple views on my view controller? - swift

In my viewController I have programmatically created several squares as sub-views.
Imagine it as a chessboard.
I would like to implement a method that executes some actions upon the sub-view (e.g., change the background color of a single square) when it gets touched by the user.
I have unsuccessfully tried with the UITapGestureRecognizer - I have read from Apple documentation that it applies only to one view at a time.
Do you have any suggestions?
Many thanks

You can subclass UIWindow class and handle all the events in - (void)sendEvent:(UIEvent *)event there like sending notifications to all the views or communicate with some other way. Check the following repository for more info:
https://github.com/mwinoto/TapDetectingWindow

Do it like this
class YourCustomView : UIView {
func addTapGesture(action : #escaping ()->Void ){
self.gestureRecognizers?.forEach({ (gr) in
self.removeGestureRecognizer(gr)
})
let tap = MyTapGestureRecognizer(target: self , action: #selector(self.handleTap(_:)))
tap.action = action
tap.numberOfTapsRequired = 1
self.addGestureRecognizer(tap)
self.isUserInteractionEnabled = true
}
}
or you can use this even in a extension of UIView if you're going to use it in multiple types of UIViews.
And then use it like:
let yourView = YourCustomView()
yourView.addTapGesture{[weak self] in
//implement tap event here
}
Notice that [weak self] is going to avoid getting a strong reference for closure to avoid a retain cycle there.

Related

Swift macOS SegmentedControl Action not getting called

Description
I am trying to use NSSegmentedControls to transition between Child ViewControllers. The ParentViewController is located in Main.storyboard and the ChildViewControllers are located in Assistant.storyboard. Each ChildViewController has a SegmentedControl divided into 2 Segments and their primary use is to navigate between the ChildViewControllers. So they are set up as momentaryPushIn rather than selectOne. Each ChildViewController uses a Delegate to communicate with the ParentViewController.
So in the ParentViewController I added the ChildViewControllers as following:
/// The View of the ParentViewController configured as NSVisualEffectView
#IBOutlet var visualEffectView: NSVisualEffectView!
var assistantChilds: [NSViewController] {
get { return [NSViewController]() }
set(newValue) {
for child in newValue { self.addChild(child) }
}
}
override func viewDidLoad() {
super.viewDidLoad()
// Do view setup here.
addAssistantViewControllersToChildrenArray()
}
override func viewWillAppear() {
visualEffectView.addSubview(self.children[0].view)
self.children[0].view.frame = self.view.bounds
}
private func addAssistantViewControllersToChildrenArray() -> Void {
let storyboard = NSStoryboard.init(name: "Assistant", bundle: nil)
let exampleChild = storyboard.instantiateController(withIdentifier: "ExampleChild") as! ExampleChildViewController
let exampleSibling = storyboard.instantiateController(withIdentifier: "ExampleSibling") as! ExampleSiblingViewController
exampleChild.navigationDelegate = self
exampleSibling.navigationDelegate = self
assistantChilds = [exampleChild, exampleSibling]
}
So far so good. The ExampleChildViewController has an NSTextField instance. While I am in the scope of the TextField, I can trigger the action of the SegmentedControls. Its navigating forward and backward as it should. But once I leave the scope of the TextField I can still click the Segments, but they are not triggering any action. They should be able to navigate forward and backward even if the TextField is not the current "First Responder" of the application. I think I am missing something out here, I hope anyone can help me with this. I know the problem is not the NSSegmentedControl because I am seeing the same behavior with an NSButton, which is configured as Switch/Checkbox, in the SiblingViewController. I just don't have any idea anymore what I am doing wrong.
It`s my first time asking a question myself here, so I hope the way I am doing is fine for making progress with the solution. Let me know if I can do something better/different or if I need to provide more information about something.
Thanks in advance!
Additional Information
For the sake of completeness:
The ParentViewController itself is embedded in a ContainerView,
which is owned by the RootViewController. I can't imagine this does
matter in any way, but this way we are not missing something out.
I am actually not showing the navigation action, because I want to
keep it as simple as possible. Furthermore the action is not problem,
it does what I want it to do. Correct me if I am wrong with this.
Possible solutions I found while researching, which did not work for me:
Setting window.delegate of the ChildViewControllers to NSApp.windows.first?.delegate
Setting the ChildViewController to becomeFirstResponder in its func viewWillAppear()
visualEffectView.addSubview(self.children[0].view, positioned: NSWindow.OrderingMode.above, relativeTo: nil)
Related problems/topics I found while researching:
Basic segmented control not working
Adding and Removing Child View Controllers
NSSegmentedControl - Odd appearance when placed in blur view
How to set first responder to NSTextView in Swift?
How to use #selector in Swift 2.2 for the first responder
Accessing methods, actions and/or outlets from other controllers with swift
How to use Child View Controllers in Swift 4.0 programmatically
Container View Controllers
issues with container view
Control a NSTabViewController from parent View
How to detect when NSTextField has the focus or is it`s content selected cocoa
SOLUTION
let parentViewControllerInstance = self.parent as! ParentViewController
segmentedControl.target = parentViewControllerInstance
In my case I just had to set the delegate as the target of the sendAction method.
Background
Ok, after hours of reading the AppKit Documentation I am now able to answer my own question.
First, debugging the UI showed that the problem was definitely not in the ViewHierarchy.
So I tried to think about the nature of NSButton and NSSegmentedControl. At some point I noticed that both are subclasses of NSControl.
class NSSegmentedControl : NSControl
class NSButton : NSControl
The AppKit Documentation says:
Discussion
Buttons are a standard control used to initiate actions within your app. You can configure buttons with many different visual styles, but the behavior is the same. When clicked, a button calls the action method of its associated target object. (...) You use the action method to perform your app-specific tasks.
The bold text points to the key of the solution – of its associated target object. Typically I define the action of an control item like this:
button.action = #selector(someFunc(_:))
This causes the NSControl instance to call this:
func sendAction(_ action: Selector?, to target: Any?) -> Bool
Parameter Description from the documentation:
Parameters
theAction
The selector to invoke on the target. If the selector is NULL, no message is sent.
theTarget
The target object to receive the message. If the object is nil, the application searches the responder chain for an object capable of handling the message. For more information on dispatching actions, see the class description for NSActionCell.
In conclusion the NSControl instance, which was firing the action method (in my case the NSSegmentedControl), had no target to send its action to. So it was only able to send its action method across the responder chain - which obviously has been nil while the first responder was located in another view.

Gesture Recognizer and UIImageView With Custom Property

My app uses images which can have various statuses so I am using custom properties as tags. This works ok, but my tap gesture recognizer can't seem to access these properties. When the image is tapped, I need the action to depend on the state of these properties. Is there a way the gesture recognizer can read these custom properties from the tapped subclassed UIImageView or should I take a different approach? Thanks!
public class advancedTagUIImageView: UIImageView {
var photoViewedStatus: Bool?
var photoLikedStatus: Bool?
}
viewDidLoad() {
let imageView = advancedTagUIImageView(frame:CGRect(origin: CGPoint(x:50, y:50), size: CGSize(width:100,height:100)))
imageView.image = UIImage(named: dog.png)
imageView.photoViewedStatus = false
imageView.photoLikeStatus = false
imageView.tag = 7
imageView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(soundTapped)))
view.addSubview(imageView)
}
#objc func soundTapped(gesture: UIGestureRecognizer) {
let photoTag = gesture.view!.tag // this works great
let isPhotoLiked = gesture.view!.photoLikeStatus // this doesn't work
// do whatever
}
Swift is strongly typed. The type of the gesture.view property is UIView which doesn't have the properties defined in your advancedTagUIImageView class. This is because you could theoretically also attach your UITapGestureRecognizer to any other type of view. In which case the program would crash on your soundTapped method, because you're just assuming that gesture.view is an advancedTagUIImageView which might not always be the case.
For the compiler to let you access these properties you need first check if gesture.view is really your sublcass like this:
if let photoView = (gesture.view? as? advancedTagUIImageView) {
// you can access your tags here
let isPhotoLiked = photoView.photoLikeStatus
} else {
// you might want to handle the case that the gesture was invoked from another view. If you're certain this should not happen, maybe just throw an assertion error to get notified in case it still does.
}
PS: According to the Swift API Design Guidelines type names should be capitalized, so in your case it should be AdvancedTagUIImageView. Not following these guidelines might not crash your program, but doing so might make your life a lot easier should you ever need to write code together with other people.

performSegueWithIdentifier from a NSView subclass?

I have a document window that contains a number of NSView subclasses, switched between using a tab control. Each of the subclasses, and the window's ViewController, support different user actions accessed through menu items tied to the First Responder.
I'd like to perform a segue from one of those views in response to a menu item. However, NSView does not support performSegueWithIdentifier, it appears to be something that is part of NSViewController alone.
Can someone suggest a way around this? I have seen suggestions to pass the VC into the views, but I am not clear how to do that. Or perhaps there is a better way?
view.containingController.performSegue()
note: you have to add containingController to your views
I WOULD add the viewController to the responder chain and then make containingController a computed property in an extension!
e.g. add vc as responder:
override func awakeFromNib() {
super.awakeFromNib()
self.nextResponder = self.view
for subview in self.view.subviews {
subview.nextResponder = self
}
}
e.g. containingController in extension
extension NSView {
var containingController: NSViewController? {
get {
while(self.nextResponder != nil) {
if(self.nextResponder is NSViewController) {
return self.nextResponder
}
}
return nil
}
}
}
You could do that (see Daij-Djan's answer), however it is not what I would recommend, since a hypothetical programmer who will be using your code, but is not familiar with it (let's say, you in a year :) ) might be caught by surprise by such behaviour.
I would recommend you to add a delegate (conforming to your custom protocol, let's call it MyViewDelegate) to your NSView with a method like viewRequiresToPerformTransition(view: YourViewSubclass). Then you implement this method (more generally, you conform to MyViewDelegate protocol) in your view controller and inside its implementation perform any segue you want.

why pass viewController as variable?

First of all I have to say I am really new to swift and Objective C.I am learning them by myself.
I have a question for this code
I have a delegate in my SettingViewController called "settingsViewControllerFinished" and it pass the whole controller as a variable.
the code like this:
in my SettingViewController.swift
protocol SettingViewControllerDelegate: class {
func settingsViewControllerFinished(settingsViewController: SettingsViewController)
}
#IBAction func close(sender: AnyObject) {
dismissViewControllerAnimated(true, completion: nil)
self.delegate?.settingsViewControllerFinished(self)
}
I am confused.What did you mean if you pass the whole controller as a variable?(maybe the question is silly for you)
in my viewController:
func settingsViewControllerFinished(settingsViewController: SettingsViewController)
{
self.brushWidth = settingsViewController.brush
self.opacity = settingsViewController.opacity
self.red = settingsViewController.red
self.green = settingsViewController.green
self.blue = settingsViewController.blue
}
I guess the reason is:I pass everything in SettingViewController to ViewController so that I could use the variables in SettingViewController.
Am I rihgt?
Generally you are correct, yes: passing the SettingViewController back to its delegate enables the original caller to not have to keep a reference to the created and shown SettingViewController since the delegate method sends the relevant information along already.
But there is more: In some cases of delegates this style is useful for something different. Imagine a click handler consisting of a function func somethingGotClicked(sender:YourSenderType). If your class creates multiple instances of YourSenderType and shows them at the same time registering itself as their delegate there would be no way to know which one got clicked if there was no sender parameter. In some func somethingGotClicked() you would not know which one got clicked. That capability is often needed when showing multiple UITableView or UICollectionView is one single view with one single instances set as their delegate.

update viewcontroller from appdelegate - best practice?

I am playing around with iBeacons implementing CoreLocation methods inside AppDelegate.swift (methods are implemented in AppDelegate to ensure App Background Capabilities)
In a SingleView application that comes with "ViewController.swift", what would be the best practice to pass data from AppDelegate to ViewController to update Views, say UIImages, UILabels or UITableViews ?
I have successfully implemented two different approaches:
1) Delegation, by firing Viewcontroller Delegation methods inside AppDelegate.swift:
protocol BeaconLocationDelegate { func minorBeaconChanged(minorValue:NSNumber) }
var locationDelegate: BeaconLocationDelegate
locationDelegate?.minorBeaconChanged(nearestBeacon.minor)
ViewController.swift:
in viewDidLoad method:
(UIApplication.sharedApplication().delegate as AppDelegate).locationDelegate = self
(I find this to look rather ugly - any better way to declare this property as delegate?)
protocol implementation:
func minorBeaconChanged(minorValue: NSNumber) {
// do fancy stuff here
}
2) By creating a reference to ViewController inside AppDelegate:
let viewController:ViewController = window!.rootViewController as ViewController
viewController.doSomethingFancy()
Both approaches work fine for me, but I think the first approach via delegation is more elegant and the better choice once you have multiple ViewControllers.
Any recommendations?
I think the second solution is much easier. It also ensures the view controller does not go out of memory (is "released") and the app delegate does not try to contact an inexistent object.
Also, I do not understand the delegate argument in the case of more than one controllers. In that case, you would have to have multiple delegates, really not an ideal situation. Or you would have to change the delegate to different view controllers from the outside - also not very elegant and quite risky.
For the multiple view controller scenario I recommend notifications via NSNotificationCenter. This is a lose coupling that ensures only those objects that need to react receive the message. View controllers should start listening to the notifications upon creation and deregister from the notification center when they disappear.
How about this?
In AppDelegate
var minorBeaconChanged: NSNumber -> () = { minor in }
And call this method when location is updated.
In ViewController's viewWillAppear
( UIApplication.sharedApplication().delegate as? AppDelegate )!.minorBeaconChanged = { minor in
// Do with minor
}
EDIT: 27/8/2015
If you wanna have multiple observers.
In AppDelegate
var minorBeaconObservers: [ NSNumber -> () ] = []
And call all the item in minorBeaconObservers with 'minor'
In ViewController's viewWillAppear
( UIApplication.sharedApplication().delegate as? AppDelegate )!.minorBeaconObservers.append(
{ minor in
// Do with minor
}
)