How to Override Trait Collection for Initial Uiviewcontroller? (With Storyboard)

How to override trait collection for initial UIViewController? (with Storyboard)

Ok, I wish there was another way around this, but for now I just converted code from the Apple example to Swift and adjusted it to use with Storyboards.

It works, but I still believe it is an awful way to archive this goal.

My TraitOverride.swift:

import UIKit

class TraitOverride: UIViewController {

required init(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}

var forcedTraitCollection: UITraitCollection? {
didSet {
updateForcedTraitCollection()
}
}

override func viewDidLoad() {
setForcedTraitForSize(view.bounds.size)
}

var viewController: UIViewController? {
willSet {
if let previousVC = viewController {
if newValue !== previousVC {
previousVC.willMoveToParentViewController(nil)
setOverrideTraitCollection(nil, forChildViewController: previousVC)
previousVC.view.removeFromSuperview()
previousVC.removeFromParentViewController()
}
}
}

didSet {
if let vc = viewController {
addChildViewController(vc)
view.addSubview(vc.view)
vc.didMoveToParentViewController(self)
updateForcedTraitCollection()
}
}
}

override func viewWillTransitionToSize(size: CGSize, withTransitionCoordinator coordinator: UIViewControllerTransitionCoordinator!) {
setForcedTraitForSize(size)
super.viewWillTransitionToSize(size, withTransitionCoordinator: coordinator)
}

func setForcedTraitForSize (size: CGSize) {

let device = traitCollection.userInterfaceIdiom
var portrait: Bool {
if device == .Phone {
return size.width > 320
} else {
return size.width > 768
}
}

switch (device, portrait) {
case (.Phone, true):
forcedTraitCollection = UITraitCollection(horizontalSizeClass: .Regular)
case (.Pad, false):
forcedTraitCollection = UITraitCollection(horizontalSizeClass: .Compact)
default:
forcedTraitCollection = nil
}
}

func updateForcedTraitCollection() {
if let vc = viewController {
setOverrideTraitCollection(self.forcedTraitCollection, forChildViewController: vc)
}
}

override func viewWillAppear(animated: Bool) {
super.viewWillAppear(animated)
performSegueWithIdentifier("toSplitVC", sender: self)
}

override func prepareForSegue(segue: UIStoryboardSegue!, sender: AnyObject!) {
if segue.identifier == "toSplitVC" {
let destinationVC = segue.destinationViewController as UIViewController
viewController = destinationVC
}
}

override func shouldAutomaticallyForwardAppearanceMethods() -> Bool {
return true
}

override func shouldAutomaticallyForwardRotationMethods() -> Bool {
return true
}
}

To make it work you need to add a new UIViewController on the storyboard and made it the initial. Add show segue from it to your real controller like this:
storyboard

You need to name the segue "toSplitVC":
segue name

and set initial controller to be TraitOverride:
assign controller

Now it should work for you too. Let me know if you find a better way or any flaws in this one.

Override separateSecondaryViewControllerForSplitViewController: for specific UIViewController in monotouch

It looks like this method is not correctly bound in UIViewController in Xamarin.iOS.

It is an extension method on the UIKit.UISplitViewController_UIViewController and defined as:

[Export ("separateSecondaryViewControllerForSplitViewController:"), Availability (Introduced = Platform.iOS_8_0), CompilerGenerated]
public static UIViewController SeparateSecondaryViewControllerForSplitViewController (this UIViewController This, UISplitViewController splitViewController);

You should be able to put the method into your view controller subclass like this:

[Export ("separateSecondaryViewControllerForSplitViewController:")]
public UIViewController SeparateSecondaryViewControllerForSplitViewController (UISplitViewController splitViewController)
{
return null; // return whatever you need to return
}

Exporting it manually should do the trick. Let me know if it works.

view.traitCollection.horizontalSizeClass returning undefined (0) in viewDidLoad

The problem is that viewDidLoad is too soon to be asking about a view's trait collection. This is because the trait collection of a view is a feature acquired from the view hierarchy, the environment in which it finds itself. But in viewDidLoad, the view has no environment: it is not in in the view hierarchy yet. It has loaded, meaning that it exists: the view controller now has a view. But it has not been put into the interface yet, and it will not be put into the interface until viewDidAppear:, which comes later in the sequence of events.

However, the view controller also has a trait collection, and it does have an environment: by the time viewDidLoad is called, the view controller is part of the view controller hierarchy. Therefore the simplest (and correct) solution is to ask for the traitCollection of self, not of view. Just say self.traitCollection where you now have view.traitCollection, and all will be well.

(Your solution, asking the screen for its trait collection, may happen to work, but it is not reliable and is not the correct approach. This is because it is possible for the parent view controller to alter the trait collection of its child, and if you bypass the correct approach and ask the screen, directly, you will fail to get the correct trait collection.)

When trait collection changes, constraint conflicts arise as though the stackview axis didn't change

Couple notes...

  • There is an inherent issue with "nested" stack views causing constraint conflicts. This can be avoided by setting the priority on affected elements to 999 (instead of the default 1000).
  • Your layout becomes a bit complex... Labels "attached" to text fields; elements needing to be on two "lines" in portrait orientation or one "line" in landscape; one element of a "multi-element line" having a different height (the stepper); and so on.
  • To get your "field2" and "field3" to be equal size, you need to constrain their widths to be equal, even though they are not subviews of the same subview. This is perfectly valid, as long as they are descendants of the same view hierarchy.
  • Stackviews are great --- except when they're not. I would almost suggest using constraints only. You need to add more constraints, but it might avoid some issues with stack views.

However, here is an example that should get you on your way.

I've added a UIStackView subclass named LabeledFieldStackView ... it sets up the Label-above-Field in a stack view. Somewhat cleaner than mixing it in within all the other layout code.

class LabeledFieldStackView: UIStackView {

var theLabel: UILabel = {
let v = UILabel()
v.translatesAutoresizingMaskIntoConstraints = false
return v
}()

var theField: UITextField = {
let v = UITextField()
v.translatesAutoresizingMaskIntoConstraints = false
v.borderStyle = .roundedRect
return v
}()

convenience init(with labelText: String, fieldText: String, verticalGap: CGFloat) {

self.init()

axis = .vertical
alignment = .fill
distribution = .fill
spacing = 2

addArrangedSubview(theLabel)
addArrangedSubview(theField)

theLabel.text = labelText
theField.text = fieldText

self.translatesAutoresizingMaskIntoConstraints = false

}

}

class LargentViewController: UIViewController {

var rootStack: UIStackView!

var fieldStackView1: LabeledFieldStackView!
var fieldStackView2: LabeledFieldStackView!
var fieldStackView3: LabeledFieldStackView!
var fieldStackView4: LabeledFieldStackView!

var stepper: UIStepper!

var fieldAndStepperStack: UIStackView!

var twoLineStack: UIStackView!

var fieldAndStepperStackWidthConstraint: NSLayoutConstraint!

// horizontal gap between elements on the same "line"
var horizontalSpacing: CGFloat!

// vertical gap between "lines"
var verticalSpacing: CGFloat!

// vertical gap between labels above text fields
var labelToFieldSpacing: CGFloat!

override func viewDidLoad() {

super.viewDidLoad()

view.backgroundColor = UIColor(white: 0.9, alpha: 1.0)

horizontalSpacing = CGFloat(2)
verticalSpacing = CGFloat(8)
labelToFieldSpacing = CGFloat(2)

createIntializeViews()
createInitializeStacks()
fillStacks()

}

private func createIntializeViews() {

fieldStackView1 = LabeledFieldStackView(with: "label 1", fieldText: "field 1", verticalGap: labelToFieldSpacing)
fieldStackView2 = LabeledFieldStackView(with: "label 2", fieldText: "field 2", verticalGap: labelToFieldSpacing)
fieldStackView3 = LabeledFieldStackView(with: "label 3", fieldText: "field 3", verticalGap: labelToFieldSpacing)
fieldStackView4 = LabeledFieldStackView(with: "label 4", fieldText: "field 4", verticalGap: labelToFieldSpacing)

stepper = UIStepper()

}

private func createInitializeStacks() {

rootStack = UIStackView()
fieldAndStepperStack = UIStackView()
twoLineStack = UIStackView()

[rootStack, fieldAndStepperStack, twoLineStack].forEach {
$0?.translatesAutoresizingMaskIntoConstraints = false
}

// rootStack has spacing of horizontalSpacing (inter-line vertical spacing)
rootStack.axis = .vertical
rootStack.alignment = .fill
rootStack.distribution = .fill
rootStack.spacing = verticalSpacing

// fieldAndStepperStack has spacing of horizontalSpacing (space between field and stepper)
// and .alignment of .bottom (so stepper aligns vertically with field)
fieldAndStepperStack.axis = .horizontal
fieldAndStepperStack.alignment = .bottom
fieldAndStepperStack.distribution = .fill
fieldAndStepperStack.spacing = horizontalSpacing

// twoLineStack has inter-line vertical spacing of
// verticalSpacing in portrait orientation
// for landscape orientation, the two "lines" will be changed to one "line"
// and the spacing will be changed to horizontalSpacing
twoLineStack.axis = .vertical
twoLineStack.alignment = .leading
twoLineStack.distribution = .fill
twoLineStack.spacing = verticalSpacing

}

private func fillStacks() {

self.view.addSubview(rootStack)

// constrain rootStack Top, Leading, Trailing = 20
// no height or bottom constraint
NSLayoutConstraint.activate([
rootStack.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 20.0),
rootStack.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor, constant: 20.0),
rootStack.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor, constant: -20.0),
])

rootStack.addArrangedSubview(fieldStackView1)

fieldAndStepperStack.addArrangedSubview(fieldStackView2)
fieldAndStepperStack.addArrangedSubview(stepper)

twoLineStack.addArrangedSubview(fieldAndStepperStack)
twoLineStack.addArrangedSubview(fieldStackView3)

rootStack.addArrangedSubview(twoLineStack)

// fieldAndStepperStack needs width constrained to its superview (the twoLineStack) when
// in portrait orientation
// setting the priority to 999 prevents "nested stackView" constraint breaks
fieldAndStepperStackWidthConstraint = fieldAndStepperStack.widthAnchor.constraint(equalTo: twoLineStack.widthAnchor, multiplier: 1.0)
fieldAndStepperStackWidthConstraint.priority = UILayoutPriority(rawValue: 999)

// constrain fieldView3 width to fieldView2 width to keep them the same size
NSLayoutConstraint.activate([
fieldStackView3.widthAnchor.constraint(equalTo: fieldStackView2.widthAnchor, multiplier: 1.0)
])

rootStack.addArrangedSubview(fieldStackView4)

}

override public func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {

super.traitCollectionDidChange(previousTraitCollection)

if traitCollection.verticalSizeClass == .regular {
fieldAndStepperStackWidthConstraint.isActive = true
twoLineStack.axis = .vertical
twoLineStack.spacing = verticalSpacing
} else if traitCollection.verticalSizeClass == .compact {
fieldAndStepperStackWidthConstraint.isActive = false
twoLineStack.axis = .horizontal
twoLineStack.spacing = horizontalSpacing
} else {
print("Unexpected")
}
}

}

And the results:

Sample Image

Sample Image



Related Topics



Leave a reply



Submit