Changing Auto Layout Anchor Constrains - swift

I have a UITextField, and I'm trying to change its position when the keyboard comes up. More specifically, I want to move the text field up so that it sits just above the keyboard. My code looks something like this
let textField = myCustomTextField()
override func viewDidLoad() {
//set up textfield here
//constrains blah blah
textField.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor,
constant: -view.bounds.height * 0.05).isActive = true
//more constraints
}
What I want to do next is change that constraint so that it raises up the textfield when the keyboard comes up. My code looks like this:
#objc func keyboardWillShow(notification: NSNotification) {
textField.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor,
constant: -view.bounds.height * 0.05).constant = 200 //200 is a sample number I have a math calculation there
textField.layoutIfNeeded()
}
That doesn't work because referencing that constraint by just using constraint(equalTo: constant:) doesn't actually return any constraint. Is there any way to reference that constraint without creating a variable for each constraint I want to change and changing its constant?

The problem with your code is that you create a second constraint instead of changing the current , you should hold a reference to the created on and change it's constant , you can reference it like this
var bottomCon:NSLayoutConstraint?
override func viewDidLoad() {
bottomCon = textField.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor,
constant: -view.bounds.height * 0.05)
bottomCon.isActive = true
}
then you can access it in any method and play with constant property
Edit: for every constraint you create assign identifier and access it as below
let textFieldCons= button.constraints.filter { $0.identifier == "id" }
if let botCons = textField.first {
// process here
}

Related

How to change the width of an NSView in a transparent window

(Swift, macOS, storyboard)
I have an NSView in a transparent window
I have this in the viewDidLoad. To make the window transparent and the NSView blue:
DispatchQueue.main.asyncAfter(deadline: .now() + 0.2){
self.view.window?.isOpaque = false
self.view.window?.backgroundColor = NSColor.clear
}
view1.wantsLayer = true
view1.layer?.backgroundColor = NSColor.green.cgColor
I want to change the width with code when I click a button.
If it has constraints:
#IBAction func button1(_ sender: NSButton) {
view1Width.constant = 74
}
I tried without constraints and different ways to change the width. They all give the same results:
view1.frame = NSRect(x:50, y:120, width:74, height:100)
But there is still a border and a shadow where the old shape was. Why does it happen and how to solve it?
It only happens in specific circumstances:
If the window is transparent (and macOS)
I change the width and do not change the position y
The window must be active. If it is not (If I click to anywhere else) it looks as it should: the shadow around the changed NSView green.
(I have simplified the case to try to find a solution. I have created a new document and there is only this code and I am sure there is no other element)
Since the window is transparent you need to invalidate the shadows.
Apple states about invalidateShadow()
Invalidates the window shadow so that it is recomputed based on the current window shape.
Complete Self-Contained Test Program
It sets up the UI pogrammatically instead of using a storyboard. Other than that, the code is very close to your example.
Note the line:
view.window?.invalidateShadow()
in the onChange method.
import Cocoa
class ViewController: NSViewController {
private let view1 = NSView()
private let changeButton = NSButton()
private var view1Width: NSLayoutConstraint?
override func viewDidLoad() {
super.viewDidLoad()
setupUI()
DispatchQueue.main.asyncAfter(deadline: .now() + 0.2){
self.view.window?.isOpaque = false
self.view.window?.backgroundColor = NSColor.clear
}
view1.wantsLayer = true
view1.layer?.backgroundColor = NSColor.green.cgColor
}
#objc private func onChange() {
view1Width?.constant += 32
view.window?.invalidateShadow()
}
private func setupUI() {
changeButton.title = "change"
changeButton.bezelStyle = .rounded
changeButton.setButtonType(.momentaryPushIn)
changeButton.target = self
changeButton.action = #selector(onChange)
self.view.addSubview(view1)
self.view.addSubview(changeButton)
self.view1.translatesAutoresizingMaskIntoConstraints = false
self.changeButton.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
view1.centerXAnchor.constraint(equalTo: view.centerXAnchor),
view1.centerYAnchor.constraint(equalTo: view.centerYAnchor),
view1.heightAnchor.constraint(equalToConstant: 128),
changeButton.topAnchor.constraint(equalTo: view1.bottomAnchor, constant:16),
changeButton.centerXAnchor.constraint(equalTo: view1.centerXAnchor)
])
view1Width = view1.widthAnchor.constraint(equalToConstant: 128)
view1Width?.isActive = true
}
}
Result
The desired result with an update of the shadows is accomplished:

Updating constraints when device orientation change

I want to change a button height constraint according to device orientation. I am creating with height constraint. And then I am setting height constraint 60 for landscape mode, 40 for portrait mode. But when I change device orientation, height is not becoming bigger. Where is the problem. Here is my code
lazy var nextEpisodeButton: CustomPlayerButton = {
let nextEpisode = CustomPlayerButton(type: .nextEpisode, backgroundImage: nil)
nextEpisode.addTarget(self, action: #selector(nextEpisodeTapped), for: .touchUpInside)
nextEpisode.translatesAutoresizingMaskIntoConstraints = false
nextEpisode.adjustsImageWhenHighlighted = false
return nextEpisode
}()
func addNextEpisodeButton() {
view.addSubview(nextEpisodeButton)
NSLayoutConstraint.activate([
nextEpisodeButton.heightAnchor.constraint(equalToConstant: 40),
nextEpisodeButton.widthAnchor.constraint(equalToConstant: 120),
nextEpisodeButton.rightAnchor.constraint(equalTo: view.safeRightAnchor, constant: -20),
nextEpisodeButton.bottomAnchor.constraint(equalTo: view.safeBottomAnchor, constant: -60)
])
}
override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
super.willTransition(to: size, with: coordinator)
if UIDevice.current.orientation.isLandscape {
nextEpisodeButton.heightAnchor.constraint(equalToConstant: 60).isActive = true
} else {
nextEpisodeButton.heightAnchor.constraint(equalToConstant: 40).isActive = true
}
nextEpisodeButton.layoutIfNeeded()
}
You should have a reference to nextEpisodeButton.heightAnchor.constraint(equalToConstant: 40) constraint somewhere in your ViewController and in willTransition callback just change its constant value. With your code you are creating and activating a new constraint every time you rotate rather than changing the existing one.
The constraints are persistent. When you are creating a second constraint for the same attribute (in your case the height of a view) and you are activating it, the 2 constraints are conflicting with each other.
So, you need to keep a reference for both of them and activate/deactivate them accordingly:
lazy var nextEpisodeButton: CustomPlayerButton = {
let nextEpisode = CustomPlayerButton(type: .nextEpisode, backgroundImage: nil)
nextEpisode.addTarget(self, action: #selector(nextEpisodeTapped), for: .touchUpInside)
nextEpisode.translatesAutoresizingMaskIntoConstraints = false
nextEpisode.adjustsImageWhenHighlighted = false
return nextEpisode
}()
private var buttonLandscapeHeightContraint: NSLayoutConstraint?
private var buttonPortraitHeightContraint: NSLayoutConstraint?
func addNextEpisodeButton() {
view.addSubview(nextEpisodeButton)
buttonLandscapeHeightContraint = nextEpisodeButton.heightAnchor.constraint(equalToConstant: 60)
buttonPortraitHeightContraint = nextEpisodeButton.heightAnchor.constraint(equalToConstant: 40)
NSLayoutConstraint.activate([
nextEpisodeButton.widthAnchor.constraint(equalToConstant: 120),
nextEpisodeButton.rightAnchor.constraint(equalTo: view.safeRightAnchor, constant: -20),
nextEpisodeButton.bottomAnchor.constraint(equalTo: view.safeBottomAnchor, constant: -60)
])
updateButtonHeightConstraint()
}
override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
super.willTransition(to: size, with: coordinator)
updateButtonHeightConstraint()
nextEpisodeButton.layoutIfNeeded()
}
private func updateButtonHeightConstraint() {
if UIDevice.current.orientation.isLandscape {
buttonPortraitHeightContraint?.isActive = false
buttonLandscapeHeightContraint?.isActive = true
} else {
buttonLandscapeHeightContraint?.isActive = false
buttonPortraitHeightContraint?.isActive = true
}
}
My way is to do two things:
Keep the constraints you wish to change in arrays to be activated and deactivated.
Detect device orientation through a means other than traits.
Let's start with the first. And you are off to a good start - the constraints are (a) in code and (b) using anchors. (If by chance you are using IB - Storyboards for others - you'll need to set the changing constraints as #IBOutlets.)
It looks like you are wanting this button to be in the right bottom, so let's make those constraints active:
nextEpisodeButton.rightAnchor.constraint(equalTo: view.safeRightAnchor, constant: -20).isActive = true
nextEpisodeButton.bottomAnchor.constraint(equalTo: view.safeBottomAnchor, constant: -60).isActive = true
This will pin things properly no matter what the orientation.
Now, let's say you want to change size. You need to put these into two arrays:
var portraitLayout = [NSLayoutConstraint]()
var landscapeLayout = [NSLayoutConstraint]()
portraitLayout.append(nextEpisodeButton.widthAnchor.constraint(equalToConstant: 120))
portraitLayout.append(nextEpisodeButton.heightAnchor.constraint(equalToConstant: 40))
landscapeLayout.append(nextEpisodeButton.widthAnchor.constraint(equalToConstant: 120))
landscapeLayout.append(nextEpisodeButton.heightAnchor.constraint(equalToConstant: 60))
This sets up a 40x120 button in portrait and a 60x120 button in landscape. This can (should?) be done in viewDidLoad. Now it's time to activate/deactivate....
Only one array should be active, and you'll need to do one at the time the view is initialized. I'll get to that, but first, let me show two lines of code that is necessary:
NSLayoutConstraint.deactivate(landscapeLayout)
NSLayoutConstraint.activate(portraitLayout)
You can try to add/delete constraints, but this is not only risky and not as easy to maintain, it's not even needed. Simply set all constraints the are constant as isActive = true, put the ones that change into arrays, and activate/deactive.
(If you want to animate such changes - I wouldn't for you - then do this and add UIView.animate(withDuration:) at the end.)
Now, the rough piece, detecting the orientation.
Apple decided to add trait collections a few years ago. They work well (and this year I finally get why they did it the way they did). But they have one serious issue - iPads in full screen mode always have a normal size. (I'm writing an iPad only app this year and in split screen mode it may be compact.)
Your question stressed orientation, so I'd recommend not use trait collection changes. Instead, use viewWillLayoutSubviews. For me, this seems to be more reliable - it's the earliest in the view controller lifecycle that I've found. You'll need to do two things... set the initial orientation and detect changes.
Here's my setup. In a UIView extension:
public func orientationHasChanged(_ isInPortrait:inout Bool) -> Bool {
if self.frame.width > self.frame.height {
if isInPortrait {
isInPortrait = false
return true
}
} else {
if !isInPortrait {
isInPortrait = true
return true
}
}
return false
}
public func setOrientation(_ p:[NSLayoutConstraint], _ l:[NSLayoutConstraint]) {
NSLayoutConstraint.deactivate(l)
NSLayoutConstraint.deactivate(p)
if self.bounds.width > self.bounds.height {
NSLayoutConstraint.activate(l)
} else {
NSLayoutConstraint.activate(p)
}
}
p and l are portrait and landscape respectively. All I do is simply check the bounds and active/deactive appropriately.
override func viewWillLayoutSubviews() {
super.viewDidLayoutSubviews()
if initialOrientation {
initialOrientation = false
if view.frame.width > view.frame.height {
isInPortrait = false
} else {
isInPortrait = true
}
view.setOrientation(p, l)
} else {
if view.orientationHasChanged(&isInPortrait) {
view.setOrientation(p, l)
}
}
}
This is likely overkill for your needs. I'm basically tracking two things (initial and current orientation) for changes and calling things when needed - viewWillLayoutSubviews can be called more than once during a load(?) and for other reasons than an orientation change.
Conclusion:
You are close, but you have a few changes. First, set your constant constraints to isActive = true and activate/deactivate the remaining ones as arrays. Second, unless your app is iPhone only (and even then it will still be available for iPad) do not use trait collections, but instead use the view controller lifecycle and the screen bounds.

Why is my UIView not displaying correctly when updating autolayout constraints?

I am trying to animate a simple autolayout constraint change, but when I call it the first time it animates but instead of just stretching and changing the height, it moves the whole view up, if I call it again it then fixes itself.
Here is how I set up the constraints:
hiddenView.addSubview(topView)
topView.translatesAutoresizingMaskIntoConstraints = false
topView.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
topView.widthAnchor.constraint(equalTo: view.widthAnchor).isActive = true
topView.bottomAnchor.constraint(equalTo: hiddenView.bottomAnchor).isActive = true
topViewDefaultTopAnchorConstraints.append(topView.topAnchor.constraint(equalTo: hiddenView.centerYAnchor))
topViewSelectedTopAnchorConstraints.append(topView.topAnchor.constraint(equalTo: hiddenView.topAnchor))
NSLayoutConstraint.activate(topViewDefaultTopAnchorConstraints)
And here is how I am updating them:
func showTopView() {
NSLayoutConstraint.deactivate(topViewDefaultTopAnchorConstraints)
NSLayoutConstraint.activate(topViewSelectedTopAnchorConstraints)
UIView.animate(withDuration: 0.25) {
self.view.layoutIfNeeded()
}
}
Update:
Here is a gif of what happens when calling showTopView, calling it again fixes the bottom constraint:
It should just animate up like in the second image, not bringing the whole view up, as the bottomAnchor does not change, how can I fix this?
Update: I realised that I am rounding the corners of topView and bottomView, if I don't round the top corners then it works correctly, so it has something to do with this.
override func viewDidLayoutSubviews() {
topView.roundCorners(corners: [.topLeft, .topRight], radius: 100*ScreenDimensions.ASPECT_RATIO_RESPECT_OF_XMAX)
bottomView.roundCorners(corners: [.topLeft, .topRight], radius: 100*ScreenDimensions.ASPECT_RATIO_RESPECT_OF_XMAX)
topConstraint = topView.topAnchor.constraint(equalTo: hiddenView.topAnchor, constant: hiddenView.frame.height/2)
topConstraint.isActive = true
}
Seems the hiddenView's bottomAnchor is pulled up along with the topView's bottom here. Make sure the hiddenView's bottomAnchor is constrained properly. And better way to do this would be to provide a heightAnchor and increase the heightConstraint constant value to animate the view to increased height.
I think I know what your issue is. You might need to play with the constant rather than having two different constraint defined that you activate/deactivate.
First, define a constraint a the top of your ViewController like so:
var topConstraint: NSLayoutConstraint!
Then, initialize it as shown below:
hiddenView.addSubview(topView)
topView.translatesAutoresizingMaskIntoConstraints = false
topView.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
topView.widthAnchor.constraint(equalTo: view.widthAnchor).isActive = true
topView.bottomAnchor.constraint(equalTo: hiddenView.bottomAnchor).isActive = true
topConstraint = topView.topAnchor.constraint(equalTo: hiddenView.topAnchor, constant: hiddenView.frame.height/2)
topConstraint.isActive = true
Then inside showTopView, you only need to update the constant and that should create the animation you are looking for:
func showTopView() {
topConstraint.isActive = false
topConstraint.constant = 0
topConstraint.isActive = true
UIView.animate(withDuration: 0.25) {
self.view.layoutIfNeeded()
}
}
Similarly if you want to put the view back to normal, you'd implement it as follows:
func hideTopView() {
topConstraint.isActive = false
topConstraint.constant = hiddenView.frame.height/2
topConstraint.isActive = true
UIView.animate(withDuration: 0.25) {
self.view.layoutIfNeeded()
}
}
You may have better luck with this approach:
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
let r = 100*ScreenDimensions.ASPECT_RATIO_RESPECT_OF_XMAX
topView.layer.cornerRadius = r
topView.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner]
bottomView.layer.cornerRadius = r
bottomView.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner]
}

Update an UITextView size that is set via NSLayout in Swift 3

I have a variable in my UIViewController that is tied to a constraint setting up the UITextView's height:
var textViewHeight: Int!
Here is the constraint:
self.view.addConstraintsWithFormat(format: "V:|-74-[v0(\(textViewHeight!))]", views: self.textView)
I use this extension:
extension UIView
{
func addConstraintsWithFormat(format: String, views: UIView...)
{
var viewDict = [String: AnyObject]()
for (index, view) in views.enumerated()
{
view.translatesAutoresizingMaskIntoConstraints = false
let key = "v\(index)"
viewDict[key] = view
}
addConstraints(NSLayoutConstraint.constraints(withVisualFormat: format, options: NSLayoutFormatOptions(), metrics: nil, views: viewDict))
}
}
I have set up a Notification that triggers when the keyboard shows up.
It is triggered correctly (I have a print and it always fires correctly) and the function that is executed includes this code:
if let keyboardSize = sender.userInfo?[UIKeyboardFrameBeginUserInfoKey] as? CGRect {
print(keyboardSize.height)
self.textViewHeight = Int(self.view.frame.height-keyboardSize.height-100)
self.view.updateConstraints()
}
The keyboard's height is printed correctly but the text view's height is not changed.....
Thank you in advance!
Simply setting the constraints once with visual format will not update the constraints later if the variable value (in this case textViewHeight) changes later. So, you'd have to actually set up a constraint via code that can be modified later as the textViewHeight value changes.
Here are the changes you'd need:
1: Add a variable to hold a reference to the constraint you'll want to modify later.
var heightConstraint:NSLayoutConstraint!
2: Create the constraints for your text view individually instead of using the visual format (self.view.addConstraintsWithFormat(format: "V:|-74-[v0(\(textViewHeight!))]", views: self.textView))
// Add vertical constraints individually
let top = NSLayoutConstraint(item:textView, attribute: NSLayoutAttribute.top, relatedBy: NSLayoutRelation.equal, toItem:topLayoutGuide, attribute: NSLayoutAttribute.bottom, multiplier:1.0, constant:74.0)
heightConstraint = NSLayoutConstraint(item:textView, attribute:NSLayoutAttribute.height, relatedBy: NSLayoutRelation.equal, toItem:nil, attribute:NSLayoutAttribute.notAnAttribute, multiplier:1.0, constant:textViewHeight)
view.addConstraint(top)
view.addConstraint(heightConstraint)
3: You are probably better off changing textViewHeight to CGFloat since all the values you'll deal with there would be CGFloat values rather than Int.
4: Where you get the keyboard notification, after you calculate textViewHeight, add the following line:
self.heightConstraint.constant = textViewHeight
And that should do the trick since now, when textViewHeight changes, the constraint will be updated as well :)

How to add constraints to a dynamically created UITextField?

I have created UITextFields dynamically. Now i want to refer to the TextFields to check for some constraints. How do i do so?
func displayTextBox1(height: Int, placeHolder: String, xtb: Int, ytb: Int, lableName: String, xl: Int, yl: Int) {
DispatchQueue.main.async{
self.textField = UITextField(frame: CGRect(x: xtb, y: ytb, width: 343, height: height))
self.textField.textAlignment = NSTextAlignment.left
self.textField.textColor = UIColor.black
self.textField.borderStyle = UITextBorderStyle.line
self.textField.autocapitalizationType = UITextAutocapitalizationType.words // If you need any capitalization
self.textField.placeholder = placeHolder
print("hi")
self.view.addSubview(self.textField)
self.displayLabel(labelName: lableName, x: xl, y: yl)
}
}
You can set constraints programmaticaly using the sample explained code below:
let constraint = NSLayoutConstraint(item: self.textField, attribute: NSLayoutAttribute.width, relatedBy: NSLayoutRelation.equal, toItem: self.view, attribute: NSLayoutAttribute.width, multiplier: 1, constant: -50)
NSLayoutConstraint.activate([constraint])
As you can see, I am creating a constraint for item textField which width should be equal to width of view multiplied by 1 minus 50. That means the width of your textField will be 50 pixels less than the width of the view. The last line of code activates given set of created constraints.
Welcome to Stackoverflow, and hope you're enjoying learning Swift!
I'm going to make some assumptions based on your code snippet:
the details you need to create the textfield and label (position, placeholder text etc) are coming from some service that operates on a background thread (perhaps a HTTP request?) which is why you're using DispatchQueue.main.async to perform UI events back on the main thread.
there will be multiple textfield/label pairs that you're going to configure and add to the interface (not just a single pair)... perhaps a 'todo' list sort of app where a label and textfield let people enter a note (more on this in part 2 of answer) which is why these views (and constraints) are being created in code rather than in a storyboard.
you want to invest in a constraint-based layout rather than frame-based positioning.
Answer part 1
If any of those assumptions are incorrect, then parts of this answer probably won't be relevant.
But assuming the assumptions are correct I suggest a couple things:
Use a separate helper methods to create a textfield/view and return the result (rather than doing everything in a single method) -- methods that have a single purpose will make more sense and be easier to follow.
Don't use a mixture of setting view position/size with frame and constraints - use one approach or the other (since you're new it will be easier to keep a single mental model of how things are working rather than mixing).
Here's a snippet of what a view controller class might start to look like:
import UIKit
class ViewController: UIViewController {
func triggeredBySomeEvent() {
// assuming that you have some equivilent to `YouBackgroundRequestManager.getTextFieldLabelDetails`
// that grabs the info you need to create the label and textfield on a background thread...
YouBackgroundRequestManager.getTextFieldLabelDetails { tfPlaceholder, tfHeight, tfX, tfY, labelName, labelX, labelY in
// perform UI work on the main thread
DispatchQueue.main.async{
// use our method to dynamically create a textfield
let newTextField: UITextField = self.createTextfield(with: tfPlaceholder, height: tfHeight)
// add textfield to a container view (in this case the view controller's view)
self.view.addSubview(newTextField)
// add constraints that pin the textfield's left and top anchor relative to the left and top anchor of the view
newTextField.leftAnchor.constraint(equalTo: self.view.leftAnchor, constant: tfX).isActive = true
newTextField.topAnchor.constraint(equalTo: self.view.topAnchor, constant: tfY).isActive = true
// repeat for label...
let newLabel: UILabel = self.createLabel(with: labelName)
self.view.addSubview(newLabel)
newLabel.leftAnchor.constraint(equalTo: self.view.leftAnchor, constant: labelX).isActive = true
newLabel.topAnchor.constraint(equalTo: self.view.topAnchor, constant: labelY).isActive = true
}
}
}
// create, configure, and return a new textfield
func createTextfield(with placeholder: String, height: CGFloat) -> UITextField {
let textField = UITextField(frame: .zero) // set the frame to zero because we're going to manage this with constraints
textField.placeholder = placeholder
textField.textAlignment = .left
textField.textColor = .black
textField.borderStyle = .line
textField.autocapitalizationType = .words
// translatesAutoresizingMaskIntoConstraints = false is important here, if you don't set this as false,
// UIKit will automatically create constraints based on the `frame` of the view.
textField.translatesAutoresizingMaskIntoConstraints = false
textField.heightAnchor.constraint(equalToConstant: height).isActive = true
textField.widthAnchor.constraint(equalToConstant: 343.0).isActive = true
return textField
}
// create, configure and return a new label
func createLabel(with labelName: String) -> UILabel {
let label = UILabel()
label.text = labelName
label.translatesAutoresizingMaskIntoConstraints = false
return label
}
}
Answer part 2
I'm struggling to imagine the situation where you actually want to do this. If you are making a UI where elements repeat themselves (like a todo list, or maybe a spreadsheet-type interface) then this approach is not the right way to do.
If you want to create a UI where elements repeat themselves as repeating elements in a single column you should investigate using a UITableViewController where you create a cell that represents a single element, and have a tableview manage that collection of cells.
If you want to create a UI where elements repeat themselves in any other way than a vertical list, then you investigate using UICollectionViewController which is a little more complex, but a lot more powerful.
Apologies if this answer goes off-topic, but hope that inspires some ideas that are useful for you.