1

I have a UIStackView which is changing axis based on its width. It includes two UViews only. There is a very easy setup (can be copy/pasted to default Xcode project):

class ViewController: UIViewController {

    enum DisplayMode {
        case regular
        case compact
    }

    private let stackView = UIStackView(frame: .zero)
    private let firstView = UIView(frame: .zero)
    private let secondView = UIView(frame: .zero)
    private var firstViewWidth: NSLayoutConstraint?
    private var secondViewWidth: NSLayoutConstraint?

    override func viewDidLoad() {
        super.viewDidLoad()

        stackView.translatesAutoresizingMaskIntoConstraints = false
        view.addSubview(stackView)
        NSLayoutConstraint.activate([
            stackView.topAnchor.constraint(equalTo: view.topAnchor),
            stackView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
            stackView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
            ])

        firstView.backgroundColor = .red
        stackView.addArrangedSubview(firstView)
        firstView.heightAnchor.constraint(equalToConstant: 80).isActive = true
        firstViewWidth = firstView.widthAnchor.constraint(equalTo: stackView.widthAnchor, multiplier: 1/3)
        firstViewWidth?.isActive = true

        secondView.backgroundColor = .black
        stackView.addArrangedSubview(secondView)
        secondView.heightAnchor.constraint(equalToConstant: 80).isActive = true
        secondViewWidth = secondView.widthAnchor.constraint(equalTo: stackView.widthAnchor, multiplier: 2/3)
        secondViewWidth?.isActive = true
    }

    override func viewDidLayoutSubviews() {
        super.viewDidLayoutSubviews()
        if view.bounds.width < 400 {
            switchMode(.compact)
        } else {
            switchMode(.regular)
        }
    }
}

private extension ViewController {

    func switchMode(_ mode: DisplayMode) {
        switch mode {
        case .regular:
            stackView.axis = .horizontal
            firstViewWidth?.isActive = true
            secondViewWidth?.isActive = true
        case .compact:
            stackView.axis = .vertical
            firstViewWidth?.isActive = false
            secondViewWidth?.isActive = false
        }
    }
}

It's working just fine but it's giving me nonsense layout error (all constraints in the output seems fine) while changing from compact to regular:

(
    "<NSLayoutConstraint:0x6000002c1c20 UIView:0x7fbd4dc149a0.width == 0.333333*UIStackView:0x7fbd4df02430.width   (active)>",
    "<NSLayoutConstraint:0x6000002c2170 UIView:0x7fbd4dc14d90.width == 0.666667*UIStackView:0x7fbd4df02430.width   (active)>",
    "<NSLayoutConstraint:0x6000002f2080 'UISV-canvas-connection' UIStackView:0x7fbd4df02430.leading == UIView:0x7fbd4dc149a0.leading   (active)>",
    "<NSLayoutConstraint:0x6000002f1c70 'UISV-canvas-connection' H:[UIView:0x7fbd4dc14d90]-(0)-|   (active, names: '|':UIStackView:0x7fbd4df02430 )>",
    "<NSLayoutConstraint:0x6000002f05f0 'UISV-spacing' H:[UIView:0x7fbd4dc149a0]-(0)-[UIView:0x7fbd4dc14d90]   (active)>"
)

Can someone explain why is this happening? Is the dynamic axis a culprit?

Viktor Kucera
  • 6,177
  • 4
  • 32
  • 43

1 Answers1

3

First of All viewDidLayoutSubviews is not good option to handle oriantion changes.Apple recommended method is viewWillTransitionToSize. For more details : What is the "right" way to handle orientation changes in iOS 8?

When you first Run your Code ,Stack View Appear With Vertical Distribution , cases both FirstView and Second View have Following Constraint

FirstView ->

         Leading =  StackView Leading,
         Trailing =  StackView Trailing,
         Top      = StackView Top,
         height   =   80

SecondView - >

            Leading =  StackView Leading    ,
              Top      = FirstView Top,
              Trailing =  StackView Trailing,
              height   =   80

When you rotate Device From Portrait to landscape , Stack View Appear With Horizontal Distribution and make Following Constraint Active.

FirstView.Width = StackView.Width * 1/3

Resulting FirstView Conflict With Your Previous Constraint(StackView Leading,StackView Trailing).because now you have two Width Constraint

  1. (FirstView.leading = StackView Leading & FirstView.trailing = StackView Trailing) == (FirstView Width == StackView.width)
  2. FirstView.Width = StackView.Width * 1/3

Why StackView not Removing one of Conflicted Constraint Automatically?

--> because both Constraint Have High Priority(1000).if we simply Change firstViewWidth to lower(999) and removing secondViewWidth Constraint than your Solution will Work

Try to Replace your Code With Following Code

   import UIKit

class ViewController: UIViewController {

    enum DisplayMode {
        case regular
        case compact
    }

    private let stackView = UIStackView(frame: .zero)
    private let firstView = UIView(frame: .zero)
    private let secondView = UIView(frame: .zero)
    private var firstViewWidth: NSLayoutConstraint?
    private var secondViewWidth: NSLayoutConstraint?

    override func viewDidLoad() {
        super.viewDidLoad()

        stackView.translatesAutoresizingMaskIntoConstraints = false
        view.addSubview(stackView)
        NSLayoutConstraint.activate([
            stackView.topAnchor.constraint(equalTo: view.topAnchor),
            stackView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
            stackView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
            ])

        firstView.backgroundColor = .red
        stackView.addArrangedSubview(firstView)
        firstView.heightAnchor.constraint(equalToConstant: 80).isActive = true
        firstViewWidth = firstView.widthAnchor.constraint(equalTo: stackView.widthAnchor, multiplier: 1/3)
        firstViewWidth?.priority = UILayoutPriority(rawValue: 999)
        firstViewWidth?.isActive = true

        secondView.backgroundColor = .black
        stackView.addArrangedSubview(secondView)
        secondView.heightAnchor.constraint(equalToConstant: 80).isActive = true


        if self.view.bounds.width < 400 {
            self.switchMode(.compact)
        } else {
            self.switchMode(.regular)
        }
    }

    override func viewDidLayoutSubviews() {
        super.viewDidLayoutSubviews()

    }

    override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
        super.viewWillTransition(to: size, with: coordinator)


        coordinator.animate(alongsideTransition: { (context) in

        }) { (context) in
            if self.view.bounds.width < 400 {
                self.switchMode(.compact)
            } else {
                self.switchMode(.regular)
            }
        }

    }

}

private extension ViewController {

    func switchMode(_ mode: DisplayMode) {
        switch mode {
        case .regular:
            stackView.axis = .horizontal
            firstViewWidth?.isActive = true

        case .compact:
            stackView.axis = .vertical
            firstViewWidth?.isActive = false
        }
    }
}
Bhavesh.iosDev
  • 924
  • 9
  • 27
  • 1
    Thanks for helping me out! Although I can see the trick - removing the arranged subviews and adding them back - I consider it workaround not proper solution. I cannot make it an accepted answer. I still didn't get the core of the answer - why stackview axis do not remove that constraint you are talking about as a conflicting one. Once stackView is the horizontal one it's not supposed to have that firstView.trailing = stackView.trailing constraint. – Viktor Kucera Nov 06 '18 at 08:54
  • hi @ViktorKucera thanks for pointing out me in right direction.StackView not Removing conflicted Constraint Automatically because both Conflicted Constraint have high Priority if you simply change firstViewWidth to lower priority it will works.see my edited Answer. – Bhavesh.iosDev Nov 06 '18 at 11:27