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
totrue
for various elements on the view and on the background view.
Here's what it looks like:
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")
}
}