Jim
Jim

Reputation: 75

How can I fix a "removeConstraint() not working" problem with my UICollectionView?

Programmatically (without storyboards), I am trying to remove one set of constraints and create a new set whenever the device is rotated. The code I use to remove the constraints executes without error but does not actually remove the constraints. This results in a layout error when the new set of (conflicting) constraints is added.

I am using Xcode 10.1 and Swift 4.2 without storyboards. In my MainViewController, I am creating and placing a UICollectionView named "ticketsView" from functions called by viewDidLoad(). After addChild() and addSubview(), I call a function named applyTicketsViewConstraints() which determines the current orientation of the device and then (is expected to) remove any existing constraints and apply the appropriate set of constraints for the current device orientation.

I override viewWillTransition:toSize:withCoordinator() which calls the same applyTicketsViewConstraints() function from within the coordinator closure whenever the device is rotated.

At app startup, the constraints are correctly applied based on the initial device orientation. The first time the device is rotated 90 degrees, the display also successfully changes, but any rotation after that result in a "[LayoutConstraints] Unable to simultaneously satisfy constraints" error message.

I have tried looping through the existing ticketsView constraints and removing them one at a time and I have tried removing them all at once - but neither approach results in the actual removal of the constraints. They both appear to run successfully, but the constraints are still in place after they complete.

    func createTicketCollectionWindow() {
        ticketsLayout = UICollectionViewFlowLayout()
        ticketsVC = TicketCollectionController(collectionViewLayout: ticketsLayout)
        ticketsVC?.collectionView?.backgroundColor = UIColor.darkGray
        ticketsView = ticketsVC!.view
        ticketsView.translatesAutoresizingMaskIntoConstraints = false
        addChild(ticketsVC!)
        self.view.addSubview(ticketsView)
        applyTicketsViewConstraints()
    }
    func applyTicketsViewConstraints() {
        let safe = self.view.safeAreaLayoutGuide
        let deviceSize = UIScreen.main.bounds.size
        for individualConstraint in ticketsView.constraints {
            print("TicketViewConstraints:   Removing Constraint: \(individualConstraint)" )
            // ticketsView.removeConstraint(individualConstraint)
        }
        ticketsView.removeConstraints(ticketsView.constraints)
        // self.view.removeConstraints(ticketsView.constraints)
        if deviceSize.width < deviceSize.height {
            print("TicketsView Constraint:  Device is in PORTRAIT orientation!")
            ticketsView.leadingAnchor.constraint(equalTo: safe.trailingAnchor, constant: -safe.layoutFrame.width).isActive = true
            ticketsView.trailingAnchor.constraint(equalTo:safe.trailingAnchor, constant: 0 ).isActive = true
            ticketsView.bottomAnchor.constraint(equalTo: safe.bottomAnchor, constant: 0).isActive = true
            ticketsView.heightAnchor.constraint(equalToConstant: safe.layoutFrame.height * 0.75).isActive = true
        } else {
            print("TicketsView Constraint:  Device is in LANDSCAPE orientation!")
            ticketsView.leadingAnchor.constraint(equalTo: safe.trailingAnchor, constant: -safe.layoutFrame.width).isActive = true
            ticketsView.trailingAnchor.constraint(equalTo:safe.trailingAnchor, constant: 0 ).isActive = true
            ticketsView.bottomAnchor.constraint(equalTo: safe.bottomAnchor, constant: 0).isActive = true
            ticketsView.heightAnchor.constraint(equalToConstant: safe.layoutFrame.height * 0.65).isActive = true
        }
        ticketsView.layoutIfNeeded()
    }
    override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
        super.viewWillTransition(to: size, with: coordinator)
        coordinator.animate(alongsideTransition: { (_) in
            self.applyTicketsViewConstraints()
            self.ticketsVC?.collectionView.reloadData()
        }) { (_) in
            self.ticketsView.layoutIfNeeded()
        }
    }

I am expecting that the 4 constraints I am creating in the code above will be removed so that I can recreate them with different values. I can't seem to find a way to remove them (that works). The 2 lines above that are commented out in the code above are there to show that I have also tried them (unsuccessfully).

Upvotes: 0

Views: 603

Answers (3)

Shehata Gamal
Shehata Gamal

Reputation: 100533

You can try

var portrait = [NSLayoutConstraint]()
var landscape = [NSLayoutConstraint]()

let porCon1  = ticketsView.leadingAnchor.constraint(equalTo: safe.trailingAnchor, constant: -safe.layoutFrame.width) 
let porCon2  =  ticketsView.trailingAnchor.constraint(equalTo:safe.trailingAnchor, constant: 0 ) 
let porCon3  = ticketsView.bottomAnchor.constraint(equalTo: safe.bottomAnchor, constant: 0) 
let porCon4  = ticketsView.heightAnchor.constraint(equalToConstant: safe.layoutFrame.height * 0.75) 

portrait = [porCon1,porCon2,porCon3,porCon4]

let landCon1  = ticketsView.leadingAnchor.constraint(equalTo: safe.trailingAnchor, constant: -safe.layoutFrame.width) 
let landCon1  = ticketsView.trailingAnchor.constraint(equalTo:safe.trailingAnchor, constant: 0 ) 
let landCon1  = ticketsView.bottomAnchor.constraint(equalTo: safe.bottomAnchor, constant: 0) 
let landCon1  = ticketsView.heightAnchor.constraint(equalToConstant: safe.layoutFrame.height * 0.65) 

landscape = [landCon1,landCon2,landCo3,landCon4]

func setPortrait(_ por:Bool) {  
    portrait.forEach { $0.isActive = por }
    landscape.forEach { $0.isActive = !por } 
}

Could be also

NSLayoutConstraint.deActivate(portrait)
NSLayoutConstraint.deActivate(landscape)
if por {
   NSLayoutConstraint.activate(portrait)
}
else { 
  NSLayoutConstraint.activate(landscape)
}

Upvotes: 2

Jim
Jim

Reputation: 75

The first 2 answers that were posted combined to give me the answer I needed. @Sh_Khan instructed me to use an NSLayoutConstraint array. @Wurzel pointed out that I should be using the NSLayoutConstraint.activate() and .deactivate() functions to enable/disable the entire group of constraints all at once.

So, the key was to add all of the portrait constraints to one array, add all of the landscape constraints to another array and then every time the orientation changes, I call the deactivate function on both arrays and then re-create and activate the appropriate array.

(The safeAreaLayoutGuide changes when the device orientation changes which is why I was unable to just pre-create both sets of constraints in advance. I also wanted a solution that would work if the constraints were to become more dynamic - for example responding to a user resizing the display area)

My original code now becomes:

    func applyTicketsViewConstraints() {
        let safe = self.view.safeAreaLayoutGuide
        let deviceSize = UIScreen.main.bounds.size

        NSLayoutConstraint.deactivate(portraitTicketsViewConstraints)
        NSLayoutConstraint.deactivate(landscapeTicketsViewConstraints)

        if deviceSize.width < deviceSize.height {
            print("TicketsView Constraint:  Device Size is in PORTRAIT orientation!")
            portraitTicketsViewConstraints = [
                ticketsView.leadingAnchor.constraint(equalTo: safe.trailingAnchor, constant: -safe.layoutFrame.width),
                ticketsView.trailingAnchor.constraint(equalTo: safe.trailingAnchor, constant: 0 ),
                ticketsView.bottomAnchor.constraint(equalTo: safe.bottomAnchor, constant: 0),
                ticketsView.heightAnchor.constraint(equalToConstant: safe.layoutFrame.height * 0.75)
            ]
            NSLayoutConstraint.activate(portraitTicketsViewConstraints)
        } else {
            print("TicketsView Constraint:  Device Size is in LANDSCAPE orientation!")
            landscapeTicketsViewConstraints = [
                ticketsView.leadingAnchor.constraint(equalTo: safe.trailingAnchor, constant: -safe.layoutFrame.width),
                ticketsView.trailingAnchor.constraint(equalTo: safe.trailingAnchor, constant: 0 ),
                ticketsView.bottomAnchor.constraint(equalTo: safe.bottomAnchor, constant: 0),
                ticketsView.heightAnchor.constraint(equalToConstant: safe.layoutFrame.height * 0.65)
            ]
            NSLayoutConstraint.activate(landscapeTicketsViewConstraints)
        }
        ticketsView.layoutIfNeeded()
    }
}

Upvotes: 0

Wurzel
Wurzel

Reputation: 13

I believe that you have to work with activate/deactivate constrains, example:

Create variables for them and set the constraints values in your setupConstrainsMethod. var notRotatedYConstraint: NSLayoutConstraint!

        var rotatedYConstraint: NSLayoutConstraint!

in viewdidload: NSLayoutConstraint.activate([notRotatedYConstraint])

In your Function for when the iPhone is rotated. NSLayoutConstraint.deactivate([notRotatedYConstraint]) NSLayoutConstraint.activate([rotatedYConstraint])

Upvotes: 0

Related Questions