Swift 5/Xcode 12.4
I created an xib file for my custom MarkerView
- the layout's pretty simple:
- View
-- StackView
--- DotLabel
--- NameLabel
View
and StackView
are both set to "User Interaction Enabled" in the inspector by default. DotLabel
and NameLabel
aren't but ticking their boxes doesn't seem to actually change anything.
At runtime I create MarkerViews (for testing purposes only one atm) in my ViewController
and add them to a ScrollView
that already contains an image (this works):
override func viewDidAppear(_ animated: Bool) {
createMarkers()
setUpMarkers()
}
private func createMarkers() {
let marker = MarkerView()
marker.setUp("Some Text")
markers.append(marker)
scrollView.addSubview(marker)
marker.alpha = 0.0
}
private func setUpMarkers() {
for (i,m) in markers.enumerated() {
m.frame.origin = CGPoint(x: (i+1)*100,y: (i+1)*100)
m.alpha = 1.0
}
}
These MarkerViews should be clickable, so I added a UITapGestureRecognizer
but the linked function is never called. This is my full MarkerView:
class MarkerView: UIView {
@IBOutlet weak var stackView: UIStackView!
@IBOutlet weak var dotLabel: UILabel!
@IBOutlet weak var nameLabel: UILabel!
let nibName = "MarkerView"
var contentView:UIView?
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
func commonInit() {
guard let view = loadViewFromNib() else { return }
view.frame = self.bounds
self.addSubview(view)
contentView = view
let gesture = UITapGestureRecognizer(target: self, action: #selector(self.clickedMarker))
self.addGestureRecognizer(gesture)
}
func loadViewFromNib() -> UIView? {
let bundle = Bundle(for: type(of: self))
let nib = UINib(nibName: nibName, bundle: bundle)
return nib.instantiate(withOwner: self, options: nil).first as? UIView
}
func setUp(_ name:String) {
nameLabel.text = name
nameLabel.sizeToFit()
stackView.sizeToFit()
}
@objc private func clickedMarker() {
print("Clicked Marker!")
}
}
Other questions recommend adding self.isUserInteractionEnabled = true
before the gesture recognizer is added. I even enabled it for the StackView and both labels and also tried to add the gesture recognizer to the ScrollView but none of it helped.
Why is the gesture recognizer not working and how do I fix the problem?
Edit: With Sweeper's suggestion my code now looks like this:
class MarkerView: UIView, UIGestureRecognizerDelegate {
.....
func commonInit() {
guard let view = loadViewFromNib() else { return }
view.frame = self.bounds
let gesture = UITapGestureRecognizer(target: self, action: #selector(self.clickedMarker))
gesture.delegate = self
self.isUserInteractionEnabled = true
self.addGestureRecognizer(gesture)
self.addSubview(view)
contentView = view
}
.....
func gestureRecognizer(_: UIGestureRecognizer, _ otherGestureRecognizer: UIGestureRecognizer) -> Bool {
print("gestureRecognizer")
return true
}
}
"gestureRecognizer" is never printed to console.
One of the comments below the accepted answer for the question @Sweeper linked gives a good hint:
What is the size of the content view (log it)?
print(self.frame.size)
print(contentView.frame.size)
both printed (0.0, 0.0)
in my app. The UITapGestureRecognizer
is attached to self
, so even if it worked, there was simply no area that you could tap in for it to recognize the tap.
Solution: Set the size of the UIView
the UITapGestureRecognizer
is attached to:
stackView.layoutIfNeeded()
stackView.sizeToFit()
self.layoutIfNeeded()
self.sizeToFit()
didn't work for me, the size was still (0.0, 0.0)
afterwards. Instead I set the size of self
directly:
self.frame.size = stackView.frame.size
This also sets the size of the contentView
(because it's the MarkerView
's child) but of course requires the size of stackView
to be set properly too. You could add up the childrens' widths/heights (including spacing/margins) yourself or simply call:
stackView.layoutIfNeeded()
stackView.sizeToFit()
The finished code:
func setUp(_ name:String) {
nameLabel.text = name
//nameLabel.sizeToFit() //Not needed anymore
stackView.layoutIfNeeded() //Makes sure that the labels already use the proper size after updating the text
stackView.sizeToFit()
self.frame.size = stackView.frame.size
let gesture = UITapGestureRecognizer(target: self, action: #selector(self.onClickStackView))
self.addGestureRecognizer(gesture)
}
Specifics:
delegate
and overriding func gestureRecognizer(_: UIGestureRecognizer, _ otherGestureRecognizer: UIGestureRecognizer) -> Bool
to always return true
, which might be an alternative but I didn't manage to get that version to work - the function was never called. If someone knows how to do it, please feel free to post it as an alternative solution.UITapGestureRecognizer
has to be done once the parent view knows its size, which it doesn't in commonInit
because the text of the labels and the size of everything isn't set there yet. It's possible to add more text after the recognizer is already attached but everything that exceeds the original length (at the time the recognizer was attached) won't be clickable if using the above code.stackView
's is ignored) but it's possible to simply set it to a transparent color.stackView
instead of self
works the same way. You can even use a UILabel
but their "User Interaction Enabled" is off by default - enable it first, either in the "Attributes Inspector" (tick box in the "View" section) or in code (myLabel.isUserInteractionEnabled = true
).