Are there hidden constraints in auto-layout - cocoa

I have a complex layout that works perfectly. Now I want to overlay the existing layout with a view that is resizable with, say a 40 pixel margin, relative to the window. When I add a empty custom view to the content view, the constraint system goes haywire and says it cannot satisfy the constraints, and the results are quite random.
I add the view as:
contentView.addSubview(customView)
let dict = ["CustomView" : customView]
let s1 = "|-(40)-[CustomView]-(40)-|"
let s2 = "V:|-(40)-[CustomView]-(40)-|"
let con1 = NSLayoutConstraint.constraints(withVisualFormat: s1, options: [],
metrics: nil, views: dict as [String : Any])
let con2 = NSLayoutConstraint.constraints(withVisualFormat: s2, options: [],
metrics: nil, views: dict as [String : Any])
viewLayout.append(contentsOf: con1)
viewLayout.append(contentsOf: con2)
NSLayoutConstraint.activate(viewLayout)
As far as I can tell, this view should add no additional constraints as to the size or shape of the underlying content view, but should simply be resized to the appropriate size. It would seem that must be some implicit constraint on the view preventing it from simply being sized or I am missing something fundamental about constraints.

I got this to work. I needed to uncheck the "Translates Mask into Constraints" checkbox for the custom view.

Related

Xcode UITest scrolling to the bottom of an UITableView

I am writing an UI test case, in which I need to perform an action, and then on the current page, scroll the only UITableView to the bottom to check if specific text shows up inside the last cell in the UITableView.
Right now the only way I can think of is to scroll it using app.tables.cells.element(boundBy: 0).swipeUp(), but if there are too many cells, it doesn't scroll all the way to the bottom. And the number of cells in the UITableView is not always the same, I cannot swipe up more than once because there might be only one cell in the table.
One way you could go about this is by getting the last cell from the tableView. Then, run a while loop that scrolls and checks to see if the cell isHittable between each scroll. Once it's determined that isHittable == true, the element can then be asserted against.
https://developer.apple.com/documentation/xctest/xcuielement/1500561-ishittable
It would look something like this (Swift answer):
In your XCTestCase file, write a query to identify the table. Then, a subsequent query to identify the last cell.
let tableView = app.descendants(matching: .table).firstMatch
guard let lastCell = tableView.cells.allElementsBoundByIndex.last else { return }
Use a while loop to determine whether or not the cell isHittable/is on screen. Note: isHittable relies on the cell's userInteractionEnabled property being set to true
//Add in a count, so that the loop can escape if it's scrolled too many times
let MAX_SCROLLS = 10
var count = 0
while lastCell.isHittable == false && count < MAX_SCROLLS {
apps.swipeUp()
count += 1
}
Check the cell's text using the label property, and compare it against the expected text.
//If there is only one label within the cell
let textInLastCell = lastCell.descendants(matching: .staticText).firstMatch
XCTAssertTrue(textInLastCell.label == "Expected Text" && textInLastCell.isHittable)
Blaines answer lead me to dig a little bit more into this topic and I found a different solution that worked for me:
func testTheTest() {
let app = XCUIApplication()
app.launch()
// Opens a menu in my app which contains the table view
app.buttons["openMenu"].tap()
// Get a handle for the tableView
let listpagetableviewTable = app.tables["myTableView"]
// Get a handle for the not yet existing cell by its content text
let cell = listpagetableviewTable.staticTexts["This text is from the cell"]
// Swipe down until it is visible
while !cell.exists {
app.swipeUp()
}
// Interact with it when visible
cell.tap()
}
One thing I had to do for this in order to work is set isAccessibilityElement to true and also assign accessibilityLabel as a String to the table view so it can be queried by it within the test code.
This might not be best practice but for what I could see in my test it works very well. I don't know how it would work when the cell has no text, one might be able to reference the cell(which is not really directly referenced here) by an image view or something else. It's obviously missing the counter from Blaines answer but I left it out for simplicity reasons.

Xcode9 Swift4.2 NSOutlineView NSTreeController data

I am trying to create an outlineview in a MacOS app that has mutliple levels that are summaries for a set of data held in SQLite3. I have an outlineview working with a treecontroller with a very simple NSMutuableDictionary based on a model class.
import Cocoa
class Summary: NSObject {
#objc dynamic var name: String
#objc dynamic var trades: Int
#objc dynamic var avgPL: Double
#objc dynamic var pandl: Double
#objc dynamic var parent: String
#objc dynamic var isLeaf: Bool
#objc dynamic var childCount: Int
#objc dynamic var children: [Summary] = []
init(name: String, trades: Int, avgPL: Double, pandl: Double, parent: String, isLeaf: Bool,childCount: Int) {
self.name = name
self.trades = trades
self.avgPL = avgPL
self.pandl = pandl
self.parent = parent
self.isLeaf = isLeaf
self.childCount = childCount
}
#objc func add(child: Summary) {
children.append(child)
}
}
My simple example data is:
let root: [String : Any] = ["name": "Overall","trades":5,"avgPL":200,"pandl":500,"parent":"","isLeaf": false,"childCount": 2 ]
let dict: NSMutableDictionary = NSMutableDictionary(dictionary: root)
let l2a = Summary(name: "L2a", trades: 3, avgPL: 100, pandl: 300, parent: "L1",isLeaf: true,childCount: 0)
let l2b = Summary(name: "L2b", trades: 2, avgPL: 100, pandl: 200, parent: "L1",isLeaf: true,childCount: 0)
dict.setObject([l2a,l2b], forKey: "children" as NSCopying)
I pass the dictionary to the treeController:
treeController.addObject(dict)
And that works nicely giving me a collapsible outline:
But I have no idea how to add more levels or children to the children. I want to have up to four levels deep in the outline. I have all the SQL summaries working and I have tried so many variations of populating arrays and trying to create a dictionary with the data to no avail. I have children and childCount and isLeaf set on everything but treecontroller does not like the array complaining that isLeaf is not KVO compliant. My data in an array looks like this (not all of the data but enough to see what I'm doing) The main level and all of the subsequent children are all based on the Summary model class above. Can I simply convert this array to a dictionary? Or, can I make it KVO compliant by adding keys to the model class or something? I have all of the 4 levels in separate arrays I use to build the resultant array if that is useful :
I should add that I have an NSObject defined as an NSMutableArray and its content tied to the treeController. My treeController is bound to each variable in the model class and at the top level has:
If I pass the array I have built to the treeController I get the following error:
Failed to set (contentViewController) user defined inspected property on (NSWindow): [<_TtGCs23_ContiguousArrayStorageC11outlinetest7Summary_ 0x604000445160> addObserver:forKeyPath:options:context:] is not supported. Key path: isLeaf
After building out my NSOutlineView without an NSTreeController and getting everything working I still wanted to get back to this and implement the treeController in order to take advantage of the sorting mechanism it provides. And I did find as per my last comment that I did have something wrong in InterfaceBuilder that was causing it to complain about KVO compliance. I had everything wired correctly except for the Content Array binding on the treeController. Here I bound it to my ViewController and added my data array reportSummary to the Model Key Path.
I also no longer needed to manually add my data array to the treeController using treeController.addObject(reportSummary). Once this was working I was then able to implement sorting and everything is working well. I should point out two things.
Setup of sorting on the TreeController is slightly different than on an ArrayController tied to a TableView. With the tableview it was sufficient to specify which columns are sortable in the identity inspector in IB. But in the outlineView scenario I also needed to setup bindings in IB to the treeController and change the Controller Key from arrangedObjects to sortDescriptors.
While testing my tree controlled outlineview I ran into a problem when I double-clicked on a summary row. I had implemented Double Action on the outlineView in IB in order to control the expanding and collapsing of summary sections. Note that I read about doing this in a thread here and someone mentioned that you would need to maintain multiple arrays and track indexes because once a row is collapsed or expanded that changes the row number of all the subsequent rows. But I figured out that the solution is simply to iterate through rows in reverse order and expand or collapse them working back up the tree starting from outlineView.numberOfRows-1. This works well and along with Double Action (clicking) to expand and collapse I also added an NSSlider which tracks to the expansion level and lets me collapse all the lowest levels moving back up the tree instead of clicking all of the little arrows on each row. This broke when I implemented the treeController. I received an error
Could not cast value of type 'NSKVONotifying_NSTreeControllerTreeNode'
This line of code was the problem
let summary = reportOutline.item(atRow: x) as! Summary
I had to change this to
let node = reportOutline.item(atRow: x) as! NSTreeNode
let summary = node.representedObject as! Summary
And that is it. Working beautifully now.

How to add constraints after an addSubview so the subview resizes with the superview

I have a sample project which demonstrates the problem here
https://github.com/ericgorr/autolayout_with_addsubview.git
I have a view called CalcView which I want to programmatically add as a subview to a view on the main window for the app. When I resize my window, I want CalcView to be resized.
In windowDidLoad in MainWindowController, I add the subview by doing:
let calcViewController = ELIZCalcView()
let calcView = calcViewController.view
calcContentView?.addSubview( calcViewController.view )
I try to add the constraints by doing:
let bindings = [ "calcView": calcView ]
let horizontalContraint:[AnyObject] = NSLayoutConstraint.constraintsWithVisualFormat( "H:|[calcView]|", options: NSLayoutFormatOptions(0), metrics: nil, views: bindings )
let verticalContraint:[AnyObject] = NSLayoutConstraint.constraintsWithVisualFormat( "V:|[calcView]|", options: NSLayoutFormatOptions(0), metrics: nil, views: bindings )
calcContentView?.addConstraints( horizontalContraint )
calcContentView?.addConstraints( verticalContraint )
Now, for someone who knows how to properly interpret that code, it is likely very apparent that it will not work. After I run my app, I cannot resize the window at all. Additionally, I see the following error message in the console:
2015-07-04 16:04:45.019 aocsCalc[5797:3526462] Unable to simultaneously satisfy constraints: (
"<NSLayoutConstraint:0x618000082440 V:|-(0)-[NSView:0x600000120f00] (Names: '|':aocsCalc.ELIZHighlightView:0x608000120500 )>",
"<NSAutoresizingMaskLayoutConstraint:0x618000084100 h=--& v=&-- V:|-(-2)-[NSView:0x600000120f00] (Names: '|':aocsCalc.ELIZHighlightView:0x608000120500 )>" )
Will attempt to recover by breaking constraint <NSLayoutConstraint:0x618000082440 V:|-(0)-[NSView:0x600000120f00] (Names: '|':aocsCalc.ELIZHighlightView:0x608000120500 )>
Set the NSUserDefault NSConstraintBasedLayoutVisualizeMutuallyExclusiveConstraints to YES to have -[NSWindow visualizeConstraints:] automatically called when this happens. And/or, break on objc_exception_throw to catch this in the debugger.
If I remove the vertical constraint, the error message goes away and I can resize the window vertically.
So, what simple thing do I need to do so CalcView is resized along with the window?
Before you add your calcView as subview try inserting this line of code:
calcView.translatesAutoresizingMaskIntoConstraints = false
This should solve your problem.
By default on UIView/NSView this property is set to YES/true and it creates it's own set of constraints based on autoresizing mask. These auto-made constraints conflict with the ones you've created in code.
It clearly says so in the error description too. On lines 2 and 3 it shows you that there's 2 set of vertical constraints regarding one view - NSView:0x600000120f00, which appears to be your calcView.
<NSLayoutConstraint:0x618000082440 V:|-(0)-[NSView:0x600000120f00]
<NSAutoresizingMaskLayoutConstraint:0x618000084100 h=--& v=&-- V:|-(-2)-[NSView:0x600000120f00]
They are both vertical. First one wants to snap view to the superview's top with no margin. The second one (created automatically) wants to snap it with a small margin, presumably taken from how it is layed out in Interface Builder.
UPDATE
Create a new Cocoa Application project and paste the following code:
override func viewDidLoad() {
super.viewDidLoad()
let view = NSView()
view.translatesAutoresizingMaskIntoConstraints = false
self.view.addSubview(view)
//Making it red just to see a little better. Ignore this two lines.
view.wantsLayer = true
view.layer?.backgroundColor = CGColorCreateGenericRGB(1, 0, 0, 1)
//-----------------------------------------------------------------
let views = ["view" : view]
self.view.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("|-[view]-|", options: NSLayoutFormatOptions(0), metrics: nil, views: views))
self.view.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("V:|-[view]-|", options: nil, metrics: nil, views: views))
}
Newly created NSView is snapped to the View Controllers main view with standard margins (8 points, it's described as "-" in the visual format string) and is resizing with the main view (see pictures). A little tip - you don't have to specify the "H:" in the visual format, only "V:". It's horizontal by default.
This should give you a good idea of how adding constraints programmatically works. Code might not be optimal, I code in Obj-C and know very little Swift.
I've downloaded your project. Error probably lies somewhere in your complicated view hierarchy and xib manipulation. But that's a whole other story. Also be careful with scroll views, they are a bit tricky when it comes to autolayout, you can find a lot of coverage on that on SO. Happy coding :)

'+=' cannot be applied to two [AnyObject] operands

I have the following code to try and create an array of constraints to add to a view:
let views = ["button": button]
let metrics = ["margin": 16]
var constraints: [AnyObject] = []
constraints += NSLayoutConstraint.constraintsWithVisualFormat("|-margin-[button]-margin-|", options: 0, metrics: metrics, views: views)
From what I understand about Swift arrays, I should just be able to '+=' them together to join the two, but I get an error:
"Binary operator '+=' cannot be applied to two [AnyObject] operands"
What's wrong with this code?
It's not because of the operator.
It's because you are passing in an Int where you are actually supposed to pass NSLayoutFormatOptions enum type.
If you pass in one of the NSLayoutFormatOptions enum for the options parameter, the error will go away:
constraints += NSLayoutConstraint.constraintsWithVisualFormat("|-margin-[button]-margin-|", options: .AlignAllLeft, metrics: metrics, views: views)
Or you could also initialize the NSLayoutFormatOptions with the Int value that you want to use, like this:
NSLayoutFormatOptions(rawValue: 0)
0 would have worked in Objective-C, but you need to use the actual enum value in Swift.
The Swift errors are still often misleading in many cases, like this one.
Hope this helps.
I think you could try saving the results of constraintsWithVisualFormat into a temporary constant, then assigning it.
let newConstraints = NSLayoutConstraint.constraintsWithVisualFormat("|-margin-[button]-margin-|", options: 0, metrics: metrics, views: views)
constraints += newConstraints
Here's the answer:
Xcode was listing the wrong thing as the error, which is what led to all this confusion. Using an Int of 0 for my format options apparently does not make the compiler happy, so instead I had to use NSLayoutFormatOptions(0).
With that, the finished working code is like this:
var constraints: [AnyObject] = []
constraints += NSLayoutConstraint.constraintsWithVisualFormat("|-margin-[button]-margin-|", options: NSLayoutFormatOptions(0), metrics: metrics, views: views)
In my case using .AlignAllLeft instead of NSLayoutFormatOptions(rawValue: 0) was not what caused the error. The array of constraints should have been statically-typed.
// BEFORE:
// var viewConstraints: [AnyObject] = [AnyObject]()
// AFTER:
var viewConstraints: [NSLayoutConstraint] = [NSLayoutConstraint]()
let views = NSDictionary(object: customView, forKey: "customView")
viewConstraints += NSLayoutConstraint.constraintsWithVisualFormat("H:|[customView]|",
options: NSLayoutFormatOptions(rawValue: 0),
metrics: nil,
views: views as! [String : AnyObject])
// "Convert to Swift 2.0" Xcode option suggests syntax:
viewConstraints += NSLayoutConstraint.constraintsWithVisualFormat("V:|[customView]|",
options: [],
metrics: nil,
views: views as! [String : AnyObject])
That appears good, because I want "no options" value.

NSLayoutConstraint : resize view when needed

I have 2 "sister view" located one above the other (talking Y) embedded in the same parent view.
The first one is the size of the screen et the one below is outside of the screen at the bottom.
On a animation, the bottom view raises and shows up on the screen.
This animation should reduce the height of the upper view.
I thought using NSLayoutConstraint for this but I can't get work.
Right now I have following :
let constraintString : String = String(format: "V:[tranlucentView][buttonContainer(%lg)]", buttonHeight * countOfButtons)
let viewsDict:NSDictionary = ["tranlucentView" : self.translucentView, "buttonContainer" : self.buttonContainer]
var constraints : NSArray = NSLayoutConstraint.constraintsWithVisualFormat(constraintString, options: NSLayoutFormatOptions(0), metrics: nil, views: viewsDict)
self.view.addConstraints(constraints)
And that's the error I'm getting :
Probably at least one of the constraints in the following list is one you don't want. Try
this: (1) look at each constraint and try to figure out which you don't expect; (2) find the code that added the unwanted constraint or constraints and fix it. (Note: If you're seeing NSAutoresizingMaskLayoutConstraints that you don't understand, refer to the documentation for the UIView property translatesAutoresizingMaskIntoConstraints)
(
"<NSLayoutConstraint:0x198f6530 V:[UIView:0x198ef880]-(0)-[UIView:0x177b11e0]>",
"<NSLayoutConstraint:0x198f6600 V:[UIView:0x177b11e0(0)]>",
"<NSAutoresizingMaskLayoutConstraint:0x19f7f7f0 h=--& v=--& UIView:0x198ef880.midY == + 284>",
"<NSAutoresizingMaskLayoutConstraint:0x1983e4c0 h=--& v=--& V:[UIView:0x198ef880(568)]>",
"<NSAutoresizingMaskLayoutConstraint:0x19f7f260 h=--& v=--& UIView:0x177b11e0.midY == + 472>"
)
Any suggestions for my problem ?
I found the solution myself :
I needed to translatesAutoresizingMaskIntoConstraints to false for my upper-view to allow its resize.
Also I needed to add a constraint for its width or else its width goes to 0.
let constraintString : String = String(format: "V:|[tranlucentView][buttonContainer(%lg)]|", buttonHeight * countOfButtons)
self.translucentView.setTranslatesAutoresizingMaskIntoConstraints(false)
let viewsDict:NSDictionary = ["tranlucentView" : self.translucentView, "buttonContainer" : self.buttonContainer]
self.view.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat(constraintString, options: NSLayoutFormatOptions(0), metrics: nil, views: viewsDict))
self.view.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("H:|[tranlucentView]|", options: NSLayoutFormatOptions(0), metrics: nil, views: viewsDict))

Resources