Programmatically adding multiple subviews with constraints throws exception - ios

EnvironmentXcode-8Swift-3iOS-10
IntentThe germain intent is to get three subviews added to the main view, such that they appear sequentially adjacent starting from the top of the screen.There's more to the ultimate intent, but this is where I'm currently stuck.
Working CodeIf I'm just adding a single subview, it works with the following code (slightly abridged, line numbers added for reference):
01: class ViewController: UIViewController, UITextFieldDelegate {
02: var textMatte: UIView!
03: override func viewDidLoad() {
04: super.viewDidLoad()
05: setupTextfieldMatte(inView: view)
06: }
07: func setupTextfieldMatte(inView: UIView) {
08: textMatte = UIView()
09: textMatte.backgroundColor = UIColor.lightGray
10: textMatte.translatesAutoresizingMaskIntoConstraints = false
11: inView.addSubview(textMatte)
12: let tmGuide = textMatte.layoutMarginsGuide
13: let inGuide = inView.layoutMarginsGuide
14: tmGuide.topAnchor.constraint(equalTo: inGuide.topAnchor, constant: 40).isActive = true
15: tmGuide.leadingAnchor.constraint(equalTo: inGuide.leadingAnchor, constant: 20).isActive = true
16: tmGuide.trailingAnchor.constraint(equalTo: inGuide.trailingAnchor, constant: -20).isActive = true
17: tmGuide.heightAnchor.constraint(equalToConstant: 40).isActive = true
18: }
19: //... other code ...
20: }
Broken CodeI wanted to break-up the creation step from the constraint step, in order to do "batch" processing (and because there's more that needs to happen, as eventually these subviews will also have their own subviews...). Added relative line numbers for ease of further description and (hopefully) answers from you folks:
01: class ViewController: UIViewController, UITextFieldDelegate {
02: var mattes = [UIView()]
03: override func viewDidLoad() {
04: super.viewDidLoad()
05: for (index, title) in ["A", "B", "C"].enumerated() { // genericized for posting
06: let v = createView(idx: index)
07: mattes.append(v)
08: view.addSubview(v)
09: }
10: _ = mattes.popLast() //from testing/debugging found that there was an extraneous entry in the array
11: addViewAnnotations(views: mattes, inView: self.view)
12: }
13: func createView(idx: Int) -> UIView {
14: let v = UIView()
15: v.backgroundColor = UIColor.lightGray
16: v.translatesAutoresizingMaskIntoConstraints = false
17: v.tag = idx
18: return v
19: }
20: func addViewAnnotations(views: [UIView], inView: UIView) {
21: let inGuide = inView.layoutMarginsGuide
22: for (index, v) in views.enumerated() {
23: let myGuide = v.layoutMarginsGuide
24: if index == 0 {
25: myGuide.topAnchor.constraint(equalTo: inGuide.topAnchor, constant: 40).isActive = true
*** //^^^ libc++abi.dylib: terminating with uncaught exception of type NSException
26: }
27: else {
28: myGuide.topAnchor.constraint(equalTo: views[index-1].layoutMarginsGuide.bottomAnchor, constant: 10).isActive = true
29: }
30: myGuide.leadingAnchor.constraint(equalTo: inGuide.leadingAnchor, constant: 20).isActive = true
31: myGuide.trailingAnchor.constraint(equalTo: inGuide.trailingAnchor, constant: -20).isActive = true
32: myGuide.heightAnchor.constraint(equalToConstant: 40).isActive = true
33: }
34: }
35: //... other code ...
36: }
The error occurs on line 25 (error message in comment below that line), verified with print statements above and below the line.There's no additional error/debugging information that I can seeI had expected that it might break on line 28, but 25 (in the "broken code") is identical to line 14 (in the working code)Note: If I put a try in from of the line, Xcode tells me: "No calls to throwing functions occur within 'try' expression"
I have a feeling that this is one of those stupid oversights that crop up so often when modifying code, but I just cannot seem to spot the source of the problem - so I'm hoping one of you can.
UPDATE 2016-10-16Based on my modification as described in the first comment I added, I played around with the code a bit more and found that the problem occurs only when trying to make [what is now] the last constraint active:
001: override func viewDidLoad() {
002: super.viewDidLoad()
003: for (index, title) in ["Small Blind", "Big Blind", "Ante"].enumerated() {
004: let v = createView(idx: index)
005: mattes.append(v)
006: view.addSubview(v)
007: let myGuide = v.layoutMarginsGuide
008: let inGuide = view.layoutMarginsGuide
009: myGuide.leadingAnchor.constraint(equalTo: inGuide.leadingAnchor, constant: 20 ).isActive = true
010: myGuide.trailingAnchor.constraint(equalTo: inGuide.trailingAnchor, constant: -20).isActive = true
011: myGuide.heightAnchor.constraint(equalToConstant: 40).isActive = true
012: if index == 0 {
013: myGuide.topAnchor.constraint(equalTo: inGuide.topAnchor, constant: 40).isActive = true
014: }
015: else {
016: let x = myGuide.topAnchor.constraint(equalTo: mattes[index-1].bottomAnchor, constant: 10)
017: x.isActive = true
//^^^ libc++abi.dylib: terminating with uncaught exception of type NSException
018: }
019: }
020: }
All the other constraints have no issues. It doesn't seem to matter if I use:mattes[index-1].bottomAnchormattes[index-1].layoutMarginsGuide.bottomAnchorThe first one evaluates to <NSLayoutYAxisAnchor:0x174076540 "UIView:0x100b0d380.bottom">, the second to <NSLayoutYAxisAnchor:0x174076c00 "UILayoutGuide:0x174191e00'UIViewLayoutMarginsGuide'.bottom"> (if that matters?)If I comment out line 017, the code runs, but while the first rectangle is in the correct location, the remaining two are not - they're flush against the top of the screen instead of below the top indicators.So - perhaps there's a better way to set them up?

What is the description of the exception? Be sure to add views to their superview (in the same view hierarchy) before adding the constraints. You can't create constraints between views that aren't both in the same hierarchy.
In your update, you're using index-1 as an index into the mattes array, that may be the cause of the exception the first time around.
I don't know if this is contributing to issues, but I don't create constraints from one view's layoutMarginsGuide to a superview's layoutMarginsGuide. Think of that guide as for the view and it's subviews. So the constraints would be from the subview itself (not its margin) to the superview's layoutMarginsGuide.

I think it was a combination of things that was causing the problem, but first let me present the working code and a couple of screen shots
001: class ViewController: UIViewController, UITextFieldDelegate {
002: var textfields: [UITextField] = []
003: var labels: [UILabel] = []
004: var mattes: [UIView] = []
005: override func viewDidLoad() {
006: super.viewDidLoad()
007: view.backgroundColor = UIColor.blue
008: for title in ["A", "B", "C"] {
009: let matte = setupMatte(inView: view)
010: let textfield = setupTextfield(inView: view, title: title)
011: let label = setupLabel(inView: view, title: title)
012: mattes.append(matte)
013: textfields.append(textfield)
014: labels.append(label)
015: }
016: addConstraints(inView: view) // could probably leave off parameter, but for now...
017: }
018: func setupMatte(inView: UIView) -> UIView {
019: let matte = UIView()
//...various view attribute configurations...
020: matte.translatesAutoresizingMaskIntoConstraints = false
021: inView.addSubview(matte)
022: return matte
023: }
024: func setupTextfield(inView: UIView, title: String) -> UITextField {
025: let textfield = UITextField()
//...various textfield attribute configurations...
026: textfield.translatesAutoresizingMaskIntoConstraints = false
027: inView.addSubview(textfield)
028: return textfield
029: }
030: func setupLabel(inView: UIView, title: String) -> UILabel {
031: let label = UILabel()
//...various label attribute configurations...
032: label.translatesAutoresizingMaskIntoConstraints = false
033: inView.addSubview(label)
034: return label
035: }
036: func addConstraints(inView: UIView) {
037: for (index, matte) in mattes.enumerated() {
038: let textfield = textfields[index]
039: let label = labels[index]
040: matte.leadingAnchor.constraint(equalTo: inView.leadingAnchor).isActive = true
041: matte.trailingAnchor.constraint(equalTo: inView.trailingAnchor).isActive = true
042: matte.heightAnchor.constraint(equalToConstant: 50).isActive = true
043: label.leadingAnchor.constraint(equalTo: matte.leadingAnchor, constant: 20).isActive = true
044: label.widthAnchor.constraint(equalToConstant: 150).isActive = true
045: textfield.leadingAnchor.constraint(equalTo: label.trailingAnchor, constant: 5).isActive = true
046: textfield.trailingAnchor.constraint(equalTo: matte.trailingAnchor, constant: -20).isActive = true
047: if index == 0 {
048: matte.topAnchor.constraint(equalTo: inView.topAnchor, constant: 20).isActive = true
049: }
050: else {
051: matte.topAnchor.constraint(equalTo: mattes[index - 1].bottomAnchor, constant: 5).isActive = true
052: }
053: textfield.centerYAnchor.constraint(equalTo: matte.centerYAnchor).isActive = true
054: label.centerYAnchor.constraint(equalTo: textfield.centerYAnchor).isActive = true
055: }
056: }
//... other code ...
057: }
Interestingly, while the image from the simulator correctly handles landscape mode, when I run it on my iPhone 6s - it doesn't rotate when I hold the phone in landscape mode - but I'm not too worried about that at this time, since the app I'm working on is only intended for portrait mode.
Now, the pieces that I think made a significant difference from my original code:
(Note: I don't know why, but for some reason I originally named the function for handling constraints "Annotations", this was fixed in my current working code)
BROKEN : var mattes = [UIView()]
WORKING: var mattes: [UIView] = []
I think the above may have been a significant difference
BROKEN : viewDidLoad function looped:
Created sub-view
Appended sub-view to mattes array
Called view.addSubview(v)
After loop:
Popped off last element of mattes array
Added annotations
WORKING: viewDidLoad function looped:
Created sub-view (matte)
The creation process called inView.addSubview(matte) (where inView is the same as view or self.view)
Appended sub-view to mattes array
After loop:
Added annotations
I think the difference in how mattes was defined eliminated the need to pop off the last (or any) element in the array before going to add the constraints
BROKEN : in addViewAnnotations I made constraints using the layoutMarginGuide of both the sub-view and [parent] view
v.layoutMarginGuide.topAnchor.constraint(equalTo: view.layoutMarginGuide.topAnchor, constant: 40).isActive = true
WORKING: in addConstraints I made the constraints directly from the sub-view and [parent] view
matte.topAnchor.constraint(equalTo: view.topAnchor, constant: 20).isActive = true
(Thanks to Jerry for pointing this out, I didn't understand the distinction and I don't think I fully fixed my code when I did my "UPDATE")
The above was probably the most significant difference

Related

Is this an iOS 14 UISplitViewController bug?

In my app, I want a three-column UISplitViewController. I create it like this:
let svc = UISplitViewController(style: .tripleColumn)
svc.preferredDisplayMode = .twoOverSecondary
svc.setViewController(TestViewController(), for: .primary)
svc.setViewController(TestViewController(), for: .supplementary)
svc.setViewController(TestViewController(), for: .secondary)
svc.primaryBackgroundStyle = .sidebar
This ViewController that I'm presenting is a really simple viewController. It doesn't do anything except, present a centered red square.
class TestViewController: UIViewController {
private var redView = UIView()
override func viewDidLoad() {
super.viewDidLoad()
redView.backgroundColor = .red
redView.translatesAutoresizingMaskIntoConstraints = false
self.view.addSubview(redView)
redView.widthAnchor.constraint(equalToConstant: 30).isActive = true
redView.heightAnchor.constraint(equalToConstant: 30).isActive = true
redView.centerXAnchor.constraint(equalTo: self.view.centerXAnchor).isActive = true
redView.centerYAnchor.constraint(equalTo: self.view.centerYAnchor).isActive = true
}
}
Now, when I run this code, I get the following result.
As you can see, 2 of the 3 squares are off-center. Anyone know, what I'm doing wrong here? Or is this a known bug?
You should use "Safe Area" instead of "self.view" when setting to center.
The detail view here has a push-away effect. You just need to click on it and the primary view controller will hide, like below.

Swift - access array from another class

In my project I have an array where the user can append elements. My problem right now is that I have another class with a tableView where I want to display all the elements dynamically so if the user adds and element to the array it is also in my tableView.
Class A
var wishListTitlesArray: [String] = [String]()
Class B
var dropDownOptions = [String]() // TableView data -> here I would like to access `wishListTitlesArray`
Update
So thanks for the comments so far, I got the main problem I had but I am still struggling.
My setup:
I create my dropDownButton (which contain a dropDownView) in my ViewController-Class where I also have my var wishListTitlesArray.
I tried dropDownButton.dropView.dropDownOptions = wishListTitlesArray . However that does not do the full job.
#objc func addWishButtonTapped(notification : Notification){
popUpView.popUpTextField.text = ""
self.popUpView.popUpTextField.becomeFirstResponder()
view.addSubview(visualEffectView)
view.addSubview(popUpView)
view.addSubview(wishButton)
self.view.addSubview(dropDownButton)
// constrain blurrEffectView
visualEffectView.leftAnchor.constraint(equalTo: view.leftAnchor).isActive = true
visualEffectView.rightAnchor.constraint(equalTo: view.rightAnchor).isActive = true
visualEffectView.topAnchor.constraint(equalTo: view.topAnchor).isActive = true
visualEffectView.bottomAnchor.constraint(equalTo: view.bottomAnchor).isActive = true
// constrain popUpView
popUpView.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
popUpView.centerYAnchor.constraint(equalTo: view.centerYAnchor, constant: -50).isActive = true
popUpView.heightAnchor.constraint(equalToConstant: 230).isActive = true
popUpView.widthAnchor.constraint(equalToConstant: view.frame.width - 85).isActive = true
// constrain wishButton
wishButton.centerXAnchor.constraint(equalTo: popUpView.centerXAnchor).isActive = true
wishButton.centerYAnchor.constraint(equalTo: popUpView.centerYAnchor, constant: 70).isActive = true
wishButton.heightAnchor.constraint(equalToConstant: 72).isActive = true
wishButton.widthAnchor.constraint(equalToConstant: 72).isActive = true
// constrain DropDownButton
dropDownButton.centerXAnchor.constraint(equalTo: self.popUpView.centerXAnchor).isActive = true
dropDownButton.centerYAnchor.constraint(equalTo: self.popUpView.centerYAnchor).isActive = true
dropDownButton.widthAnchor.constraint(equalToConstant: 100).isActive = true
dropDownButton.heightAnchor.constraint(equalToConstant: 40).isActive = true
// set the drop down menu's options
dropDownButton.dropView.dropDownOptions = wishListTitlesArray
self.view.bringSubviewToFront(visualEffectView)
self.view.bringSubviewToFront(popUpView)
self.view.bringSubviewToFront(wishButton)
self.view.bringSubviewToFront(dropDownButton)
self.view.bringSubviewToFront(dropDownButton.dropView)
This is where the user can add an element to the wishListTitlesArray:
if let txt = listNameTextfield.text {
self.newListTextfield.resignFirstResponder()
// append user-entered text to the data array
self.wishListTitlesArray.append(txt)
self.wishListImagesArray.append(self.image!)
// DonMag3 - append new empty wish array
self.userWishListData.append([Wish]())
let theCustomWishlistView = createCustomWishlistView()
self.view.addSubview(theCustomWishlistView)
// constrain CustomWishlistView
theCustomWishlistView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 120.0).isActive = true
theCustomWishlistView.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: 0).isActive = true
theCustomWishlistView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor, constant: 30.0).isActive = true
theCustomWishlistView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor, constant: -30.0).isActive = true
theCustomWishlistView.wishlistImage.image = self.image
theCustomWishlistView.wishlistLabel.text = txt
theCustomWishlistView.transform = CGAffineTransform(translationX: 0, y: 1000)
self.view.bringSubviewToFront(containerView)
// reload the collection view
theCollectionView.reloadData()
theCollectionView.performBatchUpdates(nil, completion: {
(result) in
// scroll to make newly added row visible (if needed)
let i = self.theCollectionView.numberOfItems(inSection: 0) - 1
let idx = IndexPath(item: i, section: 0)
self.theCollectionView.scrollToItem(at: idx, at: .bottom, animated: true)
// close (hide) the "New List" view
self.closeButtonTappedNewList(nil)
})
and the problem right now is that if the user adds an element, dropDownOptions is not updating the data accordingly. So how can I access the updated list? Thanks for all the comments so far!
This is fairly straightforward OO programming. As somebody pointed out, you're dealing with instance variables and instances of classes, not classes.
An analogy:
Instances: If you want one instance of a class to get a value from an instance of another class, it's like having your Toyota car request the radio station presets from your friend's Honda and set your radio the same way.
Classes: The Toyota motor company decides they like the radio presets that Honda uses, so they read the radio station presets from the Honda company and the Toyota factory uses those settings for all new Toyotas.
In class A, make wishListTitlesArray public:
public var wishListTitlesArray: [String] = [String]()
Then, your Class B object will need a pointer to the Class A object. There are various ways to do that. Let's say you set up Class B to take a Class A object at init time:
Class B {
var myAObject: A
var dropDownOptions: [String]
init(aObject: A) {
myAObject = anObject
dropDownOptions = myAObject.wishListTitlesArray
}
//more code here
}

Cant get UISwitch constraints to work - swift 4

I'm trying to create a UISwitch programmatically but for some reason I can't get any constraints to work. If I don't use any constraints the UISwitch is created but in the top left corner.
I tried creating the UISwitch by adding in the constraints when declaring my switch and this does move the switch to a different place. However this is a problem as I want to it to be constrained against other items in my view.
class cameraController: UIViewController {
var switchOnOff: UISwitch {
let switchOnOff = UISwitch()
switchOnOff.isOn = false
switchOnOff.translatesAutoresizingMaskIntoConstraints = false
switchOnOff.addTarget(self, action: #selector(switchButton(_:)), for: .valueChanged)
return switchOnOff
}
}
extension cameraController{
#objc func switchButton(_ sender:UISwitch){
if (sender.isOn == true){
print("UISwitch state is now ON")
}
else{
print("UISwitch state is now Off")
}
}
}
extension cameraController {
private func setupLayouts() {
view.addSubview(switchOnOff)
switchOnOff.rightAnchor.constraint(equalTo: view.rightAnchor, constant: -20).isActive = true
switchOnOff.topAnchor.constraint(equalTo: photosButtonIcon.bottomAnchor, constant: 20).isActive = true
}
}
I want to be able to add my constraints for the switch into my setupLayouts extension and not when the switch is being declared.
The error I am getting when running the above code is:
Thread 1: EXC_BAD_ACCESS (code=EXC_I386_GPFLT)
I am getting this error on the first line of the constraints in my setupLayouts extension.
Any help would be great.
You need a lazy var to have self inside this block which is a closure , your current code is a computed property which returns a switch for every reference and that causes a crash in constraints as the switch added to view is not the same object as the one you set constraints for
lazy var switchOnOff: UISwitch = {
let switchOnOff = UISwitch()
switchOnOff.isOn = false
switchOnOff.translatesAutoresizingMaskIntoConstraints = false
switchOnOff.addTarget(self, action: #selector(switchButton(_:)), for: .valueChanged)
return switchOnOff
}()
Then call setupLayouts inside viewDidLoad , btw extension is totally irrelevant to this problem

How to properly make a container view that contains some viewControllers(programmatically)?

I want to create a container View programmatically that has inside a bunch of view controllers. I have a segmented controller that when the user tapped a button it should display a certain viewController without doing a segue.
I have everything inside a ViewController,
I wanted to know how to properly make the segmented controller display a certain view controller when tapped.
func setUpSegmentedControl() {
let seg = UISegmentedControl(items: ["1", "2", "3"])
seg.selectedSegmentIndex = 0
seg.translatesAutoresizingMaskIntoConstraints = false
seg.layer.cornerRadius = 8
seg.backgroundColor = UIColor.white
seg.addTarget(self, action: #selector(changeColor(sender:)), for: .valueChanged)
view.addSubview(seg)
NSLayoutConstraint.activate([
seg.centerXAnchor.constraint(equalTo: view.centerXAnchor), seg.centerYAnchor.constraint(equalTo: view.centerYAnchor), seg.leadingAnchor.constraint(equalTo: view.leadingAnchor), seg.trailingAnchor.constraint(equalTo: view.trailingAnchor), seg.heightAnchor.constraint(equalToConstant: 50)
])
}
#objc func changeColor(sender: UISegmentedControl) {
switch sender.selectedSegmentIndex {
case 0:
addChild(FirstViewController())
print("1")
case 1:
addChild(SecondViewController())
print("2")
default:
addChild(ThirdViewController())
print("3")
}
}
To add a view controller to a container view programmatically, you need to call addSubview and didMove in addition to addChild.
let firstViewController = FirstViewController()
addChild(firstViewController)
firstViewController.view.translatesAutoresizingMaskIntoConstraints = false
containerView.addSubview(firstViewController.view) // replace `containerView` with the name of the view that's supposed to contain the VC's view
// add constraints or set frame manually
let trailingConstraint = firstViewController.view.trailingAnchor.constraint(equalTo: view.trailingAnchor)
let leadingConstraint = firstViewController.view.leadingAnchor.constraint(equalTo: view.leadingAnchor)
let topConstraint = firstViewController.view.topAnchor.constraint(equalTo: view.topAnchor)
let bottomConstraint = firstViewController.view.bottomAnchor.constraint(equalTo: view.bottomAnchor)
NSLayoutConstraint.activate([trailingConstraint, leadingConstraint, topConstraint, bottomConstraint])
firstViewController.didMove(to parent: self)
If you want to remove a child view controller programatically:
let child = children.first // or other way to identify your VC
child?.willMove(toParentViewController: nil)
child?.view.removeFromSuperview()
child?.view.removeFromParentViewController()
Example project

Am I adding yet another, or changing, this constraint?

Say I do this ...
override func viewDidLoad() {
super.viewDidLoad()
view.heightAnchor.constraint(equalToConstant: 50).isActive = true
later, I do this ...
view.heightAnchor.constraint(equalToConstant: 51).isActive = true
am I a bad person?
Have I now redundantly added a second constraint (what happens to the first?)
Or does it know to change the first?
Or does something else happen? Am I making a leak?
What should I do, and in what way have I been bad, if any?
What's the correct thing to do here?
view.heightAnchor.constraint(equalToConstant: 50).isActive = true
later, I do this ...
view.heightAnchor.constraint(equalToConstant: 51).isActive = true
am I a bad person?
Yes, and if I'm not mistaken, the runtime will tell you so with a loud message in the console warning that you have unsatisfiable (conflicting) constraints. You have a constraint setting the height to 50 and another setting the height to 51 and both things cannot be true.
You didn't give enough code to be sure of what's going on in your case, but you can easily see that what I'm saying is correct, by making your view controller's code consist only of the following:
func delay(_ delay:Double, closure:#escaping ()->()) {
let when = DispatchTime.now() + delay
DispatchQueue.main.asyncAfter(deadline: when, execute: closure)
}
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let v = UIView()
v.translatesAutoresizingMaskIntoConstraints = false
self.view.addSubview(v)
v.topAnchor.constraint(equalTo: self.view.topAnchor).isActive = true
v.leadingAnchor.constraint(equalTo: self.view.leadingAnchor).isActive = true
v.widthAnchor.constraint(equalToConstant: 100).isActive = true
v.heightAnchor.constraint(equalToConstant: 50).isActive = true
delay(3) {
v.heightAnchor.constraint(equalToConstant: 51).isActive = true
}
}
}
As you stipulated, we "later" add the 51 constraint — and as I said, the runtime squawks at that moment.
The correct procedure is to keep a reference to the original constraint
so that you can change its constant later. (Under more complex circumstances you could actually deactivate, i.e. remove, the first constraint before adding the second, but if all you plan to do is alter the constant, that's not necessary, as it is mutable.)

Resources