Hiding UIButton(which added programatically) once view is hidden - ios

Goal:
Hide a UIButton(w/ image) once a view is hidden.
I have a layout whereby the map view can be hidden when user taps(UITapGestureRecognizer) on the screen. When this happen, I would like to hide the "follow user button" triangle. Currently I am not able to do it.
What I've tried: (from multiple google/SO posts)
1)
followUserButton.removeFromSuperview()
followUserButton.widthAnchor.constraint(equalToConstant: 150).isActive = true
followUserButton.heightAnchor.constraint(equalToConstant: 150).isActive = true
followUserButton.setImage(image:nil for: .normal)
the last one I tried is basically just making the image black in color (to blend into the background). This does look like a success, but (see gif image), for some reason, the first click, will still show the button (very light black/grey - in the bottom middle of image). Click again, the map view comes on, then click again and it finally disappears
followUserButton.tintColor = .black
followUserButton.isHidden = true
this is how I'm adding my button programatically
var followUserImage: UIImage!
var followUserButton: UIButton!
override func viewDidLoad() {
setupFollowUserButton()
}
func setupFollowUserButton() {
addFollowUserButton()
self.view.addSubview(followUserButton)
constraintFollowUserButton()
}
func hideFollowUserButton() {
if vcTrainMapView.isHidden {
if followUserButton != nil {
// followUserButton.removeFromSuperview()
// followUserButton.tintColor = .black
followUserButton.isHidden = true
}
} else if followUserButton != nil {
followUserButton.tintColor = .lightGray
}
}
func addFollowUserButton() {
followUserButton = UIButton(type: UIButton.ButtonType.custom)
followUserImage = UIImage(named: "follow_user_high")
followUserButton.setImage(followUserImage, for:.selected)
followUserImage = (UIImage(named: "follow_user"))
followUserButton.setImage(followUserImage, for: .normal)
followUserButton.tintColor = .lightGray
followUserButton.addTarget(self, action: #selector(buttonAction(_:)), for: .touchUpInside)
}
#objc private func buttonAction(_ sender: UIButton) {
self.followUserStatus = !self.followUserStatus
sender.isSelected.toggle()
}
func constraintFollowUserButton() {
followUserButton.translatesAutoresizingMaskIntoConstraints = false
followUserButton.bottomAnchor.constraint(equalTo: vcTrainMapView.bottomAnchor, constant: -10).isActive = true
followUserButton.leadingAnchor.constraint(equalTo: vcTrainMapView.leadingAnchor, constant: 10).isActive = true
followUserButton.widthAnchor.constraint(equalToConstant: 50).isActive = true
followUserButton.heightAnchor.constraint(equalToConstant: 50).isActive = true
}
this is what I've achieved w/ #3 above. (the button is very light in the gif)

If you're going to be hiding and showing a button at the same time as a detail view (your map view), and you're displaying the button so that it looks like it's on that view, you could just add the button directly to that view rather than the view controller main view.
You can, of course, still control the action of the button from the view controller, but if it's added to the map view then the button will be hidden when the map view is hidden.

Related

messageInputBar not dismissing when I dismiss my MessagesViewController (MessageKit, begging for help!)

I am using MessageKit. I've created a MessagesViewController. I add messageInputBar as a subview from viewDidLoad along with a navigational bar that includes a back button. Whenever I am in this view controller and I tap on the messageInputBar's text field and then tap the back button, the messageInputBar stays on the screen when the app goes back to the previous UIViewController. If I don't tap on the messageInputBar when i first enter the MessagesViewController and press the back button, the messageInputBar properly is dismissed. Below is my code
override func viewDidLoad() {
super.viewDidLoad()
setUpNavBar()
navigationItem.largeTitleDisplayMode = .never
maintainPositionOnKeyboardFrameChanged = true
scrollsToLastItemOnKeyboardBeginsEditing = true
messageInputBar.inputTextView.tintColor = .systemBlue
messageInputBar.sendButton.setTitleColor(.systemTeal, for: .normal)
messageInputBar.delegate = self
messagesCollectionView.messagesDataSource = self
messagesCollectionView.messagesLayoutDelegate = self
messagesCollectionView.messagesDisplayDelegate = self
loadChat()
self.view.addSubview(messageInputBar)
messageInputBar.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
messageInputBar.bottomAnchor.constraint(equalTo: self.view.bottomAnchor),
messageInputBar.widthAnchor.constraint(equalToConstant: self.view.bounds.width)
])
NSLayoutConstraint.activate([
messagesCollectionView.topAnchor.constraint(equalTo: self.view.topAnchor, constant: 100)
])
}
func setUpNavBar() {
let navBar = UINavigationBar()
self.view.addSubview(navBar)
navBar.items?.append(UINavigationItem(title: (selectedUser?.userFirstName)!))
let backButton = UIBarButtonItem(title: "Back", style: .plain, target: self, action: #selector(backButtonTapped))
navBar.topItem?.leftBarButtonItem = backButton
navBar.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
navBar.topAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.topAnchor),
navBar.heightAnchor.constraint(equalToConstant: 44),
navBar.widthAnchor.constraint(equalToConstant: self.view.bounds.width)
])
}
#IBAction func backButtonTapped(_ sender: Any) {
let transition = CATransition()
self.view.window!.layer.add(transition.segueLeftToRight(), forKey: kCATransition)
self.dismiss(animated: false)
}
As you have created MessagesViewController, you don't need to explicitly add messageInputBar to the bottom of the view.
Let's look at the source of MessageKit
private func setupInputBar(for kind: MessageInputBarKind) {
inputContainerView.subviews.forEach { $0.removeFromSuperview() }
func pinViewToInputContainer(_ view: UIView) {
view.translatesAutoresizingMaskIntoConstraints = false
inputContainerView.addSubviews(view)
NSLayoutConstraint.activate([
view.topAnchor.constraint(equalTo: inputContainerView.topAnchor),
view.bottomAnchor.constraint(equalTo: inputContainerView.bottomAnchor),
view.leadingAnchor.constraint(equalTo: inputContainerView.leadingAnchor),
view.trailingAnchor.constraint(equalTo: inputContainerView.trailingAnchor),
])
}
switch kind {
case .messageInputBar:
pinViewToInputContainer(messageInputBar)
case .custom(let view):
pinViewToInputContainer(view)
}
}
The following code section should be removed from your source as the messageInputBar has already been set up in the library.
self.view.addSubview(messageInputBar)
messageInputBar.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
messageInputBar.bottomAnchor.constraint(equalTo: self.view.bottomAnchor),
messageInputBar.widthAnchor.constraint(equalToConstant: self.view.bounds.width)
])
NSLayoutConstraint.activate([
messagesCollectionView.topAnchor.constraint(equalTo: self.view.topAnchor, constant: 100)
])
Now, your scenario
Whenever I am in this view controller and I tap on the messageInputBar's text field and then tap the back button, the messageInputBar stays on the screen when the app goes back to the previous UIViewController.
Whenever, there is an object that you interact with (eg. messageInputBar), and it is not deallocated (stays in view) after you dismissed the view controller, there is a memory leak.
If you repeatedly enter and dismiss the view controller, you will observe a rise in the memory usage of the app. So, finding out which object is creating this retain cycle, should solve this issue.
I installed MessageKit using Cocoapods later to find out they dropped support for Cocoapods. So I completely migrated my entire project over to Swift Package Manager to get the latest MessageKit which includes the setup for the inputbar in their code. No idea why they would release a version that didn't have this initially? Anyways, solved my problem!

Accessibility focus order not working as expected [iOS]

OVERVIEW
I'm having trouble getting correct focus order (Accessibility in iOS). It seems like becomeFirstResponder() overwrites my focus order I have specified in the array and causes Voice Over Accessibility functionality to read wrong Accessibility Label first.
DETAILS:
I have a View Controller with containerView. Inside I have UIView of my progress bar image and text input field (placeholder). Both elements have isAccessibilityElement = true attributes and they have been added to my focus order array. However upon screen launch, focus order goes to the input field instead of progress bar UIView.
After extended testing I've noticed that this issue is no longer replicable if I remove below line of code.
otpNumberTextField.becomeFirstResponder()
But this is not a solution. I need cursor in the textfield but Voice Over functionality to read my Progress Bar Accessibility Label first. How to fix it?
SPECIFIC SCENARIO
I've noticed this issue occurs only when I have VC with a last active focus on a Textfield and then transition to the next VC (with a Textfield and a Progress Bar).
Bug is not replicable when I have VC with a last active focus on the Button and then transition to the next VC (with a Textfield and a Progress Bar).
CODE SNIPPET
import UIKit
class MyViewController: UIViewController, UITextFieldDelegate {
var otpNumberTextField = UITextField()
var progressMainDot = UIImageView()
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillShow), name: UIResponder.keyboardWillShowNotification, object: nil)
setupView()
setupBinding()
}
override func viewWillAppear(_ animated: Bool) {
setupView()
textFieldDidChange(otpNumberTextField)
}
func setupView(){
let containerView = UIView()
containerView.backgroundColor = UIColor.init(named: ColourUtility.BackgroundColour)
view.addSubview(containerView)
containerView.translatesAutoresizingMaskIntoConstraints = false
containerView.leadingAnchor.constraint(equalTo: self.view.leadingAnchor).isActive = true
containerView.trailingAnchor.constraint(equalTo: self.view.trailingAnchor).isActive = true
containerView.topAnchor.constraint(equalTo: self.view.topAnchor).isActive = true
containerView.bottomAnchor.constraint(equalTo: self.view.bottomAnchor).isActive = true
//Progress Bar
let progressBarView = UIView()
containerView.addSubview(progressBarView)
progressBarView.isAccessibilityElement = true
progressBarView.accessibilityLabel = "my accessibility label"
progressBarView.translatesAutoresizingMaskIntoConstraints = false
progressMainDot.image = UIImage(named:ImageUtility.progressMain)
progressMainDot.contentMode = .scaleAspectFit
progressBarView.addSubview(progressMainDot)
//Text Field
otpNumberTextField.borderStyle = UITextField.BorderStyle.none
otpNumberTextField.font = UIFontMetrics.default.scaledFont(for: FontUtility.inputLargeTextFieldStyle)
otpNumberTextField.adjustsFontForContentSizeCategory = true
otpNumberTextField.isAccessibilityElement = true
otpNumberTextField.accessibilityLabel = AccessibilityUtility.enterVerificationCode
otpNumberTextField.placeholder = StringUtility.otpPlaceholder
otpNumberTextField.textColor = UIColor.init(named: ColourUtility.TextfieldColour)
otpNumberTextField.textAlignment = .center
otpNumberTextField.keyboardType = .numberPad
otpNumberTextField.addTarget(self, action: #selector(self.textFieldDidChange(_:)), for: .editingChanged)
containerView.addSubview(otpNumberTextField)
otpNumberTextField.becomeFirstResponder()
//Accessibility - focus order
view.accessibilityElements = [progressBarView, otpNumberTextField]
}
//... more code goes here ...
}
If you have already set accessibilityElements, then voice over should respects that order but calling becomeFirstResponder() changes the focus to that text field.
You can try below code, which notifies voice over for shifting the focus to new element due to layout changes.
UIAccessibility.post(notification: .layoutChanged, argument: progressBarView)
So now your modified method should be like below:
func setupView(){
.....
otpNumberTextField.becomeFirstResponder()
//Accessibility - focus order
view.accessibilityElements = [progressBarView, otpNumberTextField]
UIAccessibility.post(notification: .layoutChanged, argument: progressBarView)
.....
}

Button doesn’t work properly after subclassing view controller to base controller

I am making a simple app, which for now has 3 pages, and I wanted to have a base view controller to all my view controllers. But my button on the first page, which was working perfectly, that, when clicked, goes to the second page, doesn't work properly anymore, meaning, the button opens up a blank page and not the second page. But when I remove the subclass from the other view controllers, the button works perfectly. BTW I'm creating this app 100% programmatically. I know that I haven't provided the code or any screenshots of my code, but I hope you can help me out! Thanks! :)
// Setup the login button
func setupLoginButton() {
loginButton.backgroundColor = UIColor.clear
loginButton.setTitle("Login", for: .normal)
loginButton.titleLabel?.font = UIFont(name: "Avenir-Heavy", size: 25)
loginButton.setTitleColor(baseColor, for: .normal)
loginButton.layer.borderWidth = 1
loginButton.layer.borderColor = baseColor.cgColor
loginButton.layer.cornerRadius = 25
// Make it go to the main screen when pressed
loginButton.addTarget(self, action: #selector(loginButtonPressed), for: .touchUpInside)
view.addSubview(loginButton)
addLoginButtonConstraints()
}
// Add the constraints to the login button
func addLoginButtonConstraints() {
loginButton.translatesAutoresizingMaskIntoConstraints = false
loginButton.heightAnchor.constraint(equalToConstant: 50).isActive = true
loginButton.widthAnchor.constraint(equalToConstant: 335).isActive = true
loginButton.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
loginButton.centerYAnchor.constraint(equalTo: view.centerYAnchor, constant: -5).isActive = true
}
// When login button is pressed
#objc func loginButtonPressed() {
let mainPage = MainScreen()
present(mainPage, animated: true, completion: nil)
}
So here, When I click on the login button, it should go to the mainPage, but it just opens up a blank page.
Here is the mainPage
import UIKit
class MainScreen: BaseAndExtensions {
override func viewDidLoad() {
super.viewDidLoad()
}
}
BaseAndExtensions is the base view controller.

make pop up view disappear

I have button, when I click that button, a view pops up.
testingButton.addTarget(self, action: #selector(popupTable), for: .touchUpInside)
#objc func popupTable(){
self.opaqueView = self.setUpPopView()
mainView.addSubview(opaqueView)
print("button clicked")
}
if the view is present, I want to click on the button again and make the view disappear.
if tinyView.isHidden == false {
tinyView.isHidden = false
self.testingButton.addTarget(self, action: #selector(removePopUp), for: .touchUpInside)
}else{
tinyView.isHidden = true
mainView.addSubview(tinyView)
}
Which it does but when I click on the button a third time (pop up view is not present) i want the pop up view to appear again but it doesn't.
show pop up
func setUpPopView () -> UIView {
tinyView.backgroundColor = UIColor(red:0.99, green:0.99, blue:0.99, alpha:1.0)
tinyView.layer.cornerRadius = 3
tinyView.layer.masksToBounds = false
tinyView.layer.shadowColor = UIColor.black.withAlphaComponent(0.4).cgColor
tinyView.layer.shadowOffset = CGSize(width: 0, height: 0)
tinyView.layer.shadowOpacity = 0.9
let post: UIButton = UIButton(frame: CGRect(x: 5, y: 10, width: 150, height: 50))
post.backgroundColor = UIColor.clear
post.setTitleColor(UIColor.black, for:UIControlState.normal)
post.setTitle("All Post", for: .normal)
post.contentHorizontalAlignment = .left
post.addTarget(self, action: #selector(removePopUp), for: .touchUpInside)
tinyView.addSubview(post)
}
remove pop up
#objc func removePopUp(_ sender: AnyObject){
self.tinyView.removeFromSuperview()
}
You should use one action only and check the visibility of the pop up view in the action. According to the visibility of the view you can then show or hide the pop up.
Like this:
#objc func popupAction() {
If self.popUpView.isHidden {
// Pop up view is hidden, so show the pop up view again
} else {
// Pop up view is visible, so hide the pop up view
}
}
Also change this:
if tinyView.isHidden == false
{
tinyView.isHidden = false
self.testingButton.addTarget(self, action: #selector(removePopUp), for: .touchUpInside)
}else{
tinyView.isHidden = true
mainView.addSubview(tinyView)
}
With this:
if tinyView.isHidden == false
{
tinyView.isHidden = true
tinyView.removeFromSuperview()
}else{
tinyView.isHidden = false
mainView.addSubview(tinyView)
}
You should not be calling removeFromSuperview. You should not be saying if. It all just adds unnecessary complications.
If your goal is to show and hide a view, just show it and hide it: toggle its hiddenness, which is a one-liner:
tinyView.isHidden = !tinyView.isHidden
That way, if it is invisible, it becomes visible, and if it is visible, it becomes invisible. In Swift 4.2, it's even simpler:
tinyView.isHidden.toggle()
That's all! You don't need know whether it's already hidden, you don't need to remove it from the superview. No if, no nothing. You just invert its hiddenness each time you want to hide or show it.

UIStackView & Gestures

I'm trying to get/keep a handle on elements in my UIStackView after I have moved it with a pan gesture.
For example, the elements I am trying to grab a handle on are things like the button tag or the text label text.
The code explained…
I am creating a UIStackView, via the function func createButtonStack(label: String, btnTag: Int) -> UIStackView
It contains a button and a text label.
When the button stack is created, I attach a pan gesture to it so I can move the button around the screen. The following 3 points work.
I get the button stack created
I can press the button and call the fun to print my message.
I can move the button stack.
The issue I have is…
Once I move the button stack the first time and the if statement gesture.type == .ended line is triggered, I lose control of the button stack.
That is, the button presses no longer work nor can I move it around any longer.
Can anyone please help? Thanks
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .lightGray
let ButtonStack = createButtonStack(label: “Button One”, btnTag: 1)
view.addSubview(ButtonStack)
ButtonStack.centerXAnchor.constraint(equalTo: self.view.centerXAnchor).isActive = true
ButtonStack.centerYAnchor.constraint(equalTo: self.view.centerYAnchor).isActive = true
let panGuesture = UIPanGestureRecognizer(target: self, action: #selector(pan(guesture:)))
ButtonStack.isUserInteractionEnabled = true
ButtonStack.addGestureRecognizer(panGuesture)
}
func createButtonStack(label: String, btnTag: Int) -> UIStackView {
let button = UIButton()
button.setImage( imageLiteral(resourceName: "star-in-circle"), for: .normal)
button.heightAnchor.constraint(equalToConstant: 100.0).isActive = true
button.widthAnchor.constraint(equalToConstant: 100.0).isActive = true
button.contentMode = .scaleAspectFit
button.tag = btnTag
switch btnTag {
case 1:
button.addTarget(self, action: #selector(printMessage), for: .touchUpInside)
case 2:
break
default:
break
}
//Text Label
let textLabel = UILabel()
textLabel.backgroundColor = UIColor.green
textLabel.widthAnchor.constraint(equalToConstant: 100.0).isActive = true
textLabel.heightAnchor.constraint(equalToConstant: 25.0).isActive = true
textLabel.font = textLabel.font.withSize(15)
textLabel.text = label
textLabel.textAlignment = .center
//Stack View
let buttonStack = UIStackView()
buttonStack.axis = UILayoutConstraintAxis.vertical
buttonStack.distribution = UIStackViewDistribution.equalSpacing
buttonStack.alignment = UIStackViewAlignment.center
buttonStack.spacing = 1.0
buttonStack.addArrangedSubview(button)
buttonStack.addArrangedSubview(textLabel)
buttonStack.translatesAutoresizingMaskIntoConstraints = false
return buttonStack
}
#objc func printMessage() {
print(“Button One was pressed”)
}
#objc func pan(guesture: UIPanGestureRecognizer) {
let translation = guesture.translation(in: self.view)
if let guestureView = guesture.view {
guestureView.center = CGPoint(x: guestureView.center.x + translation.x, y: guestureView.center.y + translation.y)
if guesture.state == .ended {
print("Guesture Center - Ended = \(guestureView.center)")
}
}
guesture.setTranslation(CGPoint.zero, in: self.view)
}
If you're using autolayout on the buttonStack you can't manipulate the guestureView.center centerX directly. You have to work with the constraints to achieve the drag effect.
So instead of ButtonStack.centerXAnchor.constraint(equalTo: self.view.centerXAnchor).isActive = true you should do something along the lines of:
let centerXConstraint = ButtonStack.centerXAnchor.constraint(equalTo: self.view.centerXAnchor)
centerXConstraint.isActive = true
ButtonStack.centerXConstraint = centerXConstraint
To do it like this you should declare a weak property of type NSLayoutConstraint on the ButtonStack class. You can do the same thing for the centerY constraint.
After that in the func pan(guesture: UIPanGestureRecognizer) method you can manipulate the centerXConstraint and centerYConstraint properties directly on the ButtonStack view.
Also, I see you are not setting the translatesAutoresizingMaskIntoConstraints property to false on the ButtonStack. You should do that whenever you are using autolayout programatically.
Thanks to PGDev and marosoaie for their input. Both provided insight for me to figure this one out.
My code worked with just the one button, but my project had three buttons inside a UIStackView.
Once I moved one button, it effectively broke the UIStackView and I lost control over the moved button.
The fix here was to take the three buttons out of the UIStackView and I can now move and control all three buttons without issues.
As for keeping a handle on the button / text field UIStackView, this was achieved by adding a .tag to the UIStackView.
Once I moved the element, the .ended action of the pan could access the .tag and therefore allow me to identify which button stack was moved.
Thanks again for all of the input.

Resources