Programmatically added UIView to a UIScrollView does not catch UITapGestureRecognizer - ios

I'm developing an app with Swift 4 and targeting iOS 9 and above. For a view I have establish a UIScrollView as in the following post:
https://medium.com/#pradeep_chauhan/how-to-configure-a-uiscrollview-with-auto-layout-in-interface-builder-218dcb4022d7
So my test app looks like this:
Storyboard status corresponds to the final step of the link above
My project is cocoapods based so I added FlexLayout and PinLayout libraries for custom layout my UIView. I added custom UIViews programmatically inside the view container of the scrollview (like cards) and for each UIView when they are instantiated I attach a UITapGestureRecognizer to such instance.
ViewController:
func drawRows() {
flexContainer.flex.direction(.column).define {
(mainFlex) in
for i in 0..<15 {
let newCard = EmptyCard(frame: CGRect(x: 0, y: 0, width: 200, height: 100))
newCard.backgroundColor = .lightGray
let label = UILabel(frame: CGRect(x: 10, y: 10, width: 100, height: 50))
label.text = "Card: \(i)"
newCard.addSubview(label)
mainFlex.addItem(newCard).marginLeft(10).marginBottom(5).width(200).height(100)
}
}
contentView.addSubview(flexContainer)
flexContainer.pin.all()
flexContainer.flex.layout(mode: .adjustHeight)
scrollView.contentSize = flexContainer.frame.size
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
drawRows()
}
Each card has the next class definition:
import UIKit
class EmptyCard: UIView {
override init(frame: CGRect) {
super.init(frame: frame)
addGestureToCard()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
addGestureToCard()
}
func addGestureToCard() {
let gesture = UITapGestureRecognizer(target: self, action: #selector(handleOnTap))
gesture.cancelsTouchesInView = false
self.addGestureRecognizer(gesture)
self.isUserInteractionEnabled = true
}
#objc func handleOnTap(sender : UITapGestureRecognizer) {
print("Hola Mundo")
}
}
The result is the following:
resulting UI
The problem comes when I tap on a UIView that is not showed at first sight because it was added in a coordinate of the UIScrollView that is far from the device dimensions, for example the cards 6 and above the tap gesture recognizer doesn't work
cards that don't have the gesture recognizer active
For the rest of the UIViews (the ones that fit initially within the device dimensions, i.e., Card 0, Card 1, Card 2, Card 3, Card 4 and the top of Card 5) the gesture recognizer works perfectly, i.e., the "Hola Mundo" string message is shown in the debug console. I tried to fix the problem giving fixed dimensions to the view container (the one inside of the scroll view) but that did't work. I hope that somebody has a clue for this scenario, thank's in advance.

Related

iOS: How can I prevent a UITextField from going off screen

I am currently developing an application which has two UILabels inside a Vertical StackView, two UITextFields inside a Vertical StackView, and both of those Vertical StackViews inside one Horizontal StackView as such:
I have constraints put in place. When the application runs on bigger devices such as an iPhone 11, it looks perfect as you can see here:
But if I switch to a smaller device like the iPhone 8 you can see the lines hug the edge of the phone as such:
The way I make the underline for the TextField is by using a class I created called StyledTextField. It looks like this:
class StyledTextField: UITextField {
var bottomLine = CALayer()
override init(frame: CGRect) {
super.init(frame: frame)
}
required init?(coder: NSCoder) {
super.init(coder: coder)
styleTextField()
}
private func styleTextField() {
font = UIFont(name: "Quicksand", size: UIFont.labelFontSize)
// Create the bottom line
bottomLine.frame = CGRect(x: 0, y: frame.height - 2, width: frame.width, height: 2)
bottomLine.backgroundColor = Environment.Colours.primary.cgColor
// Remove border on text field
borderStyle = .none
// Add the line to the text field
layer.addSublayer(bottomLine)
}
func makeUnderlineLight() {
bottomLine.backgroundColor = Environment.Colours.primaryAssisted.cgColor
}
}
In the Storyboard, I assign the UITextField class to be that of the "StyledTextField".
Additionally, in my viewDidLoad function on the Controller that deals with the UITextFields, I call the setUpUI() function which does the following:
func setUpUI() {
title = "Add Task"
taskNameTextField.attributedPlaceholder = NSAttributedString(string: "Name", attributes: [NSAttributedString.Key.foregroundColor: Environment.Colours.lightGray])
moreInfoTextField.attributedPlaceholder = NSAttributedString(string: "(Optional)", attributes: [NSAttributedString.Key.foregroundColor: Environment.Colours.lightGray])
setUpDatePicker()
moreInfoTextField.makeUnderlineLight()
if isEditing() {
showTaskToEdit()
}
view.backgroundColor = Environment.Colours.secondary
}
As you can see, I call the makeUnderlineLight() StyledTextField function once inside there.
Thank you!
Simple two-step solution:
Rewrite styleTextField so that it keeps a reference to the underline layer and removes the underline layer if it already exists, before making a new one.
Move the call to styleTextField() to layoutSubviews. (Don't forget to call super.)
select Your "More Info:" label and set horizontal compress resistanse priority to 1000
it ca be found here:

UIScrollView frame is changed by changing Simulator device

I have simple ViewController which displays images using UIScrollView which has constraints attaching it to the (top, leading, trailing) of the superView, and a UIPageControl, it works fine on iPhoneX simulator
When I run it on iPad Pro 9.7" simulator, the output is
After changing the View as attribute in the storyboard from iPhoneX to iPadPro 9.7" it worked well
This is the logic I use to calculate scrollviewContentSize & slidesSize
override internal func viewDidLoad() {
super.viewDidLoad()
tutorialScrollView.delegate = self
viewModel = TutorialViewModel()
configurePages()
setupSlideScrollView(slides: slides)
configurePageControl()
}
private func configurePages() {
if let viewModel = viewModel {
createSlides(tutotialPages: viewModel.getTutorialPages())
}
}
private func createSlides(tutotialPages: [TutorialPage]) {
for page in tutotialPages {
if let slide = Bundle.main.loadNibNamed(BUNDLE_ID, owner: self, options: nil)?.first as? TutorialSlideView {
slide.configure(title: page.title, detail: page.details, image: page.image)
slides.append(slide)
}
}
}
private func setupSlideScrollView(slides: [TutorialSlideView]) {
tutorialScrollView.contentSize = CGSize(width: view.frame.width * (CGFloat(slides.count)), height: tutorialScrollView.frame.height)
tutorialScrollView.isPagingEnabled = true
for i in 0 ..< slides.count {
slides[i].frame = CGRect(x: view.frame.width * CGFloat(i), y: 0, width: view.frame.width, height: tutorialScrollView.frame.height)
tutorialScrollView.addSubview(slides[i])
}
}
Can anyone find the problem?
Did you try to print view's frame in setupSlideScrollView method to ensure it is correct? There is no guarantee that it will be correct in the viewDidLoad method if you use AutoLayout. Sometimes it will be, sometimes not. I assume in this particular case, it happened to be correct on iPhone X, but incorrect on iPad.
If that's the problem, you should set contentSize and slides' frames in viewDidLayoutSubviews. Adding slides as subviews should stay in viewDidLoad/setupSlideScrollView because viewDidLayoutSubviews usually gets called multiple times.

UIButton inside UIView inside UIScrollView not firing tap action

I've been looking for this for hours and have no luck yet. I want to use two custom views inside a scrollview. The first view have a button as a subview that leads the user to the next page(scrolls down). But the button action it's never fired. If I use the button as a scrollview subview everything works fine, but that's not what I want.
The code for the scrollview view controller:
class ViewController: UIViewController {
#IBOutlet weak var scrollView: UIScrollView!
override func viewDidLoad() {
super.viewDidLoad()
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
let redView = View1()
redView.view.frame = CGRect(x: 0, y: 0, width: self.view.frame.width, height: self.scrollView.frame.height)
self.scrollView.addSubview(redView.view!)
let blueView = UIView(frame: CGRect(x: 0, y: self.scrollView.frame.height, width: self.view.frame.width, height: self.scrollView.frame.height))
blueView.backgroundColor = .blue
self.scrollView.addSubview(blueView)
self.scrollView.contentSize = CGSize(width: self.view.frame.width, height: self.scrollView.frame.height * 2)
}
func go(to page: Int) {
let y = CGFloat(page) * self.scrollView.frame.size.height
self.scrollView.setContentOffset(CGPoint(x: 0, y: y), animated: true)
}
}
ScrollView Storyboard Configuration
The code of the View1 Class:
class View1: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
}
#IBAction func didTouchUpInsideMoreInfoButton(_ sender: Any) {
print("test")
}
}
Any ideas are welcome. Thanks in advance.
Turn out that I was using a view controller's view and for some reason, the selectors don't work this way. So, what I did was to create a UIView only and then everything works just fine.
Check if the buttons userInteraction is enabled.
Sometimes if your button is inside another view, a UIView for example, it doesn't allow you to tap it. To fix this, you might want to add the button ON TOP of that view but not inside it. Make sure it's above but not inside any view in on your storyboard.

Adding a UIView to the bottom of a UICollectionViewController

I have a UICollectionViewController embedded inside a UINavigationController which in turn embedded inside a UITabBarController.
I want to add a UIView to the UICollectionViewController just above the tab bar (shown by red rectangle).
I have the UIView created separately as a nib file.
import UIKit
class BottomView: UIView {
#IBOutlet var view: UIView!
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
commonInit()
}
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
fileprivate func commonInit() {
Bundle.main.loadNibNamed("BottomView", owner: self, options: nil)
view.frame = self.frame
view.autoresizingMask = [.flexibleHeight, .flexibleWidth]
addSubview(view)
}
}
And I initialize and add it in the UICollectionViewController like so.
class CollectionViewController: UICollectionViewController {
fileprivate var bottomView: BottomView!
override func viewDidLoad() {
super.viewDidLoad()
let yPos = view.bounds.height - (tabBarController!.tabBar.frame.size.height + 44)
bottomView = BottomView(frame: CGRect(x: 0, y: yPos, width: view.bounds.width, height: 44))
collectionView?.addSubview(bottomView)
}
// ...
}
I figured if I substract the combined height of the bottom view plus the tab bar from the entire view's height, I should be able to get the correct y position value. But it's not happening. The view is getting added but way off screen.
How do I calculate the correct y position without hardcoding it?
Example demo project
I would suggest adding the BottomView to the UICollectionViewController's view rather than to the collection view itself. This is part of the problem you're having.
You're also trying to set the frame of the BottomView in the viewDidLoad() method using values from view.bounds. The CGRect will return (0.0, 0.0, 0.0, 0.0) at this point because the layout has yet to take place, which is most likely why your positioning is off. Try moving your layout logic to the viewWillLayoutSubviews() method and see if that helps.
A better approach would be by setting auto layout constrains rather than defining a frame manually, this will take a lot of the leg work out for you.
Here's a quick example:
self.bottomView.translatesAutoresizingMaskIntoConstraints = false
self.view.insertSubview(self.bottomView, at: 0)
self.bottomView.bottomAnchor.constraint(equalTo: self.view.layoutMarginsGuide.bottomAnchor).isActive = true
self.bottomView.leadingAnchor.constraint(equalTo: self.view.leadingAnchor).isActive = true
self.bottomView.trailingAnchor.constraint(equalTo: self.view.trailingAnchor).isActive = true
self.bottomView.heightAnchor.constraint(equalToConstant: 100.0).isActive = true
You can apply autolayout logic in your viewDidLoad() and it should work correctly.
You can find some more information on setting autolayout constraints programatically here:
https://developer.apple.com/library/content/documentation/UserExperience/Conceptual/AutolayoutPG/ProgrammaticallyCreatingConstraints.html
Sounds what you want to achieve is exactly the footer view for the UICollectionView.
A footerView is like a view that will stick to the bottom of the collectionView and wont move with the cells.
This will help you add a footer View: https://stackoverflow.com/a/26893334/3165112
Hope that helps!

UIView not registering touch events when adding to sharedApplication's keyWindow

I have a UIView subclass called messageView which I call from the appDelegate like below
let mesgView = messageView(frame: CGRect(x: application.statusBarFrame.minX, y: application.statusBarFrame.minY+50.0, width: application.statusBarFrame.width, height: 75))
UIApplication.sharedApplication().keyWindow?.addSubview(mesgView)
mesgView.window?.windowLevel = UIWindowLevelStatusBar+1
I call it like this because I need to show a view when the user is in the app and they receive a push notification.
in messageView,
override init(frame: CGRect) {
super.init(frame: frame)
self.userInteractionEnabled = true
self.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(messageView.didTap)))
}
func didTap() {
print("tapped")
}
However, when tapping on mesgView it does not register a touch. I have also tried doing this with a transparent UIButton with no luck.
Where is the touch being registered?
Make sure your custom's view frame has correct value set. That was the case for me.
You might have your custom view clipsToBounds = false. That would be the reason you can see your view but taps won't register due to wrong frame

Resources