I have two UIViews A and B. Both of these views are placed one on another. Suppose View A is placed above B. So when I tap on View A it is consuming all tap actions. But I dont want that action to be consumed till View A only. I want to get actions on View B where it was tapped.
All my view has userInteractionEnabled = true
class PassthroughView : UIView {
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView?
{
let view = super.hitTest(point, with: event)
return view == self ? nil : view
}
override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
print("Tap Called")
return false
}
}
I tried these codes after see some solutions but doesnt seems to be working.
Also View A has swipe gesture to work.
_V_i_e_w_B___
| ____
| |View A
| |
| |
| |
| |
|__|__
|_____
In UIView subclass override hitTest method only
class PassthroughView : UIView {
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView?
{
let view = super.hitTest(point, with: event)
return view == self ? nil : view
}
}
And create view A with the subclass and another view B with UIView class.
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let b = UIView(frame: CGRect(x: 0, y: 0, width: 300, height: 300))
b.backgroundColor = .gray
b.center = view.center
view.addSubview(b)
let a = PassthroughView(frame: CGRect(x: 0, y: 0, width: 275, height: 275))
a.backgroundColor = .lightGray
a.center = view.center
view.addSubview(a)
}
}
Whenever View A is placed above View B change the userInteractionEnabled to false for View A and your tap will be passed to View B.
Related
I am trying to build the iOS app that will track the user interactions with screen, what user clicks and what gestures user does. My plan was to create it as a OverlayView that could be put over any iOS app in a form of a View, without breaking any previous functionalities. I am currently testing it with the button that has another View on top of it.
Currently I am managing to either register click in top-layer View and track the gesture, or in button action function, but couldn't manage to get them both triggered at once.
override func viewDidLoad() {
super.viewDidLoad()
let button = UIButton(frame: CGRect(x: 100, y: 100, width: 100, height: 50))
button.backgroundColor = .green
button.addTarget(self, action: #selector(buttonClicked), for: .touchUpInside)
self.view.addSubview(button)
let topLayerView = UIView(frame: CGRect(x: 0, y: 0, width: 400, height: 900))
self.view.addSubview(topLayerView)
topLayerView.backgroundColor = .yellow
self.view.bringSubviewToFront(topLayerView)
topLayerView.alpha = 0.1
let singleTap: UITapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(singleTap))
singleTap.numberOfTapsRequired = 1
topLayerView.addGestureRecognizer(singleTap)
topLayerView.isUserInteractionEnabled = true
}
This is code that I have, and I can get the call to either buttonClicked function or singleTap function.
I looked over the web and found many similar questions, but nothing worked for me.
I have tried creating the new class that conforms to UIView protocol, and overrides 'point' function, but that didn't change a thing.
I don't think you can distribute a single touch to multiple views. However, I really don't think you need to.
You can subclass UIView and override hitTest to detect events and forward the events to the subviews you are interested in. If the event doesn't correspond to a subview, then you just return the superview so that the superview can process the event.
Here is what this method does:
Filters for subviews that are not itself and that are valid for touches by checking if the view is hidden, transparent, of if user interaction is disabled. Yes this is necessary in order to be compliant with the documentation: https://developer.apple.com/documentation/uikit/uiview/1622469-hittest
Propagate events to subviews of the superview.
Triggers an event if a subview has been touched.
Miscellaneous error checking
Note that this method will propagate events subviews that are nested inside other subviews. This works recursively.
The other thing I want to point out is how the subviews property is reversed. This is because the topmost subviews are stored at the end of the array, and those are the ones we want to process first.
class TrackerView: UIView {
var trackerAction: (TrackerEvent) -> Void = { _ in }
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
guard let superview = superview else {
return nil
}
lazy var superviewPoint = convert(point, to: superview)
let topMostViews = superview.subviews.reversed()
let subview = topMostViews.first(where: {
let subviewPoint = self.convert(point, to: $0)
let valid = $0.isUserInteractionEnabled && !$0.isHidden && $0.alpha >= 0.01
return valid && $0.point(inside: subviewPoint, with: event) && $0 !== self
})
guard let subview = subview else {
return processHit(for: superview, event: event, at: superviewPoint)
}
let subviewPoint = self.convert(point, to: subview)
guard let subviewHit = subview.hitTest(subviewPoint, with: event) else {
return processHit(for: superview, event: event, at: superviewPoint)
}
return processHit(for: subviewHit, event: event, at: superviewPoint)
}
private func processHit(for view: UIView?, event: UIEvent?, at point: CGPoint) -> UIView? {
guard let view = view, let event = event else { return view }
trackerAction(TrackerEvent(
viewType: type(of: view),
touchPoint: point,
timestamp: event.timestamp
))
return view
}
}
The event structure just stores some info that may be useful to you. Modify as needed. However, the reason why I include the timestamp is because hitTest is called more than once for a single event. The timestamp will allow you to weed out duplicates. It will also allow you to sort the events.
struct TrackerEvent: Hashable, Comparable, CustomStringConvertible {
let viewType: UIView.Type
let touchPoint: CGPoint
let timestamp: TimeInterval
func hash(into hasher: inout Hasher) {
hasher.combine(timestamp)
}
static func < (lhs: TrackerEvent, rhs: TrackerEvent) -> Bool {
lhs.timestamp < rhs.timestamp
}
static func == (lhs: TrackerEvent, rhs: TrackerEvent) -> Bool {
lhs.timestamp == rhs.timestamp
}
var description: String {
let elements = [
"viewType: \(viewType)",
"touchPoint: \(touchPoint)",
"timestamp: \(String(format: "%.02f", timestamp))",
]
let elementsJoined = elements.joined(separator: ", ")
return "[\(elementsJoined)]"
}
}
You may want a utility class to weed out duplicates every so often before processing them.
class TrackerEventHandler {
var events: Set<TrackerEvent> = []
private var timer: Timer?
func queueEvent(_ event: TrackerEvent) {
print("Queueing event", event)
events.insert(event)
}
func start() {
timer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { [weak self] timer in
self?.events.sorted().forEach { event in
print("Handling event: \(event)")
}
self?.events.removeAll()
}
}
func stop() {
timer?.invalidate()
}
}
Usage is as follows:
class ViewController: UIViewController {
let handler = TrackerEventHandler()
override func viewDidLoad() {
super.viewDidLoad()
self.handler.start()
let topLayerView = TrackerView(frame: CGRect(x: 0, y: 0, width: 400, height: 900))
topLayerView.trackerAction = handler.queueEvent(_:)
self.view.addSubview(topLayerView)
// ...
}
// ...
}
Note that this is not a perfect implementation. There are limitations, but it might be a good starting point.
I have two view controllers, B is presented on top of A, but not full screen. I am using a UIPresentationController to custom B's frame. What I want is, when tap on A, B is dismissed and A responds to it's own tap gesture.
How to achieve this within B and the UIPresentationController, without touching A's code?
I tried to add a full screen background view for B, but don't know how to pass the tap gesture down without changing A.
Customize a background view, always return false for pointInside method to ignore the touch event and do whatever needed before return, such as dismiss B.
class BackgroundView: UIView {
override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
// anything you want
return false
}
}
Add the custom background view to the UIPresentationController. Set containerView's isUserInteractionEnabled to false, otherwise the UITransitionView will block the touch event to pass down.
class MyPresentationController: UIPresentationController {
var backgroundView: BackgroundView = BackgroundView()
var containerFrame: CGRect = UIScreen.main.bounds
override var frameOfPresentedViewInContainerView: CGRect {
return CGRect(x: 0, y: 300, width: UIScreen.main.bounds.width, height: UIScreen.main.bounds.height - 300)
}
override init(presentedViewController: UIViewController, presenting presentingViewController: UIViewController?) {
super.init(presentedViewController: presentedViewController, presenting: presentingViewController)
self.backgroundView.backgroundColor = UIColor.yellow
self.backgroundView.alpha = 0.5
self.backgroundView.translatesAutoresizingMaskIntoConstraints = false
}
override func presentationTransitionWillBegin() {
self.containerView?.addSubview(self.backgroundView)
self.containerView?.isUserInteractionEnabled = false // !!!
}
override func containerViewDidLayoutSubviews() {
super.containerViewDidLayoutSubviews()
self.containerView?.frame = self.containerFrame
self.backgroundView.frame = self.containerFrame
self.presentedView?.frame = self.frameOfPresentedViewInContainerView
}
}
What I'm trying to achieve:
Trying to grab the coordinate of the currently touched area on the screen and draw the received coordinate on the screen. Simply put, a basic drawing app that's all written programmatically (for my own practice).
Problem
touchesBegan and touchesMoved for PaintingSubclass are not getting called at all.
Current setup
I have a PaintingViewController then I also have a PaintingSubclass.
PaintingViewController is a UIViewController and PaintingSubclass is a UIView.
PaintingViewController creates an instance of PaintingSubclass and adds it to the subview of PaintingViewController.
PaintingSubclass is where the actual drawing happens.
What I've tried so far
Put a breakpoint inside the touchesMoved and touchesBegan (didn't work)
Tried to add a UITapGestureRecognizer (didn't work)
Enabled self.isUserInteractionEnabled = true (didn't work)
Make PaintingSubclass inherit UIControl and call sendActions(for: .valueChanged) at the end of touchesMoved and touchesBegan (didn't work)
Current Code
(please ignore any the unnecessary variables)
import UIKit
class PaintingViewController: UIViewController{
var _paintView: PaintingSubclass? = nil
override func loadView() {
view = UILabel()
}
private var labelView: UILabel {
return view as! UILabel
}
override func viewDidLoad() {
_paintView = PaintingSubclass()
_paintView?.frame = CGRect(x: view.bounds.minX, y: view.bounds.minY, width: 400, height: 750)
_paintView?.backgroundColor = UIColor.white
_paintView?.setNeedsDisplay()
view.addSubview(_paintView!)
}
class PaintingSubclass: UIView{
private var context: CGContext? = nil
var styleSelection: String = "buttcap"
var linewidth: Float = 5
var lineCapStyle: CGLineCap? = nil
var lineJoinStyle: CGLineJoin? = nil
var lineWidthValue: CGFloat? = nil
var colorValue: UIColor? = nil
override init(frame: CGRect) {
super.init(frame: frame)
lineWidthValue = 0.5
colorValue = UIColor(cgColor: UIColor.black.cgColor)
self.isUserInteractionEnabled = true
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func draw(_ rect: CGRect) {
context = UIGraphicsGetCurrentContext()!
context?.move(to: CGPoint(x: 0.0, y: 110.0))
context?.setStrokeColor((colorValue?.cgColor)!)
context?.drawPath(using: CGPathDrawingMode.stroke)
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
super.touchesBegan(touches, with: event)
let touch: UITouch = touches.first!
let touchPoint: CGPoint = touch.location(in: self)
let _xValue = touchPoint.x
let _yValue = touchPoint.y
NSLog("coordinate: \(_xValue), \(_yValue)")
}
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
super.touchesMoved(touches, with: event)
let touch: UITouch = touches.first!
let touchPoint: CGPoint = touch.location(in: self)
let _xValue = touchPoint.x
let _yValue = touchPoint.y
NSLog("coordinate: \(_xValue), \(_yValue)")
}
}
}
What am I doing wrong here? Been stuck in this state for hours not making any progress. Any help/feedbacks will be much appreciated.
I don't know why you are setting your default view to UILabel(), but your default isUserInteractionEnabled of your view is false. Set it to true
override func loadView() {
view = UILabel()
view.isUserInteractionEnabled = true
}
According to your code, your PaintingViewController's view is a UILabel, the property isUserInteractionEnabled default is false.Try to set this property to true may help you.
The problem lies on:
// override func loadView() {
// view = UILabel()
// }
//
// private var labelView: UILabel {
// return view as! UILabel
// }
You turn the view controller's self view object to a UILabel object.
It would be better to have your painting view as the main view in the view controller and the UILabel as a subview of the painting view. The UILabel needs to have userInteractionEnabled in your current layout, but instead of changing that switch the layout around.
Apart from anything else, presumably you don't want the painting view to draw over the label, but rather the label to be drawn on top, so the label should be the subview.
I have a UIView that am adding to a parent ViewController and it has a Firebase observer which I am removing when UIView is being removed from parent controller. But the first time that I remove the UIView , deinit() won't be called, after that though, if I add it again and remove, deinit() is being called each time. I can't figure out why, any idea?
UIView with the observer:
class TestView: UIView {
var ref = FIRDatabase.database().reference()
var hndlI : FIRDatabaseHandle?
hndlI = ref.child(node).observe(.childAdded , with: { (snap) in
...
}
func rmvObsr() { //this method is called in parent controller before removing the UIView to remove the observer
ref.child(node).removeAllObservers()
}
deinit {
print("de initing ")
}
}
Here's paren UIViewController which adds UIView by tapping on a button:
class ParentController: UIViewController {
weak var tstVw: TestView?
//adding UIView
#IBAction func seeSocialMedia(_ sender: UIButton) {
let view = TestView()
tstVw = view
tstVw?.frame = CGRect(x: 50, y: 60, width: self.view.frame.width-100, height: self.view.frame.height-200)
self.view.addSubview(tstVw!)
}
//removing UIView by tapping elsewhere
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
tstVw?.rmvObsr() //removing observer
tstVw?.removeFromSuperview()
tstVw = nil
}
}
So when you first click then this is called #IBAction func seeSocialMedia(_ sender: UIButton) on the first click that adds self.view.addSubview(tstVw!)
then on second click it removes it(if I am reading the code correctly). That is why it is removed on the second click. I moved
let view = TestView()
tstVw = view
tstVw?.frame = CGRect(x: 50, y: 60, width: self.view.frame.width-100, height: self.view.frame.height-200)
self.view.addSubview(tstVw!)
to the viewDidLoad now when you click the button it will do as you wanted.
I am trying to create a custom UIView/Scrollview named MyScrollView that contains a few labels (UILabel), and these labels receive tap gestures/events in order to respond to user's selections .
In order to make the tap event work on the UILabels, I make sure they all have userIteractionEnabled = true and I created a delegate as below:
protocol MyScrollViewDelegate {
func labelClicked(recognizer: UITapGestureRecognizer)
}
The custom UIView is being used in ScrollViewController that I created, this ScrollViewController implements the delegate method as well:
import UIKit
import Neon
class ScrollViewController: UIViewController, MyScrollViewDelegate {
var curQuestion: IPQuestion?
var type: QuestionViewType?
var lastClickedLabelTag: Int = 0 //
init(type: QuestionViewType, question: IPQuestion) {
super.init(nibName: nil, bundle: nil)
self.curQuestion = question
self.type = type
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func viewDidLoad() {
super.viewDidLoad()
self.automaticallyAdjustsScrollViewInsets = false
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
}
override func loadView() {
view = MyScrollView(delegate: self, q: curQuestion!)
view.userInteractionEnabled = true
}
}
// implementations for MyScrollViewDelegate
extension ScrollViewController {
func labelTitleArray() -> [String]? {
print("labelTitleArray called in implemented delegate")
return ["Comments", "Answers"]
}
func labelClicked(recognizer: UITapGestureRecognizer) {
print("labelClicked called in implemented delegate")
let controller = parentViewController as? ParentViewController
controller?.labelClicked(recognizer)
lastClickedLabelTag = recognizer.view!.tag
}
}
// MARK: - handle parent's ViewController event
extension QuestionDetailViewController {
func updateActiveLabelsColor(index: Int) {
print("updating active labels color: \(index)")
if let view = view as? MyScrollView {
for label in (view.titleScroll.subviews[0].subviews as? [UILabel])! {
if label.tag == index {
label.transform = CGAffineTransformMakeScale(1.1,1.1)
label.textColor = UIColor.purpleColor()
}
else {
label.transform = CGAffineTransformMakeScale(1,1)
label.textColor = UIColor.blackColor()
}
}
}
}
}
This above ScrollViewController is added, as a child view controller to the parent view controller, and positioned to the top part of the parent's view:
override func viewDidLoad() {
super.viewDidLoad()
self.automaticallyAdjustsScrollViewInsets = false
self.view.backgroundColor = UIColor.whiteColor()
addChildViewController(scrollViewController) // added as a child view controller here
view.addSubview(scrollViewController.view) // here .view is MyScrollView
scrollViewController.view.userInteractionEnabled = true
scrollViewController.view.anchorToEdge(.Top, padding: 0, width: view.frame.size.width, height: 100)
}
The app can load everything up in the view, but the tap gesture/events are not passed down to the labels in the custom MyScrollView. For this, I did some google search and have read Event Delivery: Responder Chain on Apple Developer website and did a hit test as well. The hitTest function below can be triggered in the MyScrollView:
override func hitTest(point: CGPoint, withEvent event: UIEvent?) -> UIView? {
print("hit test started, point: \(point), event: \(event)")
return self
}
My observations with the hitTest is that the touchesBegan() and touchesEnded() methods are triggered in the view only when the hitTest function is there. Without hitTest, both functions do not get called with taps.
but no luck getting the UILabel to respond to Tap Gestures. So I am reaching out to experts on SO here. Thanks for helping!
I think I found out the reason why the UILabel did not respond to tapping after much struggle: the .addGestureRecognizer() method to the label was run in the init() method of my custom UIView component, which is wrong, because the view/label may not have been rendered yet. Instead, I moved that code to the lifecycle method layoutSubviews(), and everything started to work well:
var lastLabel: UILabel? = nil
for i in 0..<scrollTitleArr.count {
let label = UILabel()
label.text = scrollTitleArr[i] ?? "nothing"
print("label: \(label.text)")
label.font = UIFont(name: "System", size: 15)
label.textColor = (i == 0) ? MaterialColor.grey.lighten2 : MaterialColor.grey.darken2
label.transform = (i == 0) ? CGAffineTransformMakeScale(1.1, 1.1) : CGAffineTransformMakeScale(0.9, 0.9)
label.sizeToFit()
label.tag = i // for tracking the label by tag number
label.userInteractionEnabled = true
label.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.labelClicked(_:))))
titleContainer.addSubview(label)
if lastLabel == nil {
label.anchorInCorner(.TopLeft, xPad: 0, yPad: 0, width: 85, height: 40)
// label.anchorToEdge(.Left, padding: 2, width: 85, height: 40)
} else {
label.align(.ToTheRightMatchingTop, relativeTo: lastLabel!, padding: labelHorizontalGap, width: 85, height: 40)
}
lastLabel = label
}
In addition, I don't need to implement any of the UIGestureRecognizer delegate methods and I don't need to make the container view or the scroll view userInteractionEnabled. More importantly, when embedding the custom UIView to a superview, I configured its size and set clipsToBounds = true.
I guess I should have read more UIView documentation on the Apple Developer website. Hope this will help someone like me in the future! Thanks to all!
You have to set the property userInteractionEnabled = YES.
For some reason, my simulator was frozen or something when the tap gesture recognizer wasn't working. So, when I restarted the app, then it all worked again. I don't know if this applies here, but that was the fix for me.