I'm working with Xcode 10.0 beta 4 and Swift 4 and I'm trying to add a UIGestureRecognizers specifically a UISwipeGestureRecognizer for both left and right swipes to a UIView that has been added as a subview to my main View Controller via a nib but unfortunately the gesture doesn't seem to be recognized when I actually perform the action in simulator. Here's a snippet of what is in my viewDidLoad():
//Add gesture recognizer for when the calendar is swiped right.
let grSwipeRight = UISwipeGestureRecognizer(target: self, action: #selector(swipeRight(sender:)))
grSwipeRight.direction = UISwipeGestureRecognizer.Direction.right
calendar.calendarDays.isUserInteractionEnabled = true
calendar.calendarDays.addGestureRecognizer(grSwipeRight)
The swipeRight action in the selector is to a function that just contains a print() call and calendar is a reference to my UIView which is the class used in my .xib
class Calendar: UIView
{
#IBOutlet var calendarDays: UICollectionView!
let nibName = "Calendar"
var contentView: UIView?
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
guard let view = self.loadViewFromNib() else { return }
view.frame = self.bounds
self.addSubview(view)
self.bringSubviewToFront(view)
contentView = view
contentView?.isUserInteractionEnabled = true
}
private 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
}
I setup the .xib as a UIView using Interface Builder and set the class of the view to my Calendar class and the labels and such within my .xib show up properly its just when I add any Gesture Recognizer to either the calendar(View) or objects within my calendar the gesture isn't recognized. Is this even the proper way to do something like this or should I be taking a completely different approach?
Edit:
I've included what my IB .xib and .storyboard look like if its helpful at all. Storyboard, xib
To be able to use gesture recognizers within my .xib, I used an override of awakeFromNib() as follows within my custom UIView Calendar class:
override func awakeFromNib()
{
super.awakeFromNib()
let touchTest = UITapGestureRecognizer(target: self, action: #selector(self.testTap(sender:)))
testingLabel.isUserInteractionEnabled = true
testingLabel.addGestureRecognizer(touchTest)
}
Related
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:
There's no need for a 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.
Attaching the 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.
The parent view's background color is used (the stackView's is ignored) but it's possible to simply set it to a transparent color.
Attaching the gesture recognizer to 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).
I have a Controller with these items
In stack view I add one or many xib file as UiView dynamically, this is my xib file Code:
class PassengerInfoItem: UIView {
#IBOutlet weak var View_Content: UIView!
#IBOutlet weak var Label_AddPassenger: UILabel!
override init(frame: CGRect)
{
super.init(frame: frame)
self.commenInit()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
self.commenInit()
}
func commenInit()
{
Bundle.main.loadNibNamed("PassengerInfoItem", owner: self, options: nil)
self.View_Content.frame = self.bounds
self.View_Content.autoresizingMask = [.flexibleHeight, .flexibleWidth]
addSubview(self.View_Content)
}}
and this is my controller code:
for i in 1...(UserSearchModel.Passengers.AdultNumber)!
{
let passengerInfoItem = PassengerInfoItem()
passengerInfoItem.tag = i
let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(self.handleTapGesture_PassengerInfo))
tapGestureRecognizer.delegate = self
passengerInfoItem.addGestureRecognizer(tapGestureRecognizer)
self.StackView_AddPassengerInfo.addArrangedSubview(passengerInfoItem)
}
In stack view all passengerInfoItem added successfully but when touch one of those nothing happen and my problem is UITapGestureRecognizer do not work correctly , I checked all isUserInteractionEnabled = true for back View, Scroll View, Card, Stack View but UITapGesture not work
Because you add another instance here
Bundle.main.loadNibNamed("PassengerInfoItem", owner: self, options: nil)
self.View_Content.frame = self.bounds
self.View_Content.autoresizingMask = [.flexibleHeight, .flexibleWidth]
addSubview(self.View_Content)
so the gesture is applied to the back-most view , you may need
class func getInstance() -> PassengerInfoItem {
let vv = Bundle.main.loadNibNamed("PassengerInfoItem", owner: self, options: nil)?.first! as! PassengerInfoItem
return vv
}
Then replace
let passengerInfoItem = PassengerInfoItem()
with
let passengerInfoItem = PassengerInfoItem.getInstance()
Code for the Custom UIView:
Please check the video too here: https://drive.google.com/open?id=1kbrOxXWcJIi4vkiqMNer3exBr5cOWgDz
import UIKit
protocol PostAttachmentFullScreenViewDelegate: class {
func closeAttachmentFullView()
}
class PostAttachmentFullScreenView: UIView {
weak var delegate: PostAttachmentFullScreenViewDelegate?
#IBOutlet var backgroundView: UIImageView!
#IBOutlet var closeButton: UIButton!
#IBAction func closeViewAction(_ sender: Any) {
print("will call delegate to put it off")
self.delegate?.closeAttachmentFullView()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
let _ = commonInitialization()
backgroundView.image = UIImage(named: "ScrollImageTop1")
closeButton.isUserInteractionEnabled = true
}
override init(frame: CGRect) {
super.init(frame: frame)
let _ = commonInitialization()
backgroundView.image = UIImage(named: "ScrollImageTop1")
closeButton.isUserInteractionEnabled = true
}
func commonInitialization() -> UIView
{
let bundle = Bundle.init(for: type(of: self))
let nib = UINib(nibName: "PostAttachmentFullScreenView", bundle: bundle)
let view = nib.instantiate(withOwner: self, options: nil)[0] as! UIView
view.frame = bounds
view.autoresizingMask = [UIViewAutoresizing.flexibleWidth, UIViewAutoresizing.flexibleHeight]
addSubview(view)
return view
}
}
usage in ViewController (I am defining an instance of the custom view and putting it inside the Scroll View):
var frame = CGRect(x:0, y:0, width:0, height:0)
let blue = PostAttachmentFullScreenView()
blue.delegate = self
blue.isUserInteractionEnabled = true
blue.backgroundColor = UIColor.blue
blue.backgroundView.image = fileAttachments[1]
frame.origin.x = attachmentsScrollView.frame.size.width * CGFloat (0)
frame.size = attachmentsScrollView.frame.size
blue.frame = frame
attachmentsScrollView.addSubview(blue)
extension NewPostViewController : PostAttachmentFullScreenViewDelegate
{
func closeAttachmentFullView() {
print("hiding attachments view")
attachmentSuperView.isHidden = true
}
}
To my surprise it doesn't even print - "will call delegate to put it off".
I am not able to understand what's wrong here. Please help me understand the issue and correct it. Thank you.
You are mixing programmatic approach and xib approach.
As you have added IBOultet and IBAction that means you are using xib for the UIView.
In that scenario you have to load the UIView xib when initialising the view.
Add an extension for UIView in your project:
extension UIView {
class func fromNib<T: UIView>() -> T {
return Bundle.main.loadNibNamed(String(describing: T.self), owner: nil, options: nil)![0] as! T
}
}
when you are initialising your view add it like this :
let blue : PostAttachmentFullScreenView = UIView.fromNib()
blue.delegate = self
blue.isUserInteractionEnabled = true
blue.backgroundColor = UIColor.blue
blue.backgroundView.image = fileAttachments[1]
frame.origin.x = attachmentsScrollView.frame.size.width * CGFloat (0)
frame.size = attachmentsScrollView.frame.size
blue.frame = frame
attachmentsScrollView.addSubview(blue)
and the delegate and button action methods will work.
you missed this :
You never set the target/action on your button. Somewhere you need to call addTarget(_:action:for:) to set the target/action on the button. Also, what connects the button to your PostAttachmentFullScreenView as an outlet?
This might be an obvious one but for me (Xcode 10.1) adding all missing UI constraints to the UIButton in question (at least 4 constraints) fixed the error for me in my custom view:
Make sure you add enough constraints (typically 4 constraints) or enough to have all warnings regarding missing constraints removed. After doing this and attaching the button with Ctrl + drag from View to corresponding swift code, the click was being detected and working properly.
Hope this helps.
I am using a custom titleView and assigning it to navigationItem titleView. It had been working fine until iOS 11. Since the update it's position got misplaced to center as originally it was on more left. Beside that user interaction is not working.
titleView = Bundle.main.loadNibNamed("SomeNib", owner: self, options: nil)?.first as? SomeNib
navigationItem.titleView = titleView
titleView is just a usual nib.
then for enabling interaction:
if let titleView = self.navigationItem.titleView {
let tap = UITapGestureRecognizer(target: self, action: #selector(onTitleViewTap))
titleView.addGestureRecognizer(tap)
titleView.isUserInteractionEnabled = true
}
In iOS 11, titleView is getting set with Autolayout. Hence, the size of the titleView is the intrinsic size of the view you are setting in titleView.
This code in your view class(which you are setting as titleView) should help:
override var intrinsicContentSize: CGSize {
return UILayoutFittingExpandedSize
}
I have a created a Custom UIView with xib. Inside the UIView, I added UILabel, ImageView and a UIButton on top. On click on Hidden button I am able to call the click event.
import UIKit
class NavBarTitleView: UIView {
#IBOutlet weak var title : UILabel!
#IBOutlet weak var clickButton: UIButton!
/// Create an instance of the class from its .xib
class func instanceFromNib() -> NavBarTitleView {
return UINib(nibName: "NavBarTitleView", bundle: nil).instantiate(withOwner: nil, options: nil)[0] as! NavBarTitleView
}
//this line of code will help to enable a click event.
override var intrinsicContentSize: CGSize {
return UIView.layoutFittingExpandedSize
}
}
Then in UIViewcontroler, I added the below code.
if let title = self.navBarTitle{
if navbarTitleView == nil {
navbarTitleView = NavBarTitleView.instanceFromNib()
self.navigationItem.titleView = navbarTitleView
}
navbarTitleView!.title.text = title
navbarTitleView!.clickButton.addTarget(self, action: #selector(didTapNavbarTitle(_:)), for: .touchUpInside)
navbarTitleView!.layoutIfNeeded()
}else{
navigationItem.title = ""
}
Button action:
#objc func didTapNavbarTitle(_ sender: Any){
print("NavBar Selected")
}
Result -
I hope this will work. Tested on code 11.6 and os version 13.6
I have a default UIButton linked to an IBAction (touch up inside). This works fine, so I know my connection is OK. Once I change the UIButton class to my custom button, the IBAction is no longer triggered. Below is the code for my custom UIButton. Any ideas why this is happening?
import UIKit
#IBDesignable
class PrimaryButton: UIButton {
override init(frame: CGRect) {
super.init(frame: frame)
initView()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
initView()
}
private func initView() {
let view = viewFromNibForClass()
view.frame = bounds
view.autoresizingMask = [
UIViewAutoresizing.flexibleWidth,
UIViewAutoresizing.flexibleHeight
]
addSubview(view)
}
private func viewFromNibForClass() -> UIView {
let bundle = Bundle(for: type(of: self))
let nib = UINib(nibName: String(describing: type(of: self)), bundle: bundle)
let view = nib.instantiate(withOwner: self, options: nil).first as! UIView
return view
}
}
Your code does not work because a button does not receive touches anymore. All touches are sent to the topmost custom view you add.
Set view.userInteractionEnabled = false to make it transparent to touches.
Your custom button is covered by the view you added.