Swift: button added within UIView not clickable - swift

I have the following container view:
class NotificationsContainer: UIView {
init() {
super.init(frame: .zero)
controller.view.translatesAutoresizingMaskIntoConstraints = false
addSubview(controller.view)
controller.view.isHidden = true
self.isUserInteractionEnabled = true
self.clipsToBounds = false
configureAutoLayout()
}
var showNotifications = false {
didSet {
if showNotifications == true {
controller.view.isHidden = false
} else {
controller.view.isHidden = true
}
}
}
internal lazy var notificationBanner: AlertView = {
let banner = AlertView()
banner.attrString = UploadNotificationManager.shared.notificationBannerText()
banner.alertType = .notification
banner.translatesAutoresizingMaskIntoConstraints = false
addSubview(banner)
banner.isUserInteractionEnabled = true
banner.showMeButton.addTarget(self, action: #selector(showHideNotifications), for: .touchDown)
return banner
}()
#objc func showHideNotifications() {
showNotifications = showNotifications == false ? true : false
}
private lazy var notificationView: NotificationContentView = {
let notificationView = NotificationContentView()
return notificationView
}()
private lazy var controller: UIHostingController = {
return UIHostingController(rootView: notificationView)
}()
private func configureAutoLayout() {
NSLayoutConstraint.activate([
notificationBanner.leadingAnchor.constraint(equalTo: leadingAnchor),
notificationBanner.trailingAnchor.constraint(equalTo: trailingAnchor),
controller.view.trailingAnchor.constraint(equalTo: notificationBanner.trailingAnchor),
controller.view.topAnchor.constraint(equalTo: notificationBanner.bottomAnchor)
])
}
}
AlertView contains a button as follows:
internal lazy var showMeButton: UIButton = {
let button = UIButton()
button.setTitle("Show me...", for: .normal)
button.setTitleColor(UIColor.i6.blue, for: .normal)
button.titleLabel?.font = .systemFont(ofSize: Constants.fontSize)
addSubview(button)
button.translatesAutoresizingMaskIntoConstraints = false
return button
}()
Then I add the container view to my main view:
private lazy var notifications: NotificationsContainer = {
let notifications = NotificationsContainer()
notifications.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(notifications)
notifications.leadingAnchor.constraint(equalTo: flightNumber.leadingAnchor).isActive = true
notifications.trailingAnchor.constraint(equalTo: flightNumber.trailingAnchor).isActive = true
return notifications
}()
override public func viewDidLoad() {
super.viewDidLoad()
stackView.insert(arrangedSubview: notifications, atIndex: 0)
}
Now as you can see I am trying to add an action to the showMeButton. However, when I click on the button, it does nothing. I have read before that this could be to do with the frame of the container view. However, I have tried setting the height of the notification view in my main view (width should already be there due to leading and trailing constraints) and I have tried setting the height of notificationBanner as well but nothing is working.
Here is the view in the view debugger:
The showMe button does not appear to be obscured and all other views appear to have dimensions...

Look at the debug view hierarchy in Xcode and see if the view containing the button is actually showing up. You haven't set enough constraints on any of these views so the height and width look like they could be ambiguous to me. Once you're inside the view debugger, another common problem is that another invisible view is covering up the one with the button and intercepting the touch gestures.

Related

How to hide and unhide a view with height in swift?

I am trying to hide and unhide a view. Its size should be 0 when it is hidden and about 200 when it is unhidden. I have two view controllers. When the first controller shows the view is hidden for the first time and its size is set to 0 and then it navigates to other controller and takes some values from the textfeilds and display them on a tableview in previous controller.
Now, I am able to hide the view for the first time with height 0 but when I take up the values the view is still hidden.
This is the code I have tried so far:
mainView.isHidden == true
mainView.heightAnchor.constraint(equalToConstant: CGFloat(0)).isActive = true
// when I get the values but this code doesn't work
mainView.isHidden == false
mainView.heightAnchor.constraint(equalToConstant: CGFloat(100)).isActive = true
Any help would be appreciated.
class viewController: UIViewController {
var height: NSLayoutConstraint!
override func viewDidLoad() {
super.viewDidLoad()
height = mainView.heightAnch.constraint(equalToConstant: 0)
height.isActive = true
//handle change height
if mainView.isHidden == true {
height.constant = 0
}
else {
height.constant = 200
}
}
}
You have two options.
The first method set identifier to height constraint
set identifier:
then find and change with below code:
// first option
// find own constraint with indentifier
if let heightConstraint = self.myView.constraints.first(where: { item -> Bool in
return item.identifier == "heightIdentifier"
}) {
// set any constant to constraint
heightConstraint.constant = 200.0 // for hidden
heightConstraint.constant = 0.0 // for hide
// any work
}
second option: set IBOutlet to target constraint:
#IBOutlet weak var heightConstraint: NSLayoutConstraint!
then change direct and simple:
// second option
// change constant direct
self.heightConstraint.constant = 200.0 // for hidden
self.heightConstraint.constant = 0.0 // for hide
// any work
You keep both created constraints in your view controller and activate however you need accordingly, using isActive property of NSLayoutConstraint:
var hiddenHeightConstraint: NSLayoutConstraint?
var showingHeightConstraint: NSLayoutConstraint?
var isMainViewHidden: Bool = false {
didSet {
mainView.isHidden == isMainViewHidden
hiddenHeightConstraint?.isActive = isMainViewHidden
showingHeightConstraint?.isActive = !isMainViewHidden
// Don't forget to call layoutIfNeeded() when you messing with the constraints
view.layoutIfNeeded()
}
}
override func viewDidLoad() {
super.viewDidLoad()
hiddenHeightConstraint = mainView.heightAnchor.constraint(equalToConstant: CGFloat(0))
showingHeightConstraint = mainView.heightAnchor.constraint(equalToConstant: CGFloat(100))
isMainViewHidden = false
}

Why doesn't my UILabel in a nested view receive touch events / How can I test the Responder Chain?

I have found lots of similar questions about not receiving touch events and I understand that in some cases, writing a custom hitTest function may be required - but I also read that the responder chain will traverse views and viewControllers that are in the hierarchy - and I don't understand why a custom hitTest would be required for my implementation.
I'm looking for an explanation and/or a link to a document that explains how to test the responder chain. This problem is occurring in Xcode 10.2.1.
My scenario (I am not using Storyboard):
I have a mainViewController, that provides a full screen view with an ImageView and a few Labels. I have attached TapGestureRecognizers to the ImageView and one of the labels - and they both work properly.
When I tap the label, I add a child viewController and it's view as a subview to the mainViewController. The view is constrained to cover only the right-half of the screen.
The child viewController contains a vertical stack view that contains 3 arrangedSubviews.
Each arrangedSubview contains a Label and a horizontal StackView.
The horizontal stackView's each contain a View with a Label as a subview.
The Label in the subview sets it's isUserInteractionEnabled flag to True and adds a TapGestureRecognizer.
These are the only objects in the child ViewController that have 'isUserInteractionEnabled' set.
The Label's are nested fairly deep, but since this is otherwise a direct parent/child hierarchy (as opposed to the 2 views belonging to a NavigationController), I would expect the Label's to be in the normal responder chain and function properly. Do the Stack View's change that behavior? Do I need to explicitly set the 'isUserInteractionEnabled' value to False on some of the views? Is there way I can add logging to the ResponderChain so I can see which views it checked and find out where it is being blocked?
After reading this StackOverflow post I tried adding my gesture recognizers in viewDidLayoutSubviews() instead of what's shown below - but they still do not receive tap events.
Thank you in advance to any who can offer advice or help.
Here is the code for the label that is not responding to my tap events and the tap event it should call:
func makeColorItem(colorName:String, bgColor:UIColor, fgColor:UIColor) -> UIView {
let colorNumber:Int = colorLabelDict.count
let colorView:UIView = {
let v = UIView()
v.tag = 700 + colorNumber
v.backgroundColor = .clear
v.contentMode = .center
return v
}()
self.view.addSubview(colorView)
let tapColorGR:UITapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(tapColor))
let colorChoice: UILabel = {
let l = UILabel()
l.tag = 700 + colorNumber
l.isUserInteractionEnabled = true
l.addGestureRecognizer(tapColorGR)
l.text = colorName
l.textAlignment = .center
l.textColor = fgColor
l.backgroundColor = bgColor
l.font = UIFont.systemFont(ofSize: 24, weight: .bold)
l.layer.borderColor = fgColor.cgColor
l.layer.borderWidth = 1
l.layer.cornerRadius = 20
l.layer.masksToBounds = true
l.adjustsFontSizeToFitWidth = true
l.translatesAutoresizingMaskIntoConstraints = false
l.widthAnchor.constraint(equalToConstant: 100)
return l
}()
colorView.addSubview(colorChoice)
colorChoice.centerXAnchor.constraint(equalTo: colorView.centerXAnchor).isActive = true
colorChoice.centerYAnchor.constraint(equalTo: colorView.centerYAnchor).isActive = true
colorChoice.heightAnchor.constraint(equalToConstant: 50).isActive = true
colorChoice.widthAnchor.constraint(equalToConstant: 100).isActive = true
colorLabelDict[colorNumber] = colorChoice
return colorView
}
#objc func tapColor(sender:UITapGestureRecognizer) {
print("A Color was tapped...with tag:\(sender.view?.tag ?? -1)")
if let cn = sender.view?.tag {
colorNumber = cn
let v = colorLabelDict[cn]
if let l = (v?.subviews.first as? UILabel) {
print("The \(l.text) label was tapped.")
}
}
}
It looks like the main reason you're not getting a tap recognized is because you are adding a UILabel as a subview of a UIView, but you're not giving that UIView any constraints. So the view ends up with a width and height of Zero, and the label exists outside the bounds of the view.
Without seeing all of your code, it doesn't look like you need the extra view holding the label.
Take a look at this... it will add a vertical stack view to the main view - centered X and Y - and add "colorChoice" labels to the stack view:
class TestViewController: UIViewController {
let stack: UIStackView = {
let v = UIStackView()
v.axis = .vertical
v.spacing = 4
return v
}()
var colorLabelDict: [Int: UIView] = [:]
override func viewDidLoad() {
super.viewDidLoad()
let v1 = makeColorLabel(colorName: "red", bgColor: .red, fgColor: .white)
let v2 = makeColorLabel(colorName: "green", bgColor: .green, fgColor: .black)
let v3 = makeColorLabel(colorName: "blue", bgColor: .blue, fgColor: .white)
[v1, v2, v3].forEach {
stack.addArrangedSubview($0)
}
stack.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(stack)
NSLayoutConstraint.activate([
stack.centerXAnchor.constraint(equalTo: view.centerXAnchor),
stack.centerYAnchor.constraint(equalTo: view.centerYAnchor),
])
}
func makeColorLabel(colorName:String, bgColor:UIColor, fgColor:UIColor) -> UILabel {
let colorNumber:Int = colorLabelDict.count
// create tap gesture recognizer
let tapColorGR:UITapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(tapColor))
let colorChoice: UILabel = {
let l = UILabel()
l.tag = 700 + colorNumber
l.addGestureRecognizer(tapColorGR)
l.text = colorName
l.textAlignment = .center
l.textColor = fgColor
l.backgroundColor = bgColor
l.font = UIFont.systemFont(ofSize: 24, weight: .bold)
l.layer.borderColor = fgColor.cgColor
l.layer.borderWidth = 1
l.layer.cornerRadius = 20
l.layer.masksToBounds = true
l.adjustsFontSizeToFitWidth = true
l.translatesAutoresizingMaskIntoConstraints = false
// default .isUserInteractionEnabled for UILabel is false, so enable it
l.isUserInteractionEnabled = true
return l
}()
NSLayoutConstraint.activate([
// label height: 50, width: 100
colorChoice.heightAnchor.constraint(equalToConstant: 50),
colorChoice.widthAnchor.constraint(equalToConstant: 100),
])
// assign reference to this label in colorLabelDict dictionary
colorLabelDict[colorNumber] = colorChoice
// return newly created label
return colorChoice
}
#objc func tapColor(sender:UITapGestureRecognizer) {
print("A Color was tapped...with tag:\(sender.view?.tag ?? -1)")
// unwrap the view that was tapped, make sure it's a UILabel
guard let tappedView = sender.view as? UILabel else {
return
}
let cn = tappedView.tag
let colorNumber = cn
print("The \(tappedView.text ?? "No text") label was tapped.")
}
}
Result of running that:
Those are 3 UILabels, and tapping each will trigger the tapColor() func, printing this to the debug console:
A Color was tapped...with tag:700
The red label was tapped.
A Color was tapped...with tag:701
The green label was tapped.
A Color was tapped...with tag:702
The blue label was tapped.

removeFromSubView disables interaction

I have a UICollectionView that is basically a chat log. I have an imageView in some of the cells and added the ability to expand an image to full screen on tap.
///
ChatLogMessageCell.swift
/**
*
* I add the target to the UIButton with an image as a background
*/
messageImage.addTarget(self, action: #selector(fullscreenImage), for: .touchUpInside)
/*
* Full screen code
*/
#objc func fullscreenImage() {
if let chatlog = parentViewController as? ChatLogController {
let imageScroll = UIScrollView()
imageScroll.delegate = self
imageScroll.minimumZoomScale = 1.0
imageScroll.maximumZoomScale = 5.0
imageScroll.frame = UIScreen.main.bounds
let newImageView = UIImageView(image: messageImage.backgroundImage(for: .normal))
newImageView.frame = UIScreen.main.bounds
newImageView.backgroundColor = .black
newImageView.contentMode = .scaleAspectFit
newImageView.isUserInteractionEnabled = true
imageScroll.addSubview(newImageView)
chatlog.view.addSubview(imageScroll)
chatlog.navigationController?.isNavigationBarHidden = true
chatlog.tabBarController?.tabBar.isHidden = true
let tap = UITapGestureRecognizer(target: self, action: #selector(dismissFullscreenImage))
newImageView.addGestureRecognizer(tap)
}
}
#objc func dismissFullscreenImage(_ sender: UITapGestureRecognizer) {
if let chatlog = parentViewController as? ChatLogController {
chatlog.navigationController?.isNavigationBarHidden = false
chatlog.tabBarController?.tabBar.isHidden = false
sender.view?.removeFromSuperview()
}
}
When The fullscreen image is removed the ChatLogController is no longer interactable. I can't scroll or re-enter fullscreen mode on an image.What am I missing here? I simply want to dismiss the full screen image and allow the user to choose another image or just scroll through the messages.
Here you remove the imageView
sender.view?.removeFromSuperview()
while you need to remove the scrollView like
sender.view?.superview?.removeFromSuperview()

Set a bool value using model object didSet

I'm struggling to figure out how to properly set bool values using a model object's didSet. My app has a series of swipable cards where some flip and some don't. This code below is the CardView which is run for each card created.
Currently, the code works perfectly for the image and label—each card loads unique information based each card's model object. However, the button and isFlippable property are where I'm struggling.
The code right now is always loading the green pathway. The weird thing, however, is that even when the cardModel should sets the button isEnabled to false, it will still load the green (but the button won't work, so it did become disabled...)
var cardModel: CardModel! {
didSet {
imageView.image = cardModel.image
label.text = cardModel.label
flipButton.isEnabled = cardModel.isFlippable
isBackShowing = cardModel.isFlippable //Intentionally use isFlippable here because I want the initial layout to be based on this true or false value.
}
}
let imageView = UIImageView()
let label = UILabel()
let flipButton = UIButton()
var isBackShowing = false
override init(frame: CGRect) {
super.init(frame: frame)
setupLayout()
}
fileprivate func setupLayout() {
if flipButton.isEnabled == true {
if isBackShowing == true {
backgroundColor = .red
} else {
backgroundColor = .green
}
} else {
backgroundColor = .yellow
}
}
I also have code for when the button flips that alternates "isBackShowing" and then calls setupLayout()—it is working fine. But it always loads as false during the initial setup of the card.
For better readability you can little bit update your code replacing var isBackShowing = Bool() by var isBackShowing = false.
And also you can call setupLayout() to update your layout after setting of cardModel. For example didSet of cardModel can looks like this:
var cardModel: CardModel! {
didSet {
imageView.image = cardModel.image
label.text = cardModel.label
flipButton.isEnabled = cardModel.isFlippable
isBackShowing = cardModel.isFlippable
setupLayout()
}
}

How to create block screen with circle loader

I am doing an app that does background job that can take some time
I want to show a loader in that time
I want a black screen with a simple loader in the front of it
and show it \ hide it,
when I do actions in the background
I want to do a simple half black square with loader circle
that also blocks presses to the screen
Like in this picture:
How can I achieve that and that ?
First create one UIView which you will put in front of your LogIn view. Then add UIActivityIndicatorView to the created UIView.
let loadingIndicatorView = UIView()
let activityIndicatorView = UIActivityIndicatorView(activityIndicatorStyle: .gray)
Now the loadingIndicatorView should have same frame size as your LogIN view. For color you can set your own color with alpha as you want to show LogIn content too. Initially keep it hidden and whenever you want to show it unhide it.
loadingIndicatorView.frame = view.frame
loadingIndicatorView.backgroundColor = .gray
loadingIndicatorView.isHidden = true
Now setup activityIndicatorView, it should be shown at centre,
activityIndicatorView.center = CGPoint(
x: UIScreen.main.bounds.size.width / 2,
y: UIScreen.main.bounds.size.height / 2
)
You can set some color to the indicator,
activityIndicatorView.color = .white
activityIndicatorView.hidesWhenStopped = true
Now add this activityIndicatorView to loadingIndicatorView and loadingIndicatorView to LogIn View.
loadingIndicatorView.addSubview(activityIndicatorView)
view.addSubview(loadingIndicatorView)
Lastly for showing do,
loadingIndicator.startAnimating()
loadingIndicatorView.isHidden = false
And for hiding,
loadingIndicator.stopAnimating()
loadingIndicatorView.isHidden = true
Updated Answer
Since the OP wanted an example code. Hence the updated answer. Hope everyone gets to learn something or the other out of it.
To start with, I created a subclass of UIView and named it PSOverlaySpinner and it looks something like below:
import UIKit
class PSOverlaySpinner: UIView {
//MARK: - Variables
private var isSpinning: Bool = false
private lazy var spinner : UIActivityIndicatorView = {
var spinner = UIActivityIndicatorView(style: UIActivityIndicatorView.Style.white)
spinner.translatesAutoresizingMaskIntoConstraints = false
spinner.hidesWhenStopped = true
return spinner
}()
// MARK: - View Lifecycle Functions
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
init() {
super.init(frame: CGRect.zero)
self.translatesAutoresizingMaskIntoConstraints = false
self.backgroundColor = UIColor.init(white: 0.0, alpha: 0.8)
self.isSpinning = false
self.isHidden = true
createSubviews()
}
deinit {
self.removeFromSuperview()
}
func createSubviews() -> Void {
self.addSubview(spinner)
setupAutoLayout()
}
// MARK: - Private Methods
private func setupAutoLayout() {
if #available(iOS 11.0, *) {
spinner.safeAreaLayoutGuide.centerXAnchor.constraint(equalTo: safeAreaLayoutGuide.centerXAnchor).isActive = true
spinner.safeAreaLayoutGuide.centerYAnchor.constraint(equalTo: safeAreaLayoutGuide.centerYAnchor).isActive = true
} else {
// Fallback on earlier versions
spinner.centerXAnchor.constraint(equalTo: centerXAnchor).isActive = true
spinner.centerYAnchor.constraint(equalTo: centerYAnchor).isActive = true
}
}
// MARK: - Public Methods
public func show() -> Void {
DispatchQueue.main.async {
if !self.spinner.isAnimating {
self.spinner.startAnimating()
}
self.isHidden = false
}
isSpinning = true
}
public func hide() -> Void {
DispatchQueue.main.async {
if self.spinner.isAnimating {
self.spinner.stopAnimating()
}
self.isHidden = true
}
isSpinning = false
}
}
Now move onto the ViewController that you want to add this overlay view to. Since I create my views programmatically, I will show how to do it the same way, but you can easily do it via storyboard or xibs.
Step 1 : Initialize
public lazy var spinnerView : PSOverlaySpinner = {
let loadingView : PSOverlaySpinner = PSOverlaySpinner()
return loadingView
}()
Step 2 : Add as a subview
self.view.addSubview(spinnerView)
Step 3 : Set constraints
spinnerView.leadingAnchor.constraint(equalTo: self.view.leadingAnchor).isActive = true
spinnerView.trailingAnchor.constraint(equalTo: self.view.trailingAnchor).isActive = true
spinnerView.topAnchor.constraint(equalTo: self.view.topAnchor).isActive = true
spinnerView.bottomAnchor.constraint(equalTo: self.view.bottomAnchor).isActive = true
Step 4 : To show PSOverlaySpinner
spinnerView.show()
Step 5 : To hide PSOverlaySpinner
spinnerView.hide()
That is it!!
If you want you can go ahead and modify the PSOverlaySpinner as per your needs. For example, you might want to add a UILabel below the spinner indicating him of the type of action taking place and so on.
Before
After
Old Answer
If you wish to do it manually then create a UIView with the its frame matching self.view.bounds, with 0.5-0.7 alpha and black background color. Add UIActivityIndicator as its subview constrained to its center. For a spinner specific to the image you will have to use the open sourced spinners made available. A couple of them can be found here. Once done add this view as the topmost subview in self.view.
You need to import this library SVProgressHUD and then set few properties like as follows:
SVProgressHUD.setDefaultStyle(SVProgressHUDStyle.dark)
SVProgressHUD.setBackgroundColor(.clear)
SVProgressHUD.setForegroundColor(.white)
SVProgressHUD.setDefaultMaskType(.black)
SVProgressHUD.show()
//SVProgressHUD.show(withStatus: "Loading something, Loading something,Loading something ...")
This will produce same UI output as needed by you in OP. You can find a running sample at my repository (TestPreLoader)