How do I update constraints after changing UIButton's image - ios

I have a mini game in my project where you have to guess a flag of a country, I don't use Interface Builder, all UI is written in code. When user taps one of the flags I load new set of countries to guess, set their flags to buttons and since flags are different in sizes from previous ones I need to adjust UIButton constraints so I call my function setupConstraints() which looks like that:
func setupConstraints() {
NSLayoutConstraint.activate([
countryNameLabel.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 10),
countryNameLabel.centerXAnchor.constraint(equalTo: view.centerXAnchor),
countryNameLabel.widthAnchor.constraint(equalTo: view.widthAnchor, constant: -20),
countryNameLabel.heightAnchor.constraint(equalToConstant: 30),
firstCountryButton.topAnchor.constraint(equalTo: countryNameLabel.bottomAnchor, constant: 35),
firstCountryButton.centerXAnchor.constraint(equalTo: view.centerXAnchor),
firstCountryButton.heightAnchor.constraint(equalToConstant: 120),
firstCountryButton.widthAnchor.constraint(equalTo: firstCountryButton.heightAnchor, multiplier: flagImages[0].getAspectRatio()),
secondCountryButton.topAnchor.constraint(equalTo: firstCountryButton.bottomAnchor, constant: 25),
secondCountryButton.centerXAnchor.constraint(equalTo: view.centerXAnchor),
secondCountryButton.heightAnchor.constraint(equalToConstant: 120),
secondCountryButton.widthAnchor.constraint(equalTo: secondCountryButton.heightAnchor, multiplier: flagImages[1].getAspectRatio()),
thirdCountryButton.topAnchor.constraint(equalTo: secondCountryButton.bottomAnchor, constant: 25),
thirdCountryButton.centerXAnchor.constraint(equalTo: view.centerXAnchor),
thirdCountryButton.heightAnchor.constraint(equalToConstant: 120),
thirdCountryButton.widthAnchor.constraint(equalTo: thirdCountryButton.heightAnchor, multiplier: flagImages[2].getAspectRatio()),
])
}
This function works fine when I initially setup my View Controller but when I call it to update constraints I get in log following:
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.
(
"<NSLayoutConstraint:0x280d92940 UIButton:0x102315180.height == 120 (active)>",
"<NSLayoutConstraint:0x280d92990 UIButton:0x102315180.width == 1.5*UIButton:0x102315180.height (active)>",
"<NSLayoutConstraint:0x280db2b70 UIButton:0x102315180.width == 2*UIButton:0x102315180.height (active)>"
)
Will attempt to recover by breaking constraint
<NSLayoutConstraint:0x280db2b70 UIButton:0x102315180.width == 2*UIButton:0x102315180.height (active)>
I see that it's trying to set width twice but I don't know why. I'm not even sure if my approach to call this function every time to setup constraints is the right one.

Here's one way to handle it:
Create a struct ButtonSizeConstraints:
struct ButtonSizeConstraints {
let widthConStraint: NSLayoutConstraint
let heightConstraint: NSLayoutConstraint
}
Give your view an array of ButtonSizeConstraints, once for each button. (You could also have separate variables firstButtonSizeConstraints, secondButtonSizeConstraints, etc, but by making it an array you can loop through it.)
In your setupConstraints function, put those constraints into local vars, add them to your array of ButtonSizeConstraints, and activate them outside of the code you posted.
Then, when you load a new image into one or more of your buttons, fetch that button's ButtonSizeConstraints, update that constraint's constant value, and call the parent view's layoutIfNeeded() method.

Related

Swift iOS Layout Constraint. Using Constant that is proportional to view height programatically

I am laying out a view programatically in Swift for iOS, but struggling to get my constraint quite how I want it. This is what I currently have:
NSLayoutConstraint.activate([
Logo.leadingAnchor.constraint(equalTo: self.leadingAnchor, constant: 30),
Logo.trailingAnchor.constraint(equalTo: self.trailingAnchor, constant: -30),
Logo.heightAnchor.constraint(equalToConstant: 55),
Logo.topAnchor.constraint(equalTo: label.bottomAnchor, constant: 150),
])
This is fine on large screens but as the screen gets smaller I want to close the space between the Logo and the label. Currently this is set to a fixed constant of 150. What I would like to do is use a multiplier here that is based on the view height (or something similar) but I can not figure that out. How should I define the constraint to do this? Thanks!
You can try
/* Play with percent as you need also you can check current
device type iphone/ipad and set it accordingly */
let height = UIScreen.main.bounds.height * 0.25
logo.topAnchor.constraint(equalTo: label.bottomAnchor, constant: height),

UITableView cell constraints problem - Swift

I started to get this constraints error:
"<NSLayoutConstraint:0x6000014419a0 V:|-(20)-[UIImageView:0x7f809779fae0] (active, names: '|':mChat.ChatCell:0x7f8097534600'ChatCell' )>",
"<NSLayoutConstraint:0x600001441b30 UIImageView:0x7f809779fae0.bottom == mChat.ChatCell:0x7f8097534600'ChatCell'.bottom - 20 (active)>",
"<NSLayoutConstraint:0x600001440eb0 UIImageView:0x7f809779fae0.height == 150 (active)>",
"<NSLayoutConstraint:0x600001440550 'UIView-Encapsulated-Layout-Height' mChat.ChatCell:0x7f8097534600'ChatCell'.height == 60.5 (active)>"
The problem appears when the UITableView has at least two images in it, then I get these constraints errors. Or when I add another image to the UITableView.
Here are my constraints:
func setupMediaMessage(){
mediaMessage.translatesAutoresizingMaskIntoConstraints = false
mediaMessage.contentMode = .scaleAspectFill
mediaMessage.backgroundColor = .lightGray
mediaMessage.layer.cornerRadius = 16
mediaMessage.layer.masksToBounds = true
let constraints = [
mediaMessage.topAnchor.constraint(equalTo: topAnchor, constant: 20),
mediaMessage.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -20),
mediaMessage.widthAnchor.constraint(equalToConstant: 150),
mediaMessage.heightAnchor.constraint(equalToConstant: 150)
]
outcomingConstraint = mediaMessage.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 24)
incomingConstraint = mediaMessage.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -24)
NSLayoutConstraint.activate(constraints)
}
Don't worry about the trailing and leading constraints, I'm just checking if those images are incoming or outcoming.
My Github repo: https://github.com/realpaliy/mChat/blob/master/mChat/Controllers/Chats/ChatCell.swift. So, the UITableView looks good, but why do I get those errors?
If the layout looks correct, the conflicts are likely due to the internal auto-layout engine, and the order in which the constraints are evaluated.
Try this -- it should get rid of the warnings, without changing the end result:
let bAnchor = mediaMessage.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -20)
bAnchor.priority = UILayoutPriority(rawValue: 999)
let constraints = [
mediaMessage.topAnchor.constraint(equalTo: topAnchor, constant: 20),
//mediaMessage.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -20),
bAnchor,
mediaMessage.widthAnchor.constraint(equalToConstant: 150),
mediaMessage.heightAnchor.constraint(equalToConstant: 150)
]
EDIT
Note: I do not work for Apple - these are simply my observations:
When a cell is instantiated and / or dequeued, auto-layout uses the current row height to lay out the cell contents. It then uses the constraints on the cell contents to determine the actual row height. This can cause conflict warnings if the row is not tall enough for the generated height. By changing the Priority on the bottom-most constraint - or, I believe, on explicit element height constraints - to 999, that allows auto-layout to first break the constraint and then re-enforce it... without generating debug warnings.
You should (may?) also be able to eliminate the warnings by setting a big enough value for tableView.estimatedRowHeight.

Updating Frame/Constraints for programmatically added UIView on orientation change

I've got a UIView that I'm adding programmatically with the following code in viewDidLoad
dropMenuView = YNDropDownMenu(frame: CGRect(x: 0.0, y: 0, width: UIScreen.main.bounds.width, height: 40.0), dropDownViews: [typeView, brandView, priceView], dropDownViewTitles:["Type", "Brand", "Price"])
self.view.addSubview(dropMenuView)
The above code creates a view with the correct width for the current orientation, but if a user changes their device orientation, the width of the view does not update. I've tried adding a new constraint in viewWillTransitionTo:
override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
let widthConstraint = NSLayoutConstraint(item: dropMenuView, attribute: NSLayoutAttribute.width, relatedBy: NSLayoutRelation.equal, toItem: self.view, attribute: NSLayoutAttribute.notAnAttribute, multiplier: 1, constant: 100)
dropMenuView.addConstraints([widthConstraint])
dropMenuView.layoutIfNeeded()
}
I've tried a number of mutations on the above code trying to get it to work. Generally the error I get is:
'NSLayoutConstraint for >: Unknown layout attribute'
I can't help but think I'm going about this in the wrong way. Is adding new constraints upon orientation change the way to go? I tried just adding the equal width constraint after I add the subview, but I get the same error. I've never really added constraints programmatically before, so these are uncharted waters for me. What would be the best approach here?
Adding constraints during an orientation change is not a good idea. If you were to do it, you'd need to remove the previously added constraints to avoid over constraining your view.
The constraint you are trying to create isn't correct. You only use .notAnAttrubute when you are passing nil as the second view.
I suggest you use layout anchors instead. They're easier to write and read:
dropMenuView = YNDropDownMenu(frame: CGRect.zero, dropDownViews: [typeView, brandView, priceView], dropDownViewTitles:["Type", "Brand", "Price"])
self.view.addSubview(dropMenuView)
// you need to set this if you are adding your own constraints and once
// yet set it, the frame is ignored (which is why we just passed CGRect.zero
// when creating the view)
dropMenuView.translatesAutoresizingMaskIntoConstraints = false
// Create the constraints and activate them. This will set `isActive = true`
// for all of the constraints. iOS knows which views to add them to, so
// you don't have to worry about that detail
NSLayoutConstraint.activate([
dropMenuView.topAnchor.constraint(equalTo: view.topAnchor),
dropMenuView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
dropMenuView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
dropMenuView.heightAnchor.constraint(equalToConstant: 40)
])

How to properly resize the most outside view in a custom keyboard extension ViewController using swift 3?

I have a function that sets the height of a custom keyboard extension, depending on the phone. I originally just tried this in viewDidLoad():
self.view.heightAnchor.constraint(equalToConstant: 100)
This didn't seem to work, so I made a function:
func updateHeightOfView() {
var currentKeyboardInView: String!
if currentViewHeightConstraint != nil {
view.removeConstraint(currentViewHeightConstraint!)
}
currentViewHeightConstraint = NSLayoutConstraint(item: view, attribute: NSLayoutAttribute.height, relatedBy: NSLayoutRelation.equal, toItem: nil, attribute: NSLayoutAttribute.notAnAttribute, multiplier: 1.0, constant: desiredHeight)
view.addConstraint(currentViewHeightConstraint!)
}
that way when the screen changes it's orientation, I resize the view. The second block of code works perfectly, but it throws warnings of layoutConstraints, so I was wondering why the first block of code isn't working, and if there is an easier way to UPDATE constraints of the height property of a view rather than add and remove them. This view is the most outside view in the viewController.
Here's the warning the second block spits out:
[LayoutConstraints] Unable to simultaneously satisfy constraints.
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.
(
"<NSLayoutConstraint:0x608000298b00 App.KeyboardAccessoryView:0x7fdc00b1bfe0.height == 258 (active)>",
"<NSLayoutConstraint:0x600000297e80 'UIView-Encapsulated-Layout-Height' App.KeyboardAccessoryView:0x7fdc00b1bfe0.height == 216 (active)>"
Will attempt to recover by breaking constraint
<NSLayoutConstraint:0x608000298b00 App.KeyboardAccessoryView:0x7fdc00b1bfe0.height == 258 (active)>
As much detail you given here according to this I think where ever you set height of this view it is not static constant value but in view didload you are setting constant value for its height. So that's by it is not working.
In second block you are getting warning because may be your view is getting height from more than one way. To check this in updathHeight method just remove the constraint , don't add and try to run.
Exact solution could be given only looking on your constraints.

iOS constraints doesn't allow to use multiplier

I am trying to layout some custom views and when I try to activate the constraints, Xcode says that I can't use multiplier. Here is an example of the code:
class MenuView: UIView {
var addButton: AddButton!
var settingsButton: SettingsButton!
// ........
func setConstraints (withBarReference reference: NSLayoutYAxisAnchor) {
NSLayoutConstraints.activateConstraints([
// ........
addButton.centerXAnchor.constraintEqualToAnchor(self.centerXAnchor, multiplier: 0.5),
// ........
settingsButton.centerXAnchor.constraintEqualToAnchor(self.centerXAnchor, multiplier: 1.5)
])
}
}
The thing here is that Xcode gives a syntax error on the contraintEqualToAnchor: functions and says that I should replace "multiplier" to "constant".
Why can't I use the multiplier option with the X center anchors?
You can't set multiplier using helper functions, but you can set multiplier using NSLayoutConstraint initializer. Just got stuck by this myself, but found the answer.
Your code: addButton.centerXAnchor.constraintEqualToAnchor(self.centerXAnchor, multiplier: 0.5)
Correct code: NSLayoutConstraint(item: addButton, attribute: .centerX, relatedBy: .equal, toItem: self, attribute: .centerX, multiplier: 0.5, constant: 0)
Also, don't forget to activate this constraint by typing isActive = true
Previous answers work very weird now.
You can simply create UILayoutGuide with multiplier width/height with view and set guide.trailing equal to the centerX of your subview.
For example, if you need to place the addButton in the first 1/3 of a view and settingsButton in 2/3 you can simply set two layout guides
let addButtonGuide = UILayoutGuide()
self.addLayoutGuide(addButtonGuide)
addButtonGuide.widthAnchor.constraint(equalTo: self.widthAnchor, multiplier: 1/3).isActive = true
addButtonGuide.leadingAnchor.constraint(equalTo: self.leadingAnchor).isActive = true
addButton.centerXAnchor.constraint(equalTo: addButtonGuide.trailingAnchor).isActive = true
// same for settingsButton but using 2/3 for the multiplier
But the really best way is to use UIStackView and set its distribution property to equalCentering.
Another option is to use uncommon Auto Layout API to create NSLayoutDimension between two centerXAnchors and make constraint to self.widthAnchor:
addButton.centerXAnchor.anchorWithOffset(to: self.centerXAnchor)
.constraint(equalTo: self.widthAnchor, multiplier: 0.25).isActive = true
self.centerXAnchor.anchorWithOffset(to: settingsButton.centerXAnchor)
.constraint(equalTo: self.widthAnchor, multiplier: 0.25).isActive = true
It seems that in IB you can use the multiplier option with Center X and obtain the effect you're looking for (set the center of button1 at 1/4 the width of the view it's in, and the center of button2 at 2/3 of the width of the view it's in):
.
I tried to use it both in code and in IB, and in code I got the same error as you.
Surprisingly, in IB it worked, no errors, no warnings. (I am using Xcode 7, will try it in Xcode 8 to see if it still works).
You can't use multipliers on NSLayoutXAxisAnchor anchors - multiplying by a position along a line doesn't make sense in a way that the constraints system can understand. You can only use multipliers with NSLayoutDimension anchors, which measure lengths, like the width.
The layout you are trying to make would be better achieved using a stack view.

Resources