How Do I Hit Test A SubView's Layers? - ios12
I have encountered a challenge in my application. I want to be able to tap in a subview and determine which of the subview's layers was tapped. I (naively?) chose to use the hitTest(:CGPoint) method of the subview's root layer. The results were not what I expected. I have created a playground to illustrate the difficulty.
The playground (code below):
Creates a view controller and its root view
Adds a subview to the root view
Adds a sublayer to the subview's root layer
Here is what it looks like:
The root view's root layer is black
The subview's root layer is red
The subview's sublayer is translucent gray
The white line is there for illustrative purposes
With the playground running I start tapping, beginning at the top of the white line and moving down. Here are the print statements produced by the tap gesture handling method:
Root view tapped #(188.5, 12.5)
Hit test returned "Root View -> Root Layer" with frame (0.0, 0.0, 375.0, 668.0)
Root view tapped #(188.5, 49.0)
Hit test returned "Root View -> Root Layer" with frame (0.0, 0.0, 375.0, 668.0)
Root view tapped #(188.5, 88.5)
Hit test returned "Root View -> Root Layer" with frame (0.0, 0.0, 375.0, 668.0)
Sub view tapped #(140.0, 11.0)
Sub view tapped #(138.5, 32.5)
Sub view tapped #(138.5, 55.5)
Sub view tapped #(138.0, 84.0)
Sub view tapped #(139.0, 113.0)
Hit test returned "Sub View -> Root Layer" with frame (50.0, 100.0, 275.0, 468.0)
Sub view tapped #(138.0, 138.5)
Hit test returned "Sub View -> Root Layer" with frame (50.0, 100.0, 275.0, 468.0)
Sub view tapped #(140.0, 171.5)
Hit test returned "Sub View -> Root Layer" with frame (50.0, 100.0, 275.0, 468.0)
Sub view tapped #(137.5, 206.5)
Hit test returned "Sub View -> Sub Layer" with frame (50.0, 100.0, 175.0, 268.0)
Sub view tapped #(135.5, 224.0)
Hit test returned "Sub View -> Sub Layer" with frame (50.0, 100.0, 175.0, 268.0)
You can see that as I tap in the root view things are "normal". As soon as I enter into the subview the hit test no longer finds a layer until I am more than 100 points below the top of the subview; at which point the subview's root layer is encountered. 100 points after that the sublayer of the subview's root layer is encountered.
What is going on? Well, apparently, when the subview was added to the root view, the subview's root layer became a sublayer of the root view's root layer and its frame was altered to reflect its placement in the root view's root layer instead of in the sub view.
So, may questions are:
Am I correct regarding what has apparently happened?
Is it normal and expected?
How do I deal with it?
Thank you for taking the time to consider my question.
Here is the playground code:
//: [Previous](#previous)
import UIKit
import PlaygroundSupport
class ViewController: UIViewController {
class View : UIView {
override func draw(_ rect: CGRect) {
let context = UIGraphicsGetCurrentContext()!
context.setStrokeColor(UIColor.white.cgColor)
context.setLineWidth(2.0)
context.move(to: CGPoint(x: rect.midX, y: 0))
context.addLine(to: CGPoint(x: rect.midX, y: rect.maxY))
context.strokePath()
}
}
private var subview: View!
override func loadView() {
let rootView = View(frame: CGRect.zero)
rootView.backgroundColor = .black
rootView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(tapGestureRecognized(_:))))
rootView.layer.name = "Root View -> Root Layer"
self.view = rootView
}
override func viewDidLayoutSubviews() {
subview = View(frame: view.frame.insetBy(dx: 50.0, dy: 100.0))
subview.backgroundColor = .red
subview.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(tapGestureRecognized(_:))))
subview.layer.name = "Sub View -> Root Layer"
let sublayer = CALayer()
sublayer.bounds = subview.layer.bounds.insetBy(dx: 50, dy: 100)
sublayer.position = CGPoint(x: subview.layer.bounds.midX, y: subview.layer.bounds.midY)
sublayer.backgroundColor = UIColor.gray.cgColor.copy(alpha: 0.5)
sublayer.name = "Sub View -> Sub Layer"
subview.layer.addSublayer(sublayer)
view.addSubview(subview)
}
#objc func tapGestureRecognized(_ sender: UITapGestureRecognizer) {
guard sender.state == .ended, let tappedView = sender.view else { return }
var viewName: String
switch tappedView {
case view: viewName = "Root"
case subview: viewName = "Sub"
default: return
}
let location = sender.location(in: tappedView)
print("\n\(viewName) view tapped #\(location)")
if let layer = tappedView.layer.hitTest(location) {
print("Hit test returned <\(layer.name ?? "unknown")> with frame \(layer.frame)")
}
}
}
PlaygroundPage.current.liveView = ViewController()
PlaygroundPage.current.needsIndefiniteExecution = true
//: [Next](#next)
Update: Problem Solved
With guidance from the always knowledgeable Matt Nueburg (and his most excellent book), I modified the tap gesture handler method to the following:
#objc func tapGestureRecognized(_ sender: UITapGestureRecognizer) {
guard sender.state == .ended, let tappedView = sender.view else { return }
var viewName: String
let hitTestLocation: CGPoint
switch tappedView {
case view:
viewName = "Root"
hitTestLocation = sender.location(in: tappedView)
case subview:
viewName = "Sub"
hitTestLocation = sender.location(in: tappedView.superview!)
default: return
}
print("\n\(viewName) view tapped #\(sender.location(in: tappedView))")
if let layer = tappedView.layer.hitTest(hitTestLocation) {
print("Hit test returned <\(layer.name ?? "unknown")> with frame \(layer.frame)")
}
}
Now the console output looks good:
Root view tapped #(186.0, 19.0)
Hit test returned "Root View -> Root Layer" with frame (0.0, 0.0, 375.0, 668.0)
Root view tapped #(187.0, 45.0)
Hit test returned "Root View -> Root Layer" with frame (0.0, 0.0, 375.0, 668.0)
Root view tapped #(187.0, 84.5)
Hit test returned "Root View -> Root Layer" with frame (0.0, 0.0, 375.0, 668.0)
Sub view tapped #(138.0, 9.0)
Hit test returned "Sub View -> Root Layer" with frame (50.0, 100.0, 275.0, 468.0)
Sub view tapped #(137.5, 43.5)
Hit test returned "Sub View -> Root Layer" with frame (50.0, 100.0, 275.0, 468.0)
Sub view tapped #(138.5, 77.5)
Hit test returned "Sub View -> Root Layer" with frame (50.0, 100.0, 275.0, 468.0)
Sub view tapped #(138.0, 111.0)
Hit test returned "Sub View -> Sub Layer 1" with frame (50.0, 100.0, 175.0, 268.0)
Sub view tapped #(138.5, 140.5)
Hit test returned "Sub View -> Sub Layer 1" with frame (50.0, 100.0, 175.0, 268.0)
Sub view tapped #(139.5, 174.5)
Hit test returned "Sub View -> Sub Layer 1" with frame (50.0, 100.0, 175.0, 268.0)
Second Update - Clearer Code
I am circling back with the code that I ended up using to detect touches on sub layers. It is, I hope, more clear than the playground code above.
private struct LayerTouched : CustomStringConvertible {
let view: UIView // The touched view
var layer: CALayer // The touched layer
var location: CGPoint // The touch location expressed in the touched layer's coordinate system
init(by recognizer: UIGestureRecognizer) {
view = recognizer.view!
let gestureLocation = recognizer.location(in: view)
// AFAIK - Any touchable layer will have a super layer. Hence the forced unwrap is okay.
let hitTestLocation = view.layer.superlayer!.convert(gestureLocation, from: view.layer)
layer = view.layer.hitTest(hitTestLocation)!
location = layer.convert(gestureLocation, from: view.layer)
}
var description: String {
return """
Touched \(layer)
of \(view)
at \(location).
"""
}
}
the hitTest(:CGPoint) method of the subview's root layer
But you have forgotten that layer hit testing works in a special way. The CGPoint you provide must be in the coordinate system of the superlayer of the layer you are hit testing.
Related
Xamarin Form passing parameters between page keep a hard link to it
I try the new Shell of Xamarin Form 4 for a small project. I have a list of order, then someone chooses an order and start picking some inventory for this order with barcode. To be simple, I use 2 views and 2 viewmodel. The process is: 1. User select an order the click a button "pickup" 2. I use ViewModelLocator (TinyIoc) to resolve the correct ViewModel of the pickup view 3. I call Initialize on the new ViewModel and pass some parameters needed. Like a list of items needed to be pickup. 4. Open the view in modal state. I don't understand that if I change some qty in the list I pass on the second viewmodel, then hit the back button (modal view close). The quantity changed is now in the original viewmodel. How this is possible? I was thinking that passing parameter to a function do not share the same variable but just copy it?? Viewmodel of the first view (look at the Initialize function the List passed and the JobEnCours object) private async Task GoPickup() { Device.BeginInvokeOnMainThread(async () => { if (this.CodeJob != null && this.CodeJob != "") { this.IsBusy = true; PrepChariotSP3ViewModel vm = ViewModelLocator.Resolve<PrepChariotSP3ViewModel>(); await vm.InitializeAsync(this._jobEnCours, this.ComposantesPick.ToList()) ; await Shell.Current.Navigation.PushAsync(new PrepChariotSP3Page(vm)); this.IsBusy = false; } }); } the the Initialize code on the second Viewmodel (look I set the JobEnCours.Description=Test) public async Task InitialiseAsync(Job jobEnCours, List<ComposantePick> composantePick) { Title = "Ceuillette: " + jobEnCours.CodeProd; this._jobEnCours = jobEnCours; this.ComposantesPick = new ItemsChangeObservableCollection<ComposantePick>(); foreach(ComposantePick c in composantePick) { this.ComposantesPick.Add(c); } jobEnCours.Description = "test"; So, If I do the back button, then in the first VM the JobEnCours.Description is now set to "test" how this is possible?! Any idea?
unwind segue Xcode 10 swift with navigation controller not working
I have pursued the internet for an answer and following steps in apple documentation but to no avail. I have two view controllers. One which contains a list of favourite items (FavouriteTableViewController), another which contains the details of the item being added to the list of favourites (ExplorationDetailViewController). I have a save button on the navigation bar (from a navigation controller scene) in the ExplorationDetailViewController that I want to click to kick off the unwind segue. I have carried out the following steps, but when I click on the save button, it is completely unresponsive. I am including the most relevant code for the unwind. I have created prepareforsegue function in ExplorationResultViewController from where I will be passing data for an image and a label to the other ViewController as part of the unwind segue class ExplorationResultViewController: UIViewController { var exploration_result: Favourites? #IBOutlet weak var ExplorationLabel: UILabel! #IBOutlet weak var ExplorationPhoto: UIImageView! override func prepare(for segue: UIStoryboardSegue, sender: Any?) { // Pass the selected object to the new view controller. super.prepare(for: segue, sender: sender) let photo = ExplorationPhoto.image let name = ExplorationLabel.text ?? "" exploration_result = Favourites(name: name, photo: photo) } Then in the FavouriteTableViewController, I have added an unwindtoHere method which adds image and label to the list. class FavouritesTableViewController: UITableViewController { // MARK: Properties var favourites = [Favourites]() // MARK: Actions #IBAction func unwindToFavouriteList(sender: UIStoryboardSegue) { if let sourceViewController = sender.source as? ExplorationResultViewController, let favourite = sourceViewController.exploration_result{ // Add a new meal. let newIndexPath = IndexPath(row: favourites.count, section: 0) favourites.append(favourite) tableView.insertRows(at: [newIndexPath], with: .automatic) } Then going into my storyboard, I control+drag the Save button in the ExplorationDetailViewController and connect it to the exit item at the top of the scene. My unwind method appears in the pop up as: 'unwindToFavouriteListWithSender'. I select this. I should be good to go now, but when I run the simulator and click the Save button, it is absolutely unresponsive. See StoryBoard Design Please help
From your comments, I'm guessing that the problem is that there is no FavouritesTableViewController in the view controller hierarchy at the time that the Save button is pressed. You cannot unwind to something that isn't already present: "unwind" means go back to something that was instantiated earlier and remains present, above the current view controller in the view controller hierarchy. For example, let's say we have UINavigationController -> (root view controller) -> FavouritesTableViewController -> (push) ExplorationResultViewController Now we can unwind from the Exploration controller to the Favourites controller, because the Favorites controller is already in the hierarchy before the Exploration controller. It sounds like maybe you are not appreciating that fact.
How can shift my tab view from one to another in JavaFx
I am doing a JavaFx project . I have a table view which is populated by few names (e.g. BG Manager , User Management , Find Acl , ....) . Each time I click on each one of those names (or each row of that table ) it will open a new tab which is again populated with the information I set out earlier inside . Here is an example function that being called once I click on a row with the name of BG Management : private void callBGManager(){ try { Tab tab = new Tab (); FXMLLoader bgm = new FXMLLoader(getClass().getResource("/views/BGMWindow.fxml")); BGMController bgmc = new BGMController(_session); bgm.setController(bgmc); Parent root = bgm.load(); for (Tab tabs : TabPane1.getTabs()){ if (tabs.getText().equals(ListView1.getSelectionModel().getSelectedItem())){ SingleSelectionModel<Tab> selectionModel = TabPane1.getSelectionModel(); selectionModel.select(tab); return; } } tab.setContent(root); tab.setText(ListView1.getSelectionModel().getSelectedItem()); tab.closableProperty().set(true); TabPane1.setTabClosingPolicy(TabPane1.getTabClosingPolicy().SELECTED_TAB); TabPane1.getTabs().add(tab); SingleSelectionModel<Tab> selectionModel = TabPane1.getSelectionModel(); selectionModel.select(tab); }catch(Exception e){ e.printStackTrace(); } } There is no problem in the compilation or operation of this code as it works well . Here is the scenario for the better understanding of the situation . I have lets say 5 rows in my list and every time I click on each if it's not already opened it will show up and if it's already open nothing happens . Lets say I am already in another tab view and the BG Manager is already open . How can I shift to BG Manager once I click re-click on it :)
Hello Danial u can use (row)cell.setUserData(); and by deafult set for each true value. And onClick on this row(cell) you should to check this data (row)cell.getUserData(); and if it is true than change it to false and open window in another case do nothing. EXAMPLE: table.setRowFactory( tv -> { TableRow<MyType> row = new TableRow<>(); row.setOnMouseClicked(event ->{ boolean result = (boolean) row.getUserData(); if(result){ row.setUserData(false); //There will be your method to open new window methodToOpenNewWindow(row); }else{ Alert alert = new Alert(Alert.AlertType.INFORMATION); alert.setTitle("Information"); alert.setContentText("This entity already was opened."); alert.show(); } }); return row ; }); In method methodToOpenNewWindow on close window set row userData back to true .
how to make an object visible again in game maker
Fist of all, I'm really sorry for my bad English and I pretty new to game maker . I have 2 object in the game : obj_apple and obj_door( I unchecked visible box) my question is how can I make an obj_door visible in the room when all the obj_apple are destroyed?
Object obj_door, Step event, place code (Add event -> Step -> Step -> tab Control -> section Code, first icon (Execute code)): if !instance_exists(obj_apple) visible = true;
Another option, so you aren't making a check in every step event, is to put the check for the number of obj_apple in the destroy event of obj_apple. For example, in the destroy event of obj_apple you would have: if (instance_number(object_index) == 0) { with (obj_door) { visible = true; } }
In javaFX need object reference to node under mouse cursor
I need a reference to the object under the mouse cursor in a javaFX Pane (or ScrollPane) The following code removes the node under the cursor from the Pane. The node object is stored in an ArrayList and has a unique id. If I had a reference to the node object, it would be a simple matter to remove it from the list. I'd be happy just to be able to pull the id of the node object out of the target description supplied by the MouseEvent's description of the target, and use it to find my node in the list. Note: I am creating these nodes dynamically and they are nameless: myList.add(new TYPE()). Here's the snippet that I'm using to remove the node in the Pane; root.setOnMouseClicked(new EventHandler<MouseEvent>() { #Override public void handle (MouseEvent me) { // would like a line here that grabbed the reference to the target pane.getChildren().remove(me.getTarget()); } });
It would be better for you to put the setOnMouseClicked listener to the node on which you want to remove from the list. List<Node> listOfNodes = new ArrayList(); Node node = new Node(); listOfNodes.add(node); node.setOnMouseClicked((event) -> { listOfNodes.remove(node); }); Note: Better use ObservableList than the ordinary ArrayList especially if you want that node to be removed also out from the root pane.
To James_D: Reason for disbelief: the syntax that fails to get past the compiler is the statement containing the expression me.getTarget().getText() which is commented out. It seemed to me that that ought to have worked. for (Text tl : textList) if ( (me.getTarget()) == (tl) ) { System.out.println("HuRah!"); System.out.println("text is " + tl.getText()); //System.out.println("text is " + me.getTarget().getText()); textList.remove(me.getTarget()); }