iOS 16 bug - ViewController does not respond to taps

Viewed 523

Edit: per the request of the commenters, we've created a small project that you can use to reproduce the issue. https://github.com/Coursicle/ReproducingiOS16PopUpControllerBug. Notice that when you run this project on iOS 15 or below, the pop-up responds to taps. On iOS 16, it does not respond to any taps.

We have a view controller that allows users to set notification preferences for a channel in our app. The view slides up from the bottom of the screen and dims the rest of screen with a semi-transparent background view. In iOS 15 and below, it worked as expected: you could dismiss the view by tapping the background or select a new option and tap "Apply". However, in iOS 16 the view and the semi-transparent background view are unresponsive to any taps.

What's most surprising is that we have nearly identical view controllers (ones that have a semi-transparent background view, that are presented as a popover from the UITabBarController, etc.) which work fine in both iOS 15 and iOS 16. And there doesn't appear to be any significant difference between the working and non-working views in iOS 16.

Things we've tried:

  • Having view controllers other than the UITabBarController present the pop-up.
  • Putting a UIWindow sendEvent breakpoint, which indicates the taps are hitting the correct elements, e.g. <UILabel: 0x133db3e10; frame = (78 0; 208 48); text = 'Every Post'; backgroundColor = UIExtendedGrayColorSpace 0 0; layer = <_UILabelLayer: 0x60000233c0a0>.
  • Inspecting the view hierarchy visually, which indicates that the view and the background are top-most and thus should be responding to events. Here's the image.
  • Explicitly setting isUserInteractionEnabled to true for various elements on the view and on the background view.

Here's what it looks like: Visual example

Here's the code that creates and displays the popup:

let rootController = UIApplication.shared.windows.first!.rootViewController as! CustomTabBar
let popover = NotificationSettingsPopUpController(customTabBarController: rootController, delegateController: self)
popover.presentationController?.delegate = self
popover.displayFilterPopUp()

Here's the code for the pop-up:

class NotificationSettingsPopUpController: UIViewController {
    
    var customTabBarController : CustomTabBar
    let backgroundView : UIView = UIView()
    let filterPopUpViewSlideDuration = 0.2
    var filterPopUpViewHeight = CGFloat(0.52*Double(deviceScreenHeight))
    let filterPopUpViewWidth = CGFloat(deviceScreenWidth)
    let filterPopUpViewRadius = CGFloat(10.0)
    let filterOptionsContainer = UIStackView()
    
    // These variables will be used to handle the pan gesture
    // to drag the class filter view on and off screen.
    lazy var startingPosition = filterPopUpViewHeight*2.3
    lazy var finalPosition = filterPopUpViewHeight*1.5
    lazy var turningPointToShow = finalPosition*1.4
    lazy var turningPointToHide = finalPosition*1.2
    
    // Header Sizing Variables
    let headerLabel : UILabel = UILabel()
    var headerLabelTopMargin : CGFloat = 28
    let headerLabelSideMargin : CGFloat = 20
    
    // Header Sizing Variables
    let descriptionLabel : UILabel = UILabel()
    var descriptionLabelTopMargin : CGFloat = 45
    let descriptionLabelSideMargin : CGFloat = 40
    
    // Apply Button Variables
    let applyButton = UIButton()
    var applyButtonWidth: CGFloat = CGFloat(deviceScreenWidth)*0.85
    var applyButtonHeight: CGFloat = 45
    var applyButtonFontSize: CGFloat = 18
    let applyButtonColor = UIColor.init(hex: "#207af3")
    let applyButtonColorLight = UIColor.init(hex: "#3686f3")
    
    // Sort option buttons
    let everyPostOption : MenuOptionsView = MenuOptionsView()
    let topDailyOption : MenuOptionsView = MenuOptionsView()
    let topWeeklyOption : MenuOptionsView = MenuOptionsView()
    let topMonthlyOption : MenuOptionsView = MenuOptionsView()
    let neverOption : MenuOptionsView = MenuOptionsView()
    
    var currentlySelectedOption : String
    
    // Delegate Controller
    var delegateController : PostsInChannelViewController
    
    init(customTabBarController: CustomTabBar, delegateController: PostsInChannelViewController){
        
        // Set the initial sort option
        self.currentlySelectedOption = "Top Weekly Post"
        if let settings = getSettingsForChannel(delegateController.channel.id){
            if settings.keys.contains("notificationPreference"){
                self.currentlySelectedOption = settings["notificationPreference"] ?? "Top Weekly Post"
            }
        }
        
        // set delegate controller
        self.delegateController = delegateController
        
        // set viewToReturnTo so the correct view is displayed
        // once the class filter view is dismissed
        self.customTabBarController = customTabBarController
        super.init(nibName: nil, bundle: nil)
        
        // add class filter view and background to the window
        customTabBarController.view.addSubview(backgroundView)
        customTabBarController.view.addSubview(view)
        
        // display translucent black backdrop to hide classes view
        // when class filter view is being displayed
        backgroundView.backgroundColor = UIColor(hex: "#000000", alpha: 0)
        backgroundView.frame = CGRect(x: 0, y: -deviceScreenHeight, width: deviceScreenWidth, height: deviceScreenHeight)
        
        // adjust layout variables based on screen size
        if UIDevice().screenType == .iPhones_6_6s_7_8 || UIDevice().screenType == .iPhone_12Mini {
            filterPopUpViewHeight = CGFloat(0.55*Double(deviceScreenHeight))
            startingPosition = filterPopUpViewHeight*2.3
            finalPosition = filterPopUpViewHeight*1.5
            turningPointToShow = finalPosition*1.4
            turningPointToHide = finalPosition*1.2
        }
        
        if UIDevice().screenType == .iPhones_6_6s_7_8{
            filterPopUpViewHeight = CGFloat(0.65*Double(deviceScreenHeight))
        }
        
        if UIDevice().screenType == .iPhone_XSMax_ProMax || UIDevice().screenType == .iPhone_12ProMax{
            filterPopUpViewHeight = CGFloat(0.47*Double(deviceScreenHeight))
        }
        
        // position class filter view off-screen initially (tried to do this
        // with layout anchors but seemed more complicated than it was
        // worth - could try doing it again if we find it's necessary)
        view.frame = CGRect(x: 0, y: CGFloat(deviceScreenHeight), width: filterPopUpViewWidth, height: filterPopUpViewHeight)
        view.layer.cornerRadius = filterPopUpViewRadius
        view.backgroundColor = .white
    }
    
    // add gesture recognizers so that when user taps outside of
    // class filter view or swipes down, the class filter view is dismissed
    override func viewDidLoad() {
        
        backgroundView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(dismissMyself)))
        
        // Set up the header label
        view.addSubview(headerLabel)
        headerLabel.text = "Notify me"
        headerLabel.font = .systemFont(ofSize: 24, weight: .bold)
        headerLabel.sizeToFit()
        if (UIDevice().screenType == .iPhone_XSMax_ProMax || UIDevice().screenType == .iPhones_X_XS_12MiniSimulator || UIDevice().screenType == .iPhone_XR_11) && iOSIsOld {
            headerLabelTopMargin += 15
        }
        headerLabel.translatesAutoresizingMaskIntoConstraints = false
        NSLayoutConstraint.activate([
            headerLabel.leftAnchor.constraint(equalTo: view.leftAnchor, constant: headerLabelSideMargin),
            headerLabel.topAnchor.constraint(equalTo: view.topAnchor, constant: headerLabelTopMargin),
        ])
        
        setUpFilterOptions()
        
        setUpApplyButton()
    }
    
    func setUpApplyButton() {
        // Create apply now button
        applyButton.translatesAutoresizingMaskIntoConstraints = false
        applyButton.layer.cornerRadius = applyButtonHeight/2
        
        if(UIDevice().screenType == .iPhones_5_5s_5c_SE){applyButtonFontSize -= 4}
        applyButton.titleLabel?.font = UIFont.systemFont(ofSize: applyButtonFontSize, weight: UIFont.Weight.regular)
        applyButton.setTitle("Apply", for: .normal)
        applyButton.setTitleColor(UIColor.white, for: .normal)
        applyButton.backgroundColor = applyButtonColor
        
        // Add gesture recognizer for the apply button
        applyButton.addTarget(self, action: #selector(applyButtonTapped), for: .touchUpInside)
        applyButton.addTarget(self, action: #selector(applyButtonTouchedDown), for: .touchDown)

        // Add apply button to the view and set position
        view.addSubview(applyButton)
        NSLayoutConstraint.activate([
            applyButton.topAnchor.constraint(equalTo: filterOptionsContainer.bottomAnchor, constant: 22),
            applyButton.widthAnchor.constraint(equalToConstant: applyButtonWidth),
            applyButton.heightAnchor.constraint(equalToConstant: applyButtonHeight),
            applyButton.centerXAnchor.constraint(equalTo: view.centerXAnchor),
        ])
    }

    func setUpFilterOptions() {
        
        filterOptionsContainer.translatesAutoresizingMaskIntoConstraints = false
        filterOptionsContainer.axis  = NSLayoutConstraint.Axis.vertical
        filterOptionsContainer.distribution  = UIStackView.Distribution.fillEqually
        filterOptionsContainer.alignment = UIStackView.Alignment.center
        filterOptionsContainer.spacing = 5
        
        view.addSubview(filterOptionsContainer)
        
        filterOptionsContainer.addArrangedSubview(everyPostOption)
        filterOptionsContainer.addArrangedSubview(topDailyOption)
        filterOptionsContainer.addArrangedSubview(topWeeklyOption)
        filterOptionsContainer.addArrangedSubview(topMonthlyOption)
        filterOptionsContainer.addArrangedSubview(neverOption)
        
        everyPostOption.iconView.text = String.fontAwesomeIcon(name: .bell)
        topDailyOption.iconView.text = String.fontAwesomeIcon(name: .bell)
        topWeeklyOption.iconView.text = String.fontAwesomeIcon(name: .bell)
        topMonthlyOption.iconView.text = String.fontAwesomeIcon(name: .bell)
        neverOption.iconView.text = String.fontAwesomeIcon(name: .bell)
        
        everyPostOption.infoLabel.text = "Every Post"
        topDailyOption.infoLabel.text = "Top Daily Post"
        topWeeklyOption.infoLabel.text = "Top Weekly Post"
        topMonthlyOption.infoLabel.text = "Top Monthly Post"
        neverOption.infoLabel.text = "Never"
        
        everyPostOption.addTarget(self, action: #selector(selectNewSortOption(_:)), for: .touchUpInside)
        topDailyOption.addTarget(self, action: #selector(selectNewSortOption(_:)), for: .touchUpInside)
        topWeeklyOption.addTarget(self, action: #selector(selectNewSortOption(_:)), for: .touchUpInside)
        topMonthlyOption.addTarget(self, action: #selector(selectNewSortOption(_:)), for: .touchUpInside)
        neverOption.addTarget(self, action: #selector(selectNewSortOption(_:)), for: .touchUpInside)
        
        switch(currentlySelectedOption) {
        case "Every Post":
            everyPostOption.backgroundColor = UIColor.init(hex: "#F2F2F2")
            everyPostOption.checkmarkIconView.isHidden = false
        case "Top Daily Post":
            topDailyOption.backgroundColor = UIColor.init(hex: "#F2F2F2")
            topDailyOption.checkmarkIconView.isHidden = false
        case "Top Weekly Post":
            topWeeklyOption.backgroundColor = UIColor.init(hex: "#F2F2F2")
            topWeeklyOption.checkmarkIconView.isHidden = false
        case "Top Monthly Post":
            topMonthlyOption.backgroundColor = UIColor.init(hex: "#F2F2F2")
            topMonthlyOption.checkmarkIconView.isHidden = false
        case "Never":
            neverOption.backgroundColor = UIColor.init(hex: "#F2F2F2")
            neverOption.checkmarkIconView.isHidden = false
        default:
            assert(false)
            return
        }

        NSLayoutConstraint.activate([
            filterOptionsContainer.topAnchor.constraint(equalTo: headerLabel.bottomAnchor, constant: 20),
            filterOptionsContainer.centerXAnchor.constraint(equalTo: view.centerXAnchor),
        ])
    }
    
    
    @objc func applyButtonTapped(_ sender: UIButton) {
        
        // Store the preference in user defaults so that
        // we can display what setting they have next time they open it
        storeSettingsForChannel(settingName: "notificationPreference", settingValue: currentlySelectedOption, channel: delegateController.channel.id)
        
        // send the preference to the server
        if let uuid = getUUID(){
            setUserChannelNotificationPreference(uuid: uuid, channelID: delegateController.channel.id, preference: currentlySelectedOption)
        }
        
        // Dismiss Filter modal screen
        dismissMyself()
    }
    
    @objc func applyButtonTouchedDown(_ sender: UIButton){
        sender.backgroundColor = applyButtonColorLight
    }
    
    @objc func selectNewSortOption(_ sender: MenuOptionsView) {
        everyPostOption.backgroundColor = .white
        topDailyOption.backgroundColor = .white
        topWeeklyOption.backgroundColor = .white
        topMonthlyOption.backgroundColor = .white
        neverOption.backgroundColor = .white
    
        everyPostOption.checkmarkIconView.isHidden = true
        topDailyOption.checkmarkIconView.isHidden = true
        topWeeklyOption.checkmarkIconView.isHidden = true
        topMonthlyOption.checkmarkIconView.isHidden = true
        neverOption.checkmarkIconView.isHidden = true
        
        sender.backgroundColor = UIColor.init(hex: "#F2F2F2")
        sender.checkmarkIconView.isHidden = false
        
        currentlySelectedOption = sender.infoLabel.text ?? "Top Weekly Post"
    }
    
    // this function sets up and displays the class filter view -
    // this gets called from the home view controller
    func displayFilterPopUp() {
        backgroundView.frame = CGRect(x: 0, y: 0, width: deviceScreenWidth, height: deviceScreenHeight)
        // slide in class filter view with animation
        UIView.animate(withDuration: filterPopUpViewSlideDuration, delay: 0, options: .curveEaseOut, animations: {
            self.backgroundView.backgroundColor = UIColor(hex: "#000000", alpha: 0.5)
            self.view.frame = CGRect(x: 0, y: CGFloat(deviceScreenHeight)-self.filterPopUpViewHeight, width: self.filterPopUpViewWidth, height: self.filterPopUpViewHeight)
        })
    }
    
    // This function slides the class filter view off-screen and fades out the backgroundView
    override func dismissMyself() {
        UIView.animate(withDuration: filterPopUpViewSlideDuration, animations: {
            self.backgroundView.alpha = 0
            self.view.frame = CGRect(x: 0, y: CGFloat(deviceScreenHeight), width: self.filterPopUpViewWidth, height: self.filterPopUpViewHeight)
            
            self.delegateController.headerLabel.textColor = .black
            self.delegateController.headerCaret.textColor = .black
        }, completion : { finished in
            self.backgroundView.frame = CGRect(x: 0, y: deviceScreenHeight, width: deviceScreenWidth, height: deviceScreenHeight)
            self.view.removeFromSuperview()
        })
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
}
1 Answers

The fix is to add popover controller to controllers hierarchy

let popover = FlagPopUpController(delegateController: self)
popover.presentationController?.delegate = self
popover.displayFlagPopUp()
self.addChild(popover)              // << here !!
popover.didMove(toParent: self)     // << here !!

Tested with Xcode 14b5 / iOS 16

Related