0

Using iOS14.0.1, Swift5.3, Xcode12.0.1,

I am desperate with this problem:

With iOS12 and iOS13, my App was working nicely.

But now with iOS14, no UIButton, no UISwitch, no segue - nothing is working in my Modal ViewController.

I have no idea what Apple changed in UIKit for this problem to occur ???? !!!!

Please help!

My Modal ViewController is presented as follows:

@objc func settingsBtnPressed(_ sender: UIButton) {
        
    let settingsVC = SettingsViewController()
    settingsVC.backDelegate = self

    let navController = UINavigationController(rootViewController: settingsVC)
    navController.navigationBar.barStyle = .black
    navController.navigationBar.tintColor = .white
    navController.navigationBar.prefersLargeTitles = true
    navController.navigationBar.backgroundColor = UIColor.clear
    navController.presentationController?.delegate = self

    self.present(navController, animated: true)
}

class SettingsViewController: UICollectionViewController, UICollectionViewDelegateFlowLayout {
    
    let cellId = "cellID"
    
    init() {
        let collectionViewFlowLayout = UICollectionViewFlowLayout()
        collectionViewFlowLayout.estimatedItemSize = CGSize(width: UIScreen.main.bounds.width, height: 1)
        super.init(collectionViewLayout: collectionViewFlowLayout)
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    override func viewDidLoad() {
        super.viewDidLoad()        
        // ...                        
        collectionView.register(SettingsCollectionViewCell.self, forCellWithReuseIdentifier: cellId)
    }
        
    override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        return 1
    }
    
    override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {

        let cell = collectionView.dequeueReusableCell(withReuseIdentifier: cellId, for: indexPath) as! SettingsCollectionViewCell
        cell.setup(titlesFirstSection: titlesFirstSection, iconsFirstSection: iconsFirstSection, onStatesFirstSection: onStatesFirstSection, )
        return cell
    }
}
class SettingsCollectionViewCell: UICollectionViewCell {
    
    var SettingsStackView = UIStackView()
    var attries: UICollectionViewLayoutAttributes!
    var myContentHeight: CGFloat = 0.0
    
    var firstSettingsSectionView: MenuSwitchListSectionView?
    
    let elementHeight: CGFloat = 44.0
    let separatorLineThickness: CGFloat = 1.0
    let sectionViewBackgroundColor = ImageConverter.UIColorFromRGB(0x2C2C2E, alpha: 1.0)
    
    override init(frame: CGRect) {
        super.init(frame: frame)
    }
    
    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
        
    func setup(titlesFirstSection: [String],
                iconsFirstSection: [UIImage?]?,
             onStatesFirstSection: [Bool]) {
                
        myContentHeight = UIScreen.main.bounds.height - 120
                                
        initFirstSettingsSectionView(titles: titlesFirstSection, icons: iconsFirstSection, onStates: onStatesFirstSection)
        
        SettingsStackView = VerticalStackView(arrangedSubviews: [
            UIView(),
            firstSettingsSectionView! as UIView,
            UIView()
        ], spacing: 10.0, alignment: .center )
        
        contentView.addSubview(SettingsStackView)
        contentView.isUserInteractionEnabled = true
        
        setConstraints(nrOfItemsFirstSection: titlesFirstSection.count)
    }
    
    fileprivate func initFirstSettingsSectionView(titles: [String], icons: [UIImage?]?, onStates: [Bool]) {

        firstSettingsSectionView = MenuSwitchListSectionView(frame: .zero,
                                  elementHeight: elementHeight,
                         separatorLineThickness: separatorLineThickness,
                                backgroundColor: sectionViewBackgroundColor,
                                         titles: titles,
                                          icons: icons,
                                       onStates: onStates
        )
        firstSettingsSectionView?.switchMutatedDelegate = self
        firstSettingsSectionView?.assignDelegate()
    }
    
    fileprivate func setConstraints(nrOfItemsFirstSection: Int) {

        contentView.translatesAutoresizingMaskIntoConstraints = false
        SettingsStackView.translatesAutoresizingMaskIntoConstraints = false
        firstSettingsSectionView?.translatesAutoresizingMaskIntoConstraints = false
        
        SettingsStackView.anchor(top: safeAreaLayoutGuide.topAnchor, leading: contentView.leadingAnchor, bottom: nil, trailing: contentView.trailingAnchor)
                        
        let totalHeightFirstSection: CGFloat = elementHeight * CGFloat(nrOfItemsFirstSection) + separatorLineThickness * CGFloat(nrOfItemsFirstSection - 1)
        firstSettingsSectionView?.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 8.0).isActive = true
        firstSettingsSectionView?.heightAnchor.constraint(equalToConstant: totalHeightFirstSection).isActive = true
        firstSettingsSectionView?.widthAnchor.constraint(equalToConstant: contentView.bounds.width - 16.0).isActive = true
    }
class MenuSwitchElementStackView: UIStackView {
    
    weak var switchMutatedDelegate: SwitchMutatedDelegate?

    let imgViewWidth: CGFloat = 23.0
        
    override init(frame: CGRect) {
        super.init(frame: frame)
    }
    
    convenience init(frame: CGRect, text: String, icon: UIImage?, onState: Bool, switchID: Int) {
        self.init(frame: frame)
        composeStackView(text: text, icon: icon, onState: onState, switchID: switchID)
    }
    
    required init(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    fileprivate func composeStackView(text: String, icon: UIImage?, onState: Bool, switchID: Int) {
        
        // StackView properties
        axis = .horizontal
        alignment = .center
        spacing = 23.0
        
        var iconImgView: UIImageView?
        
        // create Icon
        if let icon = icon {
            iconImgView = UIImageView()
            iconImgView?.image = icon
            iconImgView?.contentMode = .scaleAspectFit
        }
        
        // create Title
        let titleLabel = UILabel()
        titleLabel.font = AppConstants.Font.ListSectionElementTitleFont
        titleLabel.text = text
        titleLabel.textColor = .white
        titleLabel.tintColor = .white
        titleLabel.heightAnchor.constraint(equalToConstant: 19.0).isActive = true
        
        // create choice TextLbl
        let mySwitch = UISwitch()
        mySwitch.tag = switchID
        mySwitch.setOn(onState, animated: false)
        mySwitch.addTarget(self, action: #selector(MenuSwitchElementStackView.switchChanged(_:)), for: UIControl.Event.valueChanged)
        
        // text stackView creation
        if let iconImgView = iconImgView {
            addArrangedSubview(iconImgView)
        }
        addArrangedSubview(titleLabel)
        addArrangedSubview(mySwitch)
        // set Constraints
        setConstraints(iconImgView, titleLabel, mySwitch)
    }
    
    @objc func switchChanged(_ mySwitch: UISwitch) {
        switchMutatedDelegate?.switchChanged(isOn: mySwitch.isOn, switchID: mySwitch.tag)
    }
    
    fileprivate func setConstraints(_ iconImgView: UIImageView?, _ titleLabel: UILabel, _ mySwitch: UISwitch) {
        
        translatesAutoresizingMaskIntoConstraints = false
        iconImgView?.translatesAutoresizingMaskIntoConstraints = false
        titleLabel.translatesAutoresizingMaskIntoConstraints = false
        mySwitch.translatesAutoresizingMaskIntoConstraints = false
        
        iconImgView?.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 0.0).isActive = true
        iconImgView?.widthAnchor.constraint(equalToConstant: 24.0).isActive = true
        iconImgView?.centerYAnchor.constraint(equalTo: centerYAnchor).isActive = true

        titleLabel.centerYAnchor.constraint(equalTo: centerYAnchor).isActive = true
        
        mySwitch.centerYAnchor.constraint(equalTo: centerYAnchor).isActive = true
        mySwitch.leadingAnchor.constraint(equalTo: trailingAnchor, constant: -64.0).isActive = true
    }
}
class MenuSwitchListSectionView: UIView {
    
    var sectionElementsStackView: MenuSwitchListSectionWithElementsStackView?
    weak var switchMutatedDelegate: SwitchMutatedDelegate?
    
    override init(frame: CGRect) {
        super.init(frame: frame)
    }
    
    convenience init(frame: CGRect,
             elementHeight: CGFloat,
    separatorLineThickness: CGFloat,
           backgroundColor: UIColor,
                    titles: [String],
                     icons: [UIImage?]?,
                  onStates: [Bool]) {
        self.init(frame: frame)
        composeView(elementHeight: elementHeight, separatorLineThickness: separatorLineThickness, backgroundColor: backgroundColor, titles: titles, icons: icons, onStates: onStates)
    }
    
    required init(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    func assignDelegate() {
        self.sectionElementsStackView?.switchMutatedDelegate = switchMutatedDelegate
        sectionElementsStackView?.assignDelegate()
    }
    
    fileprivate func composeView(elementHeight: CGFloat, separatorLineThickness: CGFloat, backgroundColor: UIColor, titles: [String], icons: [UIImage?]?, onStates: [Bool]) {

        // create background View
        let totalHeight: CGFloat = elementHeight * CGFloat(titles.count) + separatorLineThickness * CGFloat(titles.count - 1)
        let backGroundView = ListSectionBackgroundView(frame: .zero, height: totalHeight, backgroundColor: backgroundColor)
               
        // create stackView
        sectionElementsStackView = MenuSwitchListSectionWithElementsStackView(frame: CGRect(x: 0.0, y: 0.0, width: UIScreen.main.bounds.width, height: totalHeight), elementHeight: elementHeight, separatorLineThickness: separatorLineThickness, titles: titles, icons: icons, onStates: onStates)
        
        // add background and stackView to self
        addSubview(backGroundView)
        addSubview(sectionElementsStackView!)
        
        // set Constraints
        translatesAutoresizingMaskIntoConstraints = false
        backGroundView.translatesAutoresizingMaskIntoConstraints = false
        sectionElementsStackView?.translatesAutoresizingMaskIntoConstraints = false
        
        backGroundView.centerYAnchor.constraint(equalTo: centerYAnchor).isActive = true
        backGroundView.leadingAnchor.constraint(equalTo: leadingAnchor).isActive = true
        backGroundView.trailingAnchor.constraint(equalTo: trailingAnchor).isActive = true
        backGroundView.heightAnchor.constraint(equalToConstant: totalHeight).isActive = true
        sectionElementsStackView?.centerYAnchor.constraint(equalTo: centerYAnchor).isActive = true
        sectionElementsStackView?.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 16.0).isActive = true
        sectionElementsStackView?.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -10.0).isActive = true
    }
}
class MenuSwitchListSectionWithElementsStackView: UIStackView {
    
    weak var switchMutatedDelegate: SwitchMutatedDelegate?
    var elementStackView: MenuSwitchElementStackView?
    var elements: [MenuSwitchElementStackView]?
    var titles: [String]? // needed for gesture callback
    
    override init(frame: CGRect) {
        super.init(frame: frame)
    }
    
    convenience init(frame: CGRect,
             elementHeight: CGFloat,
    separatorLineThickness: CGFloat,
                    titles: [String],
                     icons: [UIImage?]?,
                  onStates: [Bool]) {
        self.init(frame: frame)
        self.titles = titles // needed for gesture callback
        composeStackView(elementHeight: elementHeight, separatorLineThickness: separatorLineThickness, titles: titles, icons: icons, onStates: onStates)
    }
    
    required init(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    func assignDelegate() {
        if let elements = elements {
            for element in elements {
                element.switchMutatedDelegate = switchMutatedDelegate
            }
        }
    }
    
    fileprivate func composeStackView(elementHeight: CGFloat, separatorLineThickness: CGFloat, titles: [String], icons: [UIImage?]?, onStates: [Bool]) {

        // stackView properties
        axis = .vertical
        alignment = .leading
        spacing = 0.0
        
        // create stackView elements
        elements = (0..<titles.count).map { (idx) -> MenuSwitchElementStackView in
            if icons?.count ?? 0 > 0 {
                elementStackView = MenuSwitchElementStackView(frame: .zero, text: titles[idx], icon: icons?[idx], onState: onStates[idx], switchID: idx)
            } else {
                elementStackView = MenuSwitchElementStackView(frame: .zero, text: titles[idx], icon: nil, onState: onStates[idx], switchID: idx)
            }
            elementStackView?.isUserInteractionEnabled = true
            elementStackView?.tag = idx
            return elementStackView!
        }
        
        var singleLines = [UIView]()
        let lineThickness: CGFloat = separatorLineThickness
        
        // compose the stackView
        if let elements = elements {
            for (idx, view) in elements.enumerated() {
                
                // add element to stackView
                addArrangedSubview(view)
                
                if idx < titles.count - 1 {
                    // add single Line to stackView
                    let singleLineView = UIView(frame: CGRect(x: 0, y: 0, width: UIScreen.main.bounds.width, height: lineThickness))
                    singleLineView.backgroundColor = ImageConverter.UIColorFromRGB(0x606060, alpha: 1.0)
                    singleLines.append(singleLineView)
                    
                    addArrangedSubview(singleLineView)
                }
            }
        }
        
        // set constraints
        translatesAutoresizingMaskIntoConstraints = false

        if let elements = elements {
            for (idx, element) in elements.enumerated() {
                element.translatesAutoresizingMaskIntoConstraints = false
                element.leadingAnchor.constraint(equalTo: leadingAnchor).isActive = true
                element.trailingAnchor.constraint(equalTo: trailingAnchor).isActive = true
                element.heightAnchor.constraint(equalToConstant: elementHeight).isActive = true
                if idx < titles.count - 1 {
                    singleLines[idx].translatesAutoresizingMaskIntoConstraints = false
                    singleLines[idx].topAnchor.constraint(equalTo: element.bottomAnchor).isActive = true
                    singleLines[idx].heightAnchor.constraint(equalToConstant: lineThickness).isActive = true
                    if icons?.count ?? 0 > 0 {
                        if let _ = icons?[idx] {
                            singleLines[idx].leadingAnchor.constraint(equalTo: leadingAnchor, constant: 39.0).isActive = true
                        }
                    } else {
                        singleLines[idx].leadingAnchor.constraint(equalTo: leadingAnchor).isActive = true
                    }
                    singleLines[idx].trailingAnchor.constraint(equalTo: trailingAnchor, constant: 9.0).isActive = true
                }
            }
        }
    }
}
class ListSectionBackgroundView: UIView {
    
    override init(frame: CGRect) {
        super.init(frame: frame)
    }
    
    convenience init(frame: CGRect, height: CGFloat = 60.0, backgroundColor: UIColor) {
        self.init(frame: frame)
        composeView(height: height, backgroundColor: backgroundColor)
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    fileprivate func composeView(height: CGFloat, backgroundColor: UIColor) {
        
        // View properties
        self.backgroundColor = backgroundColor
        self.layer.cornerRadius = 10.0
        self.layer.masksToBounds = true
        self.clipsToBounds = true
        
        // set Constraints
        setConstraints(height)
    }
    
    fileprivate func setConstraints(_ height: CGFloat) {
        self.translatesAutoresizingMaskIntoConstraints = false
        self.heightAnchor.constraint(equalToConstant: height).isActive = true
    }
}
iKK
  • 6,394
  • 10
  • 58
  • 131
  • I'm going to guess that you are adding the button and switch directly to the cell. Unfortunately you didn't show SettingsCollectionViewCell so that's just a guess. – matt Oct 05 '20 at 00:08
  • I added `SettingsCollectionViewCell` to the original question - see above... (...i.e. it is quite verbose - but I hope you can see what is causing the iOS14 issue...). Thank you for any help - I am so desperate with this :/ – iKK Oct 05 '20 at 00:15
  • I'm sorry but I still don't see the buttons and switches that are not working. The only thing I can think of now is that they might be outside their superview. You can check that by studying the View Debugger. – matt Oct 05 '20 at 00:32
  • The View-Debugger looks fine. I do not see anything wrong there. I added more of the view composition above in the original question. At first, since rather a lot of code, I did not intend to present all of it. But maybe to find the bug, it makes more sense now. Again, in iOS12 and iOS13 everything works perfectly. It is only iOS14 that causes the problem. – iKK Oct 05 '20 at 00:36
  • So is the problem that, say, a UISwitch is untouchable, or that it is touchable but switching it has no effect? – matt Oct 05 '20 at 00:45
  • Touching the UISwitch does not do anything. Especially, the target-action (`@objc func switchChanged(...) `) does not get called. – iKK Oct 05 '20 at 00:47
  • But the switch does physically switch? – matt Oct 05 '20 at 00:48
  • No it does not switch at all – iKK Oct 05 '20 at 00:49
  • OK so it's untouchable. Well, I've given the reasons I know why that can happen: (1) something is in front of it, or (2) it is outside its superview (at some depth up the view hierarchy). You say that everything looks fine in the View Debugger but I would venture that that is not the case. You have to really study the whole view hierarchy up from the switch, one superview at a time, looking at the sizes and at the placement of subview within superview, to track this down. – matt Oct 05 '20 at 00:51
  • as to 1) - nothing is in front of it. It is the top element. as to 2) I checked and I only see a view-hierarchy that looks fine (at least, there are boxes for every layer behind that are bigger than the UISwitch. But I investigate this further. I'd love to screen share. – iKK Oct 05 '20 at 01:01
  • Walk up the view hierarchy again and check the User Interaction Enabled for every single view. One view whose `isUserInteractionEnabled` is false will turn off user interaction for all of its subviews. – matt Oct 05 '20 at 01:05
  • very good point - I check that. Thank you very much. – iKK Oct 05 '20 at 01:11
  • The `isUserInteractionEnabled` is ON throughout the entire stack. I am clueless. The strange thing is that I use the same `MenuSwitchListSectionWithElementsStackView` somewhere else and there it works fine. I am pretty sure there is something wrong in iOS14. – iKK Oct 05 '20 at 01:18
  • 1
    Yeah, they did change some stuff about cells and stack views so that's what I focused on. Sorry not to be able to help from here. – matt Oct 05 '20 at 01:27
  • I thank you for your good ideas. I will try to investigate further what you mentioned. And in the meantime, I also wait for Apple's next iOS update. – iKK Oct 05 '20 at 01:32

1 Answers1

1

I finally found a solution.

It was the UIView-extension "anchor" in my code that no longer works under iOS14 for some reason.

Here I describe the problem more precisely...

Therefore, if I use the extension and do the following - then any UIControls in my ViewHierarchy do not work (i.e. no target-actions, switch-didChanges, etc will no longer kick-in)

SettingsStackView.anchor(top: safeAreaLayoutGuide.topAnchor, leading: contentView.leadingAnchor, bottom: contentView.bottomAnchor, trailing: contentView.trailingAnchor)

However, if I do it without the UIView-extension, then everything works fine:

SettingsStackView.translatesAutoresizingMaskIntoConstraints = false
SettingsStackView.topAnchor.constraint(equalTo: contentView.safeAreaLayoutGuide.topAnchor).isActive = true
SettingsStackView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor).isActive = true
SettingsStackView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor).isActive = true
SettingsStackView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor).isActive = true

Maybe somebody can tell me why this new behaviour came up with iOS14 ???

iKK
  • 6,394
  • 10
  • 58
  • 131