I have 4 tableviews in a stackView when updating the width constraint of each tableview in causes it to scroll to top.
I need to prevent this auto scroll
self.mainCatTVWidthC = self.view.updateMultiplier(for:self.mainCatTVWidthC, with: 0.15)
extension NSLayoutConstraint {
func sameWithDifferentMultiplierOf(value multiplier: CGFloat) -> NSLayoutConstraint {
return NSLayoutConstraint(item: self.firstItem!, attribute: self.firstAttribute, relatedBy: self.relation, toItem: self.secondItem, attribute: self.secondAttribute, multiplier: multiplier, constant: self.constant)
}
}
extension UIView {
func updateMultiplier(for constraint: NSLayoutConstraint, with value: CGFloat) -> NSLayoutConstraint {
let newConstraint = constraint.sameWithDifferentMultiplierOf(value: value)
self.removeConstraint(constraint)
self.addConstraint(newConstraint)
self.layoutIfNeeded()
return newConstraint
}
}
I expect the tableview width to change and let the scroll position as it is but it scrolls to top
Related
Let's assume I have this layout design on an iPhone Portrait.
A Label (red) and an Image (blue).
But I would like to have it this way when the iPhone is in landscape. Only the image (blue).
I trying with following code but the label does not appear after return back to portrait:
override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
coordinator.animate(alongsideTransition: { (UIViewControllerTransitionCoordinatorContext) -> Void in
let orient = UIDevice.current.orientation
switch orient {
case .portrait:
print("Portrait")
let ctop = NSLayoutConstraint(item: self.imageView, attribute: .top, relatedBy: .equal, toItem: self.view, attribute: .top, multiplier: 1, constant: 44)
self.view.removeConstraint(ctop)
self.labelView.isHidden = false
break
default:
print("LandScape")
let ctop = NSLayoutConstraint(item: self.imageView, attribute: .top, relatedBy: .equal, toItem: self.view, attribute: .top, multiplier: 1, constant: 0)
self.view.addConstraint(ctop)
self.labelView.isHidden = true
break
}
}, completion: { (UIViewControllerTransitionCoordinatorContext) -> Void in
print("rotation completed")
})
}
You have a mixup with your addConstraint/removeConstraint logic. In the Portrait case you define a new cTop constraint and remove it (!?!) from the view - you probably want to add it.
It would be better to define both constraints right away when you create your views, and only set one to active - and then switch the isActive setting on both according to your new orientation. Or only define one constraint and just change the constant on it accordingly.
I'm setting up a constraint in viewDidLoad and then changing its constant when the keyboard shows up.
This is the initial set Up
bottomConstraint = NSLayoutConstraint(item: bottomBar, attribute: .bottom, relatedBy: .equal, toItem: view, attribute: .bottom, multiplier: 1, constant: 0)
view.addConstraint(bottomConstraint)
Then I change the constant when I receive a keyboard notification:
#objc func handleKeyboardNotification(notification: NSNotification){
if let keyboardFrame: NSValue = notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue {
let keyboardRectangle = keyboardFrame.cgRectValue
let keyboardHeight = keyboardRectangle.height
let isKeyboardShowing = notification.name == UIResponder.keyboardWillShowNotification
bottomConstraint?.constant = isKeyboardShowing ? -keyboardHeight : 0
UIView.animate(withDuration:0.1, delay: 0 , options: .curveEaseOut , animations: {
self.view.layoutIfNeeded()
} , completion: {(completed) in
})
}
}
It's interesting because I'm only changing the constant and not adding another constraint. Nevertheless I receive this warning in console:
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:0x282ae4ff0 UIView:0x109f11110.bottom == UIView:0x109f13240.bottom (active)>",
"<NSLayoutConstraint:0x282ae8050 UIView:0x109f11110.bottom == UIView:0x109f13240.bottom - 291 (active)>"
)
Which basically states that the constraints I have don't work well togheter.
I don't know what I'm doing wrong.
EDIT:
This is the code I use to add the bottom bar programmatically:
let bottomBar:UIView = {
let v = UIView()
v.translatesAutoresizingMaskIntoConstraints = false
return v
}()
In ViewdidLoad()
view.addSubview(bottomBar)
bottomBar.addSubview(fontView)
bottomBar.addSubview(colorPicker)
fontView.pin(to: bottomBar)
colorPicker.pin(to: bottomBar)
func setUpConstraints(){
NSLayoutConstraint.activate([
bottomBar.leadingAnchor.constraint(equalTo: view.leadingAnchor),
bottomBar.trailingAnchor.constraint(equalTo: view.trailingAnchor),
bottomBar.heightAnchor.constraint(equalToConstant: 70),
])
}
Attempting to make it a bit easier for you...
Start simple.
This code creates a text field near the top, and a red "bottomBar" view at the bottom. When the keyboard shows or hides, the bottomBar's bottom constraint constant is updated (tap anywhere on the view to dismiss the keyboard):
class ConstraintTestViewController: UIViewController {
let bottomBar = UIView()
var bottomConstraint: NSLayoutConstraint!
override func viewDidLoad() {
super.viewDidLoad()
// add a text field
let tf = UITextField()
tf.borderStyle = .roundedRect
tf.translatesAutoresizingMaskIntoConstraints = false
bottomBar.translatesAutoresizingMaskIntoConstraints = false
bottomBar.backgroundColor = .red
view.addSubview(tf)
view.addSubview(bottomBar)
let g = view.safeAreaLayoutGuide
NSLayoutConstraint.activate([
// constrain text field 80-pts from Top, Width: 240, centerX
tf.topAnchor.constraint(equalTo: g.topAnchor, constant: 80.0),
tf.widthAnchor.constraint(equalToConstant: 240.0),
tf.centerXAnchor.constraint(equalTo: g.centerXAnchor),
// constrain bottomBar Leading and Trailing, Height: 70-pts
bottomBar.leadingAnchor.constraint(equalTo: g.leadingAnchor),
bottomBar.trailingAnchor.constraint(equalTo: g.trailingAnchor),
bottomBar.heightAnchor.constraint(equalToConstant: 70.0),
])
// create and add the bottom constraint
bottomConstraint = NSLayoutConstraint(item: bottomBar, attribute: .bottom, relatedBy: .equal, toItem: view, attribute: .bottom, multiplier: 1, constant: 0)
view.addConstraint(bottomConstraint)
// or, use more modern syntax...
//bottomConstraint = bottomBar.bottomAnchor.constraint(equalTo: g.bottomAnchor)
//bottomConstraint.isActive = true
// keyboard show/hide notifications
NotificationCenter.default.addObserver(self,
selector: #selector(self.handleKeyboardNotification(notification:)),
name: UIResponder.keyboardWillShowNotification,
object: nil)
NotificationCenter.default.addObserver(self,
selector: #selector(self.handleKeyboardNotification(notification:)),
name: UIResponder.keyboardWillHideNotification,
object: nil)
// add a "tap on the view to dismiss the keyboard" gesture
let t = UITapGestureRecognizer(target: self, action: #selector(self.didTap(_:)))
view.addGestureRecognizer(t)
}
#objc func handleKeyboardNotification(notification: NSNotification){
if let keyboardFrame: NSValue = notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue {
let keyboardRectangle = keyboardFrame.cgRectValue
let keyboardHeight = keyboardRectangle.height
let isKeyboardShowing = notification.name == UIResponder.keyboardWillShowNotification
bottomConstraint.constant = isKeyboardShowing ? -keyboardHeight : 0
UIView.animate(withDuration:0.1, delay: 0 , options: .curveEaseOut , animations: {
self.view.layoutIfNeeded()
} , completion: {(completed) in
})
}
}
#objc func didTap(_ g: UITapGestureRecognizer) -> Void {
view.endEditing(true)
}
}
If this runs without auto-layout errors / warnings (which it will), then start adding in your other UI elements (and supporting code) one at a time. If / when you get the constraint conflict again, you'll know exactly where you went wrong.
You are not providing enough information, so it is necessary to guess. Here is one guess. When you say:
fontView.pin(to: bottomBar)
colorPicker.pin(to: bottomBar)
You create two bottomBar bottom constraints. When you say:
bottomConstraint = NSLayoutConstraint(item: bottomBar, attribute: .bottom, relatedBy: .equal, toItem: view, attribute: .bottom, multiplier: 1, constant: 0)
view.addConstraint(bottomConstraint)
you create a third bottom constraint.
Then when you say
bottomConstraint?.constant = isKeyboardShowing ? -keyboardHeight : 0
you change one bottom constraint but not the others. Perhaps that accounts for the conflict.
Another possibility is that perhaps you call the view.addConstraint twice in some way that you have not shown us. Again, that would mean you are changing one bottom constraint but not the other.
I have a set of AL constraints positioning a child vc that has two positions, expanded and collapsed.
I found that when I add the collapsed constraint, a top anchor to bottom anchor constraint with a constant, when the vc is first created, there seems to be additional spacing when I activate it. Seemingly because the actual height isn't available at the time.
When I add the constraint in viewDidLayoutSubviews there additional spacing is gone and the constraint behaves properly. Except the issue that now when I switch between the constraints in an animation, I cannot deactivate the collapsed constraint as I switch to the expanded constraint and the constraint breaks. Possibly because viewDidLayoutSubviews is called throughout the transition animation.
Here's an abstract of vc setup.
var foregroundExpandedConstraint: NSLayoutConstraint!
var foregroundCollapsedConstraint: NSLayoutConstraint!
var foregroundViewController: UIViewController? {
didSet {
setupforegroundViewController(foregroundViewController: foregroundViewController!)
}
}
func setupforegroundViewController(foregroundViewController: UIViewController) {
addChildViewController(foregroundViewController)
foregroundViewController.didMove(toParentViewController: self)
guard let foregroundView = foregroundViewController.view else { return }
foregroundView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(foregroundView)
foregroundExpandedConstraint = foregroundView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 15)
let height = view.safeAreaLayoutGuide.layoutFrame.height - 50 - 15
let cellHeight = ((height) / 6)
foregroundCollapsedConstraint = NSLayoutConstraint(item: foregroundView, attribute: .top, relatedBy: .equal, toItem: view.safeAreaLayoutGuide, attribute: .bottom, multiplier: 1, constant: (-cellHeight) * 2 - 50)
let foregroundViewControllerViewConstraints = [
foregroundView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
foregroundView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
foregroundView.heightAnchor.constraint(equalTo: view.safeAreaLayoutGuide.heightAnchor, constant: -50 - 15),
foregroundExpandedConstraint!
]
NSLayoutConstraint.activate(foregroundViewControllerViewConstraints)
}
And here the animations are preformed using UIViewPropertyAnimator.
func animateTransitionIfNeeded(state: ForegroundState, duration: TimeInterval) {
let containerFrameAnimator = UIViewPropertyAnimator(duration: duration, dampingRatio: 1) {
[unowned self] in
switch state {
case .expanded:
self.foregroundCollapsedConstraint?.isActive = false
self.foregroundExpandedConstraint?.isActive = true
self.view.layoutIfNeeded()
case .collapsed:
self.foregroundExpandedConstraint?.isActive = false
self.foregroundCollapsedConstraint?.isActive = true
self.view.layoutIfNeeded()
}
}
containerFrameAnimator.addCompletion { [weak self] (position) in
if position == .start {
switch state {
case .collapsed:
self?.foregroundCollapsedConstraint?.isActive = false
self?.foregroundExpandedConstraint?.isActive = true
self?.foregroundIsExpanded = true
self?.view.layoutIfNeeded()
case .expanded:
self?.foregroundExpandedConstraint?.isActive = false
self?.foregroundCollapsedConstraint?.isActive = true
self?.foregroundIsExpanded = false
self?.view.layoutIfNeeded()
}
} else if position == .end {
switch state {
case .collapsed:
self?.foregroundExpandedConstraint?.isActive = false
self?.foregroundCollapsedConstraint?.isActive = true
self?.foregroundIsExpanded = false
case .expanded:
self?.foregroundExpandedConstraint?.isActive = false
self?.foregroundCollapsedConstraint?.isActive = true
self?.foregroundIsExpanded = true
}
}
self?.runningAnimations.removeAll()
}
Again to reiterate, when I use the following code, setting the constraint as the vc is added to the view hierarchy, it doesn't layout properly. Checking the constraints I see they change after view did layout subviews is called. Each constraint changes appropriately except for the collapsed constraint.
When I add the collapsed constraint in view did layout subviews it behaves properly however I am unable to deactivate it going forwards and the constraint breaks.
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
let height = view.safeAreaLayoutGuide.layoutFrame.height - 50 - 15
let cellHeight = ((height) / 6)
if let v = foregroundViewController?.view {
foregroundCollapsedConstraint = NSLayoutConstraint(item: v, attribute: .top, relatedBy: .equal, toItem: view.safeAreaLayoutGuide, attribute: .bottom, multiplier: 1, constant: (-cellHeight) * 2 - 50)
}
}
Edit: I've created a repo demonstrating the issue: https://github.com/louiss98/UIViewPropertyAnimator-Layout-Test
Any suggestions?
You can eliminate the "broken" constraint by changing the constant instead of creating a new constraint.
In your viewDidLayoutSubviews() func,
change:
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
let height = view.safeAreaLayoutGuide.layoutFrame.height - 50 - 15
let cellHeight = ((height) / 6)
foregroundCollapsedConstraint = NSLayoutConstraint(item: testViewController.view, attribute: .top, relatedBy: .equal, toItem: view.safeAreaLayoutGuide, attribute: .bottom, multiplier: 1, constant: (-cellHeight) * 2 - 50)
}
to:
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
let height = view.safeAreaLayoutGuide.layoutFrame.height - 50 - 15
let cellHeight = ((height) / 6)
foregroundCollapsedConstraint.constant = (-cellHeight) * 2 - 50
}
in my game i have a ViewController that displays various items in a collectionView i need to make it so that when an item is pressed at an index...
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
// handle tap events
if indexPath.item == 0 {
//add a viewcontroller for viewing content
//other things here for customizing that view data
}
}
...a viewcontroller pops onscreen not taking up the whole screen but a small portion in the middle of the main ViewController
(i need the view to be reusable and adaptable) i have tried making a viewController and adding it to the main ViewController as a subview of sorts but no luck
i want to display different info depending what cell is selected if you could help me i would appreciate it
As far as I understand , You want to open custom view which is loaded from xib on your ViewControllers view or a different ViewControllers view on your ViewController(that display various item in collectionView).
If yes then use below code
//Create optional property
var myViewController : YourCustomViewController?
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
// handle tap events
if indexPath.item == 0 {
//add below line to load custom View Xib
loadCustomView(onView:self.view)
//OR To Add ViewController View use below code
loadViewController(onView:self.view)
}
}
fun loadCustomView(onView:UIView) {
let allViewsInXibArray = Bundle.init(for: type(of: self)).loadNibNamed("CustomView", owner: self, options: nil)
//If you only have one view in the xib and you set it's class to MyView class
let myCustomView = allViewsInXibArray?.first as! CustomView
onView addSubview(myCustomView)
addConstraints(onView: myCustomView)
}
func loadViewController(onView:UIView) {
myViewController = YourCustomViewController(nibName: "TestViewController", bundle: nil);
onView addSubview((myViewController?.view)!)
addConstraints(onView: (myViewController?.view)!)
}
func addConstraints(onView : UIView) {
onView.translatesAutoresizingMaskIntoConstraints = false;
let widthConstraint = NSLayoutConstraint(item: onView, attribute: NSLayoutAttribute.width, relatedBy: NSLayoutRelation.equal,
toItem: onView.superview, attribute: .width, multiplier: 0.8, constant: 0)
let heightConstraint = NSLayoutConstraint(item: onView, attribute: .height, relatedBy: .equal,
toItem: onView.superview, attribute: .height, multiplier: 0.6, constant: 0)
let xConstraint = NSLayoutConstraint(item: onView, attribute: .centerX, relatedBy: .equal, toItem:onView.superview , attribute: .centerX, multiplier: 1, constant: 0)
let yConstraint = NSLayoutConstraint(item: onView, attribute: .centerY, relatedBy: .equal, toItem: onView.superview, attribute: .centerY, multiplier: 1, constant: 0)
NSLayoutConstraint.activate([widthConstraint, heightConstraint, xConstraint, yConstraint])
}
Further you can add popup and popdown Animation to your custom view.
Comment me if you need anything else.
Using Swift and autolayout, what's the best way to put a label and custom view in a cell? I want the label on the left and the view on the right. The view could be any subclass of UIView. One problem I'm having is sizing the label's width to fit the text. I want the custom view to take up the remainder of the space. I prefer to not use the visual format but instead NSLayoutConstraint():
func tableView(aTableView: UITableView, cellForRowAtIndexPath anIndexPath: NSIndexPath) -> UITableViewCell {
let cell = UITableViewCell(frame: CGRect(x: 0.0, y: 0.0, width: aTableView.bounds.size.width, height: aTableView.estimatedRowHeight))
let titleLabel = UILabel()
titleLabel.adjustsFontSizeToFitWidth = true
titleLabel.translatesAutoresizingMaskIntoConstraints = false
titleLabel.numberOfLines = 0
titleLabel.text = "some text"
titleLabel.sizeToFit()
cell.contentView.addSubview(titleLabel)
titleLabel.layer.borderColor = UIColor.redColor().CGColor
titleLabel.layer.borderWidth = 1.0
cell.contentView.addConstraint(NSLayoutConstraint(item: titleLabel, attribute: .Left, relatedBy: .Equal, toItem: cell.contentView, attribute: .Left, multiplier: 1.0, constant: 0.0))
let customView = UIView()
cell.contentView.addSubview(customView)
cell.contentView.addConstraint(NSLayoutConstraint(item: customView, attribute: .Left, relatedBy: .Equal, toItem: titleLabel, attribute: .Right, multiplier: 1.0, constant: 0.0))
cell.contentView.addConstraint(NSLayoutConstraint(item: customView, attribute: .Right, relatedBy: .Equal, toItem: cell.contentView, attribute: .Right, multiplier: 1.0, constant: 0.0))
return cell
}
Try adding the following line:
customView.translatesAutoresizingMaskIntoContraints = false
you might also need an additional constraint to define maybe the max size of the UILabel or the min size of the UIView.
Since you have numberOfLines = 0, the UILabel doesn't know exactly when to break into a new line, since there are no constraints specifying that.
Hope it helps
Create the label and give these constraints
horizontal spacing constraint between custom view and the label
trailing space constraint to content view
Top space and bottom space for the label and custom view
Leading space for the custom view
Here the label automatically grows with the content and the custom view will occupy the remaining space.