iosswiftuiviewalertsremovefromsuperview

UIView alerts not being removed from Superview


I've used some code from a YouTube video (Custom Alerts in Swift 5 (Xcode 11) - 2020 iOS App by IOS Academy) to create a custom alert in swift that enters the scene from the top of an iPhone screen, stops in the middle of the screen, then when the user presses the OK button the alert exits the screen by sliding down toward the bottom of the scene.

The Issue: The issue is that when the code displays the first alert everything seems fine, however if a second or third different alert is displayed, it seems that the first alert subview (or 2nd / 3rd etc) isn't being removed properly from the superview, meaning that when the 2nd/3rd alert is being displayed it is being displayed over the top of the previous alert.

I am using Xcode v13.0. I suspect the reason its happening is because the removeFromSuperview is happening in an ObjC function which is outside of where the subviews were created. However, I don't know how to solve this. I have tried many different ways and searched high and low for similar examples.

View Controller code: Here is the code for my view controller:

import UIKit

class ViewController: UIViewController {
    
    var myCounter : Int = 0
    
    @IBOutlet weak var myButton: UIButton!
    
    let customAlert = MyAlert()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        myButton.backgroundColor = .link
        myButton.setTitleColor(.white, for: .normal)
        myButton.setTitle("Show Alert", for: .normal)
        
    }

    @IBAction func didTapButton(_ sender: Any) {
        
        if myCounter > 3 {
            myCounter = 1
        }
        
        if myCounter == 1 {
            customAlert.showMySomethingAlert(on: self)
        }
        
        if myCounter == 2 {
            customAlert.showMySomethingAlert2(on: self)
        }
        
        if myCounter == 3 {
            customAlert.showMySomethingAlert3(on: self)
            
        }
        
        myCounter = myCounter + 1
        
    }
    
    @objc func dismissAlert() {
        customAlert.dismissAlert()
    }

}

CustomAlert Swift File:

import Foundation
import UIKit


class MyAlert {
    
    struct Constants {
        static let backgroundAlphaTo: CGFloat = 0.6
    }
    
    // MARK: Define the greying out of the scene
    private let backgroundView: UIView = {
        let backgroundView = UIView()
        backgroundView.backgroundColor = .black
        backgroundView.alpha = 0
        return backgroundView
    }()
    
    
    // MARK: Define the Alert frame
    private let alertView: UIView = {
        let alert = UIView()
        alert.backgroundColor = .white
        alert.layer.masksToBounds = true
        alert.layer.cornerRadius = 4
        return alert
    }()
    
    
    private let myView: UIView = {
        let myView = UIView()
        myView.translatesAutoresizingMaskIntoConstraints = false
        return myView
    }()
    
    
    private var myTargetView: UIView?
    
 
    
    
    
    // MARK: Show the alert on the scene
    func showAlert(with title: String,
                   message: String,
                   on viewController: UIViewController) {
        guard let targetView = viewController.view else {
            return
        }
        
        // MARK: Determine size of frame needed
        var numberOfLines = message.count / 40
        print("Number of lines is \(numberOfLines)")
        
        var heightOfAlert : CGFloat = 100.0
        
        heightOfAlert = heightOfAlert + (CGFloat(numberOfLines * 25))
        
        
        // MARK: Attributes for the frame
        myTargetView = targetView
        backgroundView.frame = targetView.bounds
        targetView.addSubview(backgroundView)
        
        targetView.addSubview(alertView)
        alertView.frame = CGRect(x: 40,
                                 y: -300,
                                 width: targetView.frame.size.width-80,
                                 height: heightOfAlert)
        
        
        
        
        
        // ***********************************************
        // MARK: Attributes for image within the alert
        let myImageButton = UIButton(frame: CGRect(x: 20, y: 20, width: 50, height: 50))
        myImageButton.setImage(UIImage(named:"HarbourBridge750x390.png"), for: .normal)
        alertView.addSubview(myImageButton)
        
        
        
        // ***********************************************
        // MARK: Attributes for the title within the alert
        let titleLabel = UILabel(frame: CGRect(x: 0,
                                               y: 0,
                                               width: alertView.frame.size.width,
                                               height: 80))
                                              
        titleLabel.text = title
        titleLabel.textAlignment = .center
        //titleLabel.textColor = .black
        titleLabel.textColor = UIColor(red: 33.00/255, green: 150.00/255, blue: 243.00/255, alpha: 1.00)
        titleLabel.font = UIFont.boldSystemFont(ofSize: 12.00)
        alertView.addSubview(titleLabel)
        
        
        
        // ***********************************************
        // MARK: Attributes for the message label within the alert
        let messageLabel = UILabel(frame: CGRect(x: 15,
                                               y: 80,
                                               width: alertView.frame.size.width-30,
                                               height: (CGFloat(numberOfLines * 25))))
                                              
        messageLabel.numberOfLines = 0
        messageLabel.text = message
        messageLabel.textAlignment = .left
        messageLabel.textColor = .black
        messageLabel.font = UIFont.systemFont(ofSize: 12.00)
        alertView.addSubview(messageLabel)
        
        
        
        
        
        
        // ***********************************************
        // MARK: Attributes for the Dismiss button within the alert
        let button = UIButton(frame: CGRect(x: 0,
                                            y: alertView.frame.size.height-50,
                                            width: alertView.frame.size.width,
                                            height: 50))
        
        button.setTitle("Dismiss", for: .normal)
        button.setTitleColor(.link, for: .normal)
        button.addTarget(self,
                         action: #selector(dismissAlert),
                         for: .touchUpInside)
        alertView.addSubview(button)
        
        
        // MARK: Attributes for the animation
        UIView.animate(withDuration: 0.25,
                       animations: {
            
            self.backgroundView.alpha = Constants.backgroundAlphaTo
        
        }, completion: { done in
            if done {
                UIView.animate(withDuration: 0.25, animations: {
                    self.alertView.center = targetView.center
                
                })
            }
        })
    }
    
    
    // MARK: Dismiss the Alert
    @objc func dismissAlert() {
        
        guard let targetView = myTargetView else {
            return
        }
        
        UIView.animate(withDuration: 0.25,
                       animations: {
            
            self.alertView.frame = CGRect(x: 40,
                                          y: targetView.frame.size.height,
                                     width: targetView.frame.size.width-80,
                                     height: 300)
            
        }, completion: { done in
            if done {
                UIView.animate(withDuration: 0.25, animations: {
                    self.backgroundView.alpha = 0
                }, completion: { done in
                    if done {
                        
                        self.alertView.removeFromSuperview()
                        self.backgroundView.removeFromSuperview()
                        
                    }
                })
            }
        })
        
    }
    
    
    // MARK: Setup the alerts to keep the code tidy within the main body files

    func showMySomethingAlert(on vc: UIViewController) {
        showAlert(with: "My Something Alert", message: "Something has gone wrong and your payment was not successful.  This app will continue to operate in trial mode.", on: vc)
    }
    
    func showMySomethingAlert2(on vc: UIViewController) {
        showAlert(with: "My Something Alert", message: "This is a completely different alert and the text should be totally different and not overlap the previous alert whihc appears to be wht is happening", on: vc)
    }
    
    func showMySomethingAlert3(on vc: UIViewController) {
        showAlert(with: "My Something Alert", message: "THIS IS YOUR THIRD ALERT AND is a mixture of lower and UPPER CASE CHARACTERS.", on: vc)
    }
}

Solution

  • Your problem is inside your MyAlert class, you are reusing the instance of alertView, but every time you added new myImageButton titleLabel messageLabel and button

    First time you called customAlert.showMySomethingAlert(on: self) you have

    alertView1 
        //Childs
        myImageButton1
        titleLabel1
        messageLabel1
        button1
    

    when you called customAlert.showMySomethingAlert2(on: self) you will have

    alertView1 
            //Childs
            myImageButton1
            titleLabel1
            messageLabel1
            button1
           //Second instances
            myImageButton2
            titleLabel2
            messageLabel2
            button2
    

    And first added view are still there, so you need to remove old ones or will be better to reuse them.

    Remove ex. NOT RECOMENDED if the alert always has the same views After removing the alert view from its superview you need to do this.

    self.alertView.removeFromSuperview()
     //Add this to remove child views
     for subview in self.alertView.subviews {
         subview.removeFromSuperview()
     }
    

    If your alert always will have the save views, will be better to set new texts and image to already created views and not to create new one and add every time showAlert called.