objective-cautolayoutios8size-classes

Programmatically implementing two different layouts using size classes


I have a four buttons layout. In portrait they should be shown one above the other. In landscape they should be in two columns each with two buttons.

I implement the buttons in code - really simple stuff:

UIButton *btn1 = [[UIButton alloc] init];
[self.view addSubview: btn1]; 

UIButton *btn2 = [[UIButton alloc] init];
[self.view addSubview: btn2]; 

UIButton *btn3 = [[UIButton alloc] init];
[self.view addSubview: btn3]; 

UIButton *btn4 = [[UIButton alloc] init];
[self.view addSubview: btn4]; 

NSDictionary *views = NSDictionaryOfVariableBindings(btn1, btn2, btn3, btn4);

[btn1 setTranslatesAutoresizingMaskIntoConstraints:NO];
[btn2 setTranslatesAutoresizingMaskIntoConstraints:NO];
[btn3 setTranslatesAutoresizingMaskIntoConstraints:NO];
[btn4 setTranslatesAutoresizingMaskIntoConstraints:NO];

// portrait constraints
[self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|-(50)-[btn1]-(50)-|"
                                                                 options:0 metrics:nil views:views]];
[self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|-(50)-[btn2]-(50)-|"
                                                                 options:0 metrics:nil views:views]];
[self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|-(50)-[btn3]-(50)-|"
                                                                 options:0 metrics:nil views:views]];
[self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|-(50)-[btn4]-(50)-|"
                                                                 options:0 metrics:nil views:views]];

[self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:[btn1]-[btn2]-[btn3]-[btn4]-(50)-|"
                                                                 options:0 metrics:nil views:views]];

This is obviously the setup for portrait layout. I would used to have determined the device and its orientation to make specific case for iPad and iPhone in there respective orientations. But now we are supposed to use size classes. How can I determine if the size class is "compact"... and thus set the appropriate constraints?


Solution

  • In the meantime I have found a good solution. Since this question has so many upvotes, I thought I would quickly describe it. I was inspired to this solution by a WWDC session.

    I have moved on to Swift so please excuse that the code will be in swift - the concept is however the same for Obj-C.

    You start out by declaring three constraint arrays:

     // Constraints
     private var compactConstraints: [NSLayoutConstraint] = []
     private var regularConstraints: [NSLayoutConstraint] = []
     private var sharedConstraints: [NSLayoutConstraint] = []
    

    And then you fill the constraints accordingly. You can i.e. do this in a separate function that you call from viewDidLoad or you do it in viewDidLoad directly.

    sharedConstraints.append(contentsOf: [
         btnStackView.centerXAnchor.constraint(equalTo: self.view.centerXAnchor),
        ...
    ])
    
    compactConstraints.append(contentsOf: [
         btnStackView.widthAnchor.constraint(equalTo: self.view.widthAnchor, multiplier: 0.7),
        ...
    ])
    
    regularConstraints.append(contentsOf: [
         btnStackView.widthAnchor.constraint(equalTo: self.view.widthAnchor, multiplier: 0.4),
        ...
    ])
    

    The important part is switching between the size classes and activating/deactivating the appropriate constraints.

    override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
    
        super.traitCollectionDidChange(previousTraitCollection)
    
        if (!sharedConstraints[0].isActive) {
           // activating shared constraints
           NSLayoutConstraint.activate(sharedConstraints)
        }
    
    
        if traitCollection.horizontalSizeClass == .compact && traitCollection.verticalSizeClass == .regular {
            if regularConstraints.count > 0 && regularConstraints[0].isActive {
                NSLayoutConstraint.deactivate(regularConstraints)
            }
            // activating compact constraints
            NSLayoutConstraint.activate(compactConstraints)
        } else {
            if compactConstraints.count > 0 && compactConstraints[0].isActive {
                NSLayoutConstraint.deactivate(compactConstraints)
            }
            // activating regular constraints
            NSLayoutConstraint.activate(regularConstraints)
        }
    }
    

    I know that the constraints don't fit to the ones in the question. But the constraints themselves are irrelevant. The main thing is how one switches between two sets of constraints based on the size class.

    Hope this helps.