0

I am using UIContextMenuConfiguration for actions on a collection view cell. Everything works exactly as expected, however if my cell has (de)activated constraints from nib, it refreshes upon long press.

To demonstrate the problem, I have created a new project, with a collectionView in the storyboard. A custom cell in the collection view has a label with two constraints, a constraint pinning it to the bottom of the cell (initially active), and the other aligns it in the center (initially disabled).

Here's my code,

import UIKit

class ViewController: UIViewController, UICollectionViewDelegate, UICollectionViewDataSource, UICollectionViewDelegateFlowLayout {
    
    func numberOfSections(in collectionView: UICollectionView) -> Int {
        return 1
    }
    func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        return 10
    }
    
    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cell", for: indexPath) as! CollectionViewCell
        cell.configure()
        return cell
    }
    
    func collectionView(_ collectionView: UICollectionView, contextMenuConfigurationForItemAt indexPath: IndexPath, point: CGPoint) -> UIContextMenuConfiguration? {
        return UIContextMenuConfiguration.init(identifier: nil, previewProvider: nil) { (array) -> UIMenu? in
            return UIMenu(title: "Hello", image: nil, identifier: nil, options: .destructive, children: [UIAction(title: "Share", image: UIImage(systemName: "tray.and.arrow.up"), identifier: nil) { _ in
                print("HelloWorld!")
              }])
        }
    }
    
    func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
        return CGSize(width: 265, height: 128)
    }
}

class CollectionViewCell: UICollectionViewCell {

    @IBOutlet weak var bottomConstraint: NSLayoutConstraint!
    @IBOutlet weak var centerConstraint: NSLayoutConstraint!
    
    override func awakeFromNib() {
        super.awakeFromNib()
        // Initialization code
    }

    func configure() {
        bottomConstraint.isActive = false
        centerConstraint.isActive = true
    }
}

Initial state Context menu presentation After context menu dismissal

MMujtabaRoohani
  • 483
  • 4
  • 19
  • You need to provide a little more detail. Quick test with your code, and I'm not seeing the results you are getting. – DonMag Mar 31 '21 at 13:18
  • Oh, thanks for letting me know, is the information sufficient now? – MMujtabaRoohani Mar 31 '21 at 14:04
  • Do you ***want*** the label to drop to the bottom when the context menu is shown? – DonMag Mar 31 '21 at 14:29
  • No I don't (I want it to remain as it was before the context menu presentation) but when the context menu is shown, it seems that the framework implicitly reactivates the constraint that I have de activated for some reason. – MMujtabaRoohani Mar 31 '21 at 19:42
  • I think you're asking for trouble with that approach. Setting `.isActive` on Storyboard created constraints is generally not a good idea. What are you trying to do that would require that? – DonMag Mar 31 '21 at 20:49
  • Okay, so I am developing a chatting screen, and trying to achieve a whatsapp like behavior i.e. for a large message, the time label sticks to the bottom and for a shorter message it aligns itself in the center. There are other more complex behaviors in case of media attachments without caption that I am trying to achieve using this activating and deactivating constraints. I would love to know any better way of doing it, or at least why `.isActive` on storyboard constraints is not a good idea. – MMujtabaRoohani Apr 01 '21 at 06:52

1 Answers1

1

The issue appears to be directly related to:

  • two constraints created in Storyboard which would conflict with each other
  • setting one constraint to Not Installed
  • de-activating the installed constraint and activating the not-installed constraint

When initializing UIContextMenuConfiguration with previewProvider: nil - which tells UIKit to auto-generate a preview view - the "installed" states of the constraints are reset.

One way to get around it:

Instead of marking the Center constraint "not installed" give it a Priority of 998, and give the Bottom constraint a Priority of 999 (that will prevent IB from complaining).

You can then keep your configure() func as:

func configure() {
    bottomConstraint.isActive = false
    centerConstraint.isActive = true
}

and the label's centerY constraint will remain active in both the cell and in the context menu's preview.

However, if you WANT the previewView to look different than the cell, your best bet (and probably a better approach to begin with) is to use a custom previewProvider.

DonMag
  • 69,424
  • 5
  • 50
  • 86
  • When initializing `UIContextMenuConfiguration` with `previewProvider: nil` - which tells `UIKit` to auto-generate a preview view - the "installed" states of the constraints are reset. Can you elaborate on this, I mean how does UIKit keeps everything in place but only resets the 'installed' states of constraints. Also, the solution you proposed has a slightly different impact then what I want. The functionality of the `configure` function would not have any impact on the context menu preview and the auto-layout will try to satisfy both constraints simultaneously, resulting in an enlarged label. – MMujtabaRoohani Apr 02 '21 at 06:54
  • Basically, your presumption about the conflicting constraints is slightly misleading, because the constraints might not be conflicting but they are intended to be activated in a mutually exclusive fashion. – MMujtabaRoohani Apr 02 '21 at 06:58
  • *the "installed" states of the constraints are reset* ... this is from observation. Using `Debug View Hierarchy` we can easily see that UIKit is using the 'installed' state of the Storyboard-set constraints. By *"conflicting constraints"* I meant if they are both **active** - it doesn't matter if your code is going to set them mutually exclusive at some point. – DonMag Apr 02 '21 at 14:41
  • 1
    I had asked *"Do you want the label to drop to the bottom when the context menu is shown?"* and you said no... but it sounds like you **do** want the preview to look different than the cell. In that case, you will be way better off using your own `previewProvider` -- or, at least, *not* trying to use un-installed constraints. – DonMag Apr 02 '21 at 14:42
  • Oh no. Sorry for the confusion and Thank you so much for trying to help me out. Basically, I am looking for a context menu that should look exactly like the cell. However the cell can appear in different states (different set of installed constraints). And regarding the "conflicting constraints" thing, I wanted to highlight the fact that in our current example setting both of them *active* would take the view in a different state instead of showing a conflicting constraints warning because UIKit will simultaneously satisfy both of them by increasing the label's height (not the intended output) – MMujtabaRoohani Apr 03 '21 at 06:13
  • OK - it's either "bug" or an "unintended consequence" but you've come across an issue with adding constraints in IB and marking them "not installed." Note that, in your example, if you leave **both** constraints **Installed**, you may get complaints from IB, but you **will** get your desired results at run-time. – DonMag Apr 03 '21 at 12:39