custom titleView of navigationItem is not getting tapped on iOS 11 - ios

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

Related

UITapGestureRecognizer in custom UIView doesn't work

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).

Button click not working for custom UIView

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.

Add UI Gesture Recognizer to UIView instantiated from .xib

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)
}

Swift iOS programming access a button inside a view programmatically

I'm new to iOS programming. I'm developing a custom keyboard with a custom nib file for it's view. How can I add target for buttons in the nib file without connect them manually to my UIInputViewController?
I tried this code but doesn't works!
class LatinKeyboard: UIInputViewController {
override func viewDidLoad() {
super.viewDidLoad();
let nib = UINib(nibName: "KeyboardView", bundle: nil);
let objects = nib.instantiateWithOwner(self, options: nil)
view = objects[0] as UIView;
for subview in view.subviews {
if(subview is UIButton){
var key = subview as UIButton;
key.addTarget(self, action: "keyPressed:", forControlEvents: .TouchUpInside);
}
}
}
func keyPressed(sender: AnyObject?) {
let button = sender as UIButton
let title = button.titleForState(.Normal)
(textDocumentProxy as UIKeyInput).insertText(title!)
}
}

Get the frame of UIBarButtonItem in Swift?

how could I get the frame of a rightbarbuttonItem in swift? I found this : UIBarButtonItem: How can I find its frame? but it says cannot convert NSString to UIView, or cannot convert NSString to String :
let str : NSString = "view" //or type String
let theView : UIView = self.navigationItem.rightBarButtonItem!.valueForKey("view")//or 'str'
The goal is to remove the rightBarButtonItem, add an imageView instead, and move it with a fadeOut effect.
Thanks
You should try it like:
var barButtonItem = self.navigationItem.rightBarButtonItem!
var buttonItemView = barButtonItem.valueForKey("view")
var buttonItemSize = buttonItemView?.size
Edit (Swift 3):
var barButtonItem = self.navigationItem.rightBarButtonItem!
let buttonItemView = barButtonItem.value(forKey: "view") as? UIView
var buttonItemSize = buttonItemView?.size
In Swift4, XCode9
for view in (self.navigationController?.navigationBar.subviews)! {
if let findClass = NSClassFromString("_UINavigationBarContentView") {
if view.isKind(of: findClass) {
if let barView = self.navigationItem.rightBarButtonItem?.value(forKey: "view") as? UIView {
let barFrame = barView.frame
let rect = barView.convert(barFrame, to: view)
}
}
}
}
import UIKit
class ViewController: UIViewController {
let customButton = UIButton(type: .system)
override func viewDidLoad() {
super.viewDidLoad()
customButton.setImage(UIImage(named: "myImage"), for: .normal)
self.navigationItem.rightBarButtonItem = UIBarButtonItem(customView: customButton)
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
print(self.customButton.convert(self.customButton.frame, to: nil))
}
}
You must use UIBarButtonItem objects created with customView for this; UIBarButtonItem objects created the regular way don't have enough information exposed. It's important that the frame be looked up after the parent UINavigationController's UINavigationBar has completely laid out its entire subview tree. For most use cases, the visible UIViewController's viewDidAppear is a good enough approximation of this.

Resources