iosswiftuibutton

Button click not working when adding subview to window in swift


I am trying to do a toast notification by adding a view to the window and it has a progress bar and a button to click if it wants to dismiss before the time expires.

toastView

The button click is not working, it didn't show that it was clicked. I set up isUserInteractionEnabled for both view and button, I checked the view hierarchy to see if any view is blocking the toastView, but the toastView is on top of all the others and still the button doesn't click.

Here's is my code for the toastView and the function that is inside my UIViewController subclass:

ToastView

class iFractalToastView: UIView {
   
    private let tituloLabel = {
        let label = iFractalTituloLabel(title: "STATUS", color: .PGF)
        label.textAlignment = .center
        label.font = UIFont(name: "MuseoSans-700", size: 14)
        return label
    }()
    
    private let messageLabel = {
        let label = iFractalBodyLabel(title: "", isBold: false)
        label.textAlignment = .center
        return label
    }()
    
    let progressBar = UIProgressView(progressViewStyle: .default)
    let dismissButton = iFractalAlertButton(title: "OK")
    
    var dismissAction: (() -> Void)?
    
    private var message = String()
    private var button = Bool()
    
    init(message: String, button: Bool) {
        super.init(frame: .zero)
        self.isUserInteractionEnabled = true
        dismissButton.isUserInteractionEnabled = true
        dismissButton.addTarget(self, action: #selector(dismissButtonTapped), for: .touchUpInside)
        self.message = message
        self.button = button
        self.translatesAutoresizingMaskIntoConstraints = false
        iniciateSubViews()
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    func setProgress(_ progress: Float) {
        progressBar.setProgress(progress, animated: true)
    }
    
    @objc func dismissButtonTapped() {
        print("ENTROUU dismissButtonTapped")
        dismissAction?()
    }
   
    func imageWithColor(_ color: UIColor, size: CGSize = CGSize(width: 1, height: 1)) -> UIImage {
        let format = UIGraphicsImageRendererFormat()
        format.scale = 1
        let image =  UIGraphicsImageRenderer(size: size, format: format).image { rendererContext in
            color.setFill()
            rendererContext.fill(CGRect(origin: .zero, size: size))
        }
        return image
    }
    
    private func iniciateSubViews() {
        addSubViews()
        constraintsSubViews()
        configurateSubviews()
    }
    
    private func addSubViews() {
        addSubview(tituloLabel)
        addSubview(messageLabel)
        addSubview(progressBar)
        if button == true {
            addSubview(dismissButton)
        }
    }
    
    private func constraintsSubViews() {
        tituloLabel.translatesAutoresizingMaskIntoConstraints = false
        messageLabel.translatesAutoresizingMaskIntoConstraints = false
        progressBar.translatesAutoresizingMaskIntoConstraints = false
        
        NSLayoutConstraint.activate([
            tituloLabel.topAnchor.constraint(equalTo: self.topAnchor, constant: 10),
            tituloLabel.leadingAnchor.constraint(equalTo: self.leadingAnchor, constant: 10),
            tituloLabel.trailingAnchor.constraint(equalTo: self.trailingAnchor, constant: -10),
            
            messageLabel.topAnchor.constraint(equalTo: tituloLabel.bottomAnchor, constant: 10),
            messageLabel.leadingAnchor.constraint(equalTo: self.leadingAnchor, constant: 10),
            messageLabel.trailingAnchor.constraint(equalTo: self.trailingAnchor, constant: -10),
            
            progressBar.topAnchor.constraint(equalTo: messageLabel.bottomAnchor, constant: 10),
            progressBar.leadingAnchor.constraint(equalTo: self.leadingAnchor, constant: 0),
            progressBar.trailingAnchor.constraint(equalTo: self.trailingAnchor, constant: 0),
            progressBar.heightAnchor.constraint(equalToConstant: 4)
        ])
        
        if button == true {
            dismissButton.translatesAutoresizingMaskIntoConstraints = false
            
            NSLayoutConstraint.activate([
                dismissButton.topAnchor.constraint(equalTo: progressBar.bottomAnchor, constant: 10),
                dismissButton.centerXAnchor.constraint(equalTo: self.centerXAnchor),
                dismissButton.bottomAnchor.constraint(equalTo: self.bottomAnchor, constant: -10),
                dismissButton.widthAnchor.constraint(equalToConstant: 100)
            ])
        }
        else {
            NSLayoutConstraint.activate([
                progressBar.bottomAnchor.constraint(equalTo: self.bottomAnchor, constant: -10)
            ])
        }
    }
    
    private func configurateSubviews() {
        backgroundColor = .TD90
        layer.cornerRadius = 10
        layer.borderColor = UIColor.PGF.cgColor
        layer.borderWidth = 1.0
        clipsToBounds = true
        
        // Configure the message label
        messageLabel.text = message
        messageLabel.textAlignment = .center
        messageLabel.numberOfLines = 0
        
        // Configure the progress bar
        progressBar.progress = 0.0
        progressBar.progressImage = imageWithColor(.PGF)
        
        //dismissButton.setTitle("OK", for: .normal)
        //dismissButton.setTitleColor(.white, for: .normal)
        dismissButton.clipsToBounds = true
    }
}

UIViewController function

func showToast(message: String, duration: TimeInterval = 60.0, button: Bool = false) {
        let toastView = iFractalToastView(message: message, button: button)
       
        // Configure toast view appearance
        toastView.alpha = 0.0
        toastView.translatesAutoresizingMaskIntoConstraints = false
        if button == true {
            toastView.dismissAction = {
                UIView.animate(withDuration: 0.5, animations: {
                    toastView.alpha = 0.0
                }) { _ in
                    toastView.removeFromSuperview()
                }
            }
        }
        
        // Add the toast view to the view controller's view
        let window = UIApplication.shared.keyWindow!
        window.addSubview(toastView)
        
        // Set up constraints
        if button == false {
            NSLayoutConstraint.activate([
                toastView.leadingAnchor.constraint(equalTo: window.leadingAnchor, constant: 10),
                toastView.trailingAnchor.constraint(equalTo: window.trailingAnchor, constant: -10),
                toastView.topAnchor.constraint(equalTo: window.topAnchor, constant: 41)
            ])
        }
        else {
            NSLayoutConstraint.activate([
                toastView.leadingAnchor.constraint(equalTo: window.leadingAnchor, constant: 10),
                toastView.trailingAnchor.constraint(equalTo: window.trailingAnchor, constant: -10),
                toastView.centerXAnchor.constraint(equalTo: window.centerXAnchor),
                toastView.centerYAnchor.constraint(equalTo: window.centerYAnchor)
            ])
        }
       
        window.bringSubviewToFront(toastView)
       
        // Animate the toast view appearance
        UIView.animate(withDuration: 0.5, animations: {
            toastView.alpha = 1.0
        }) { _ in
            // Animate the progress bar
            let progress = Float(duration)
            toastView.setProgress(progress)
            
            // Animate the toast view disappearance
            UIView.animate(withDuration: 0.5, delay: duration, options: .curveEaseOut, animations: {
                toastView.alpha = 0.0
            }) { _ in
                toastView.removeFromSuperview()
            }
        }
    }

Solution

  • The problem lies in this part:

            // Animate the toast view disappearance
            UIView.animate(withDuration: 0.5, delay: duration, options: .curveEaseOut, animations: {
                toastView.alpha = 0.0
            }) { _ in
                toastView.removeFromSuperview()
            }
    
    1. You forgot to add allowUserInteraction to the animation options.
    2. Regardless of the duration set, your model layer immediately receives zero alpha. If you check toastView.layer.opacity upon toast view appearance completion, it will be zero.
    3. Touch events are only worked if your view has an opacity greater than 0.01.

    How to fix it:

        // Animate the toast view disappearance
        UIView.animate(
            withDuration: 0.5,
            delay: duration,
            // Add allowUserInteraction
            options: [.curveEaseOut, .allowUserInteraction],
            animations: {
                // Set alpha more then 0.01
                toastView.alpha = 0.2
            }) { _ in
                toastView.removeFromSuperview()
            }
    

    or:

        // In this case, there's no need to include "allowUserInteraction" or "0.02 alpha"
        // Because UIView.animate will not be called immediately.
        DispatchQueue.main.asyncAfter(deadline: .now() + duration) {
            UIView.animate(
                withDuration: 0.5,
                delay: 0,
                options: .curveEaseOut,
                animations: {
                    toastView.alpha = 0.0
                }) { _ in
                    toastView.removeFromSuperview()
                }
        }