Is there any way to share constants between constraints in a storyboard (or better yet define them globally and use when needed)?
Say, I want a distance from top to be equal to the distance from the left - those are not achievable by setting any sort of symmetry, and I don't want to change both of them every single time, just change one and see the result.
I want a storyboard (clickable) solution, no coding required.
Well, you can't use defined constants in storyboard, but it is possible to share properties. Depending on how complex your design is it might not be worth the trouble. If you just want the same distance shared between two items you are probably better off editing their value. If you have several dependencies you can create "spacers" that share the same sizes.
It is quite simple, just add two or more (hidden) UIView objects to your storyboard. Choose one to be the master item, then set the others to have the size properties of that. The master can be set to have ratio 1:1, so that you only need to set and change the height of it to resize all of them in both X and Y. You then align your other items to these objects.
Also note that you can have other values than 1:1 for the multiplier.
Another note: if you want just one view to be positioned, one such hidden view will suffice.
It's hard to say without more info, but I think the best solution would be to subclass UIView, something like this:
SWIFT 4.0
class MySymmetricView {
#IBInspectable var inset: Float = 10.0 // Put your constraints here, #IBInspectable will expose them to IB, you can also set your constants here
required init?(coder: NSCoder) {
super.init(coder: coder)
setupConstraints()
}
override init(frame: CGRect) {
super.init(frame: frame)
setupConstraints()
}
func setupConstraints() {
NSLayoutConstraint(item: self, attribute: .leading, relatedBy: .equal, toItem: superview, attribute: .leadingMargin, multiplier: 1.0, constant: inset).isActive = true
NSLayoutConstraint(item: self, attribute: .top, relatedBy: .equal, toItem: superview, attribute: .leadingMargin, multiplier: 1.0, constant: inset).isActive = true
}
}
Or subclass the constraints themselves in a similar way.
Related
So I have a UIView near the bottom of the superview, with a textfield inside of it. When the user taps inside the textfield to begin editing, I am bringing up the entire UIView with the keyboard. One problem with this, is that if you have constraints on said UIView, when you start typing in the textfield, the UIView conforms to its constraints and goes back down to its original spot, hidden by the keyboard. I created a work around by overriding updateViewContraints(), removing the default constraint (the one I set in storyboard), when the textfield is being edited, and adding a new constraint to keep it where I want it. Then, when the editing ends, the code is supposed to bring the UIView back down, and remove the new constraint, and replace it with the original. Here's that code:
override func updateViewConstraints() {
// The new constraint, active when the keyboard is shown
let constraintWhenKeyboardShown = NSLayoutConstraint(item: searchRadiusView!, attribute: .bottom, relatedBy: .equal, toItem: super.view, attribute: .bottom, multiplier: 1.0, constant: -310.0)
// The default constraint, active at all other times
let defaultConstraint = NSLayoutConstraint(item: searchRadiusView!, attribute: .bottom, relatedBy: .equal, toItem: super.view, attribute: .bottom, multiplier: 1.0, constant: -30.0)
if radiusTextfield.isEditing {
view.removeConstraint(defaultConstraint)
view.addConstraint(constraintWhenKeyboardShown)
print("new")
} else {
view.removeConstraint(constraintWhenKeyboardShown)
view.addConstraint(defaultConstraint)
print("default")
}
self.view.layoutIfNeeded()
super.view.updateConstraints()
}
I call the updateViewConstraints in each of my textfield methods:
// Raises the searchRadiusView upon editing
func textFieldDidBeginEditing(_ textField: UITextField) {
let newSearchRadiusViewYValue = self.searchRadiusView.center.y - 280
searchRadiusView.center.y = newSearchRadiusViewYValue
print(searchRadiusView.constraints)
self.updateViewConstraints()
}
// Lowers the searchRadiusView upon dismissal
func textFieldDidEndEditing(_ textField: UITextField) {
let newSearchRadiusViewYValue = self.searchRadiusView.center.y + 280
searchRadiusView.center.y = newSearchRadiusViewYValue
print("ended")
print(searchRadiusView.constraints)
self.updateViewConstraints()
}
However, upon ending editing, my UIView is not moving back down to its original position, and the debugger says
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:0x607000071590 UIView:0x61300002f280.bottom == UIView:0x613000071380.bottom - 310 (active)>",
"<NSLayoutConstraint:0x607000297f80 UIView:0x61300002f280.bottom == UIView:0x613000071380.bottom - 30 (active)>"
)
It seems that the 'constraintWhenKeyboardShown' constraint is still there (the one ending in -310), even though I have 'view.removeConstraint(constraintWhenKeyboardShown)
' in the updateViewConstraints() function. Is that what's causing my problem? Does anyone know how to fix this? Thanks
I have a collection view cell and I want to add a view to it. Apple states that views should be added to the contentView. This is there documentation:
When configuring a cell, you add any custom views representing your cell’s content to this view. The cell object places the content in this view in front of any background views.
https://developer.apple.com/documentation/uikit/uicollectionviewcell/1620133-contentview
However, it seems I can also add view not inside the contentView and there is no warnings or crashes. Here is my code. Notice the comment QUESTION -- I want to know if this is ok to do:
class MyCollectionViewCell: UICollectionViewCell {
#IBOutlet private var imageView: UIImageView!
private let titleLabel = UILabel(frame: .zero)
override func awakeFromNib() {
super.awakeFromNib()
// QUESTION: Is this ok? Notice I am not adding the `titleLabel` to `self.contentView`
self.addSubview(titleLabel)
let horizontalCenterConstraint = NSLayoutConstraint(item: titleLabel, attribute: .centerX, relatedBy: .equal, toItem: self, attribute: .centerX, multiplier: 1.0, constant: 0)
let verticalCenterConstraint = NSLayoutConstraint(item: titleLabel, attribute: .centerY, relatedBy: .equal, toItem: self, attribute: .centerY, multiplier: 1.0, constant: 0)
self.addConstraint(horizontalCenterConstraint)
self.addConstraint(verticalCenterConstraint)
}
}
In the code above, instead of adding the view with self.contentView.addSubview... instead I simply do self.addSubview. Is this ok?
Most things are possible, The question is why do you want to do this?
By directly going against an API author's recommendation, you're risking a whole host of potential issues. e.g.
You may be breaking something you're unaware of
You may be adversely affecting performance
Even if it works currently, it may break in the future and the author has no reason to prevent this from happening.
If what you're trying to do is impossible otherwise and you're ok with the risks, go for it. Otherwise, stick to the author's explicit instructions.
Yes, I tried to add cell directly instead of contentView. Now, weird thing happens, the collection view could not scroll. It took me 1 hour to figure out that I should add itself to contentView
I have two subviews that I created, one in Storyboard one with code, I want to anchor the second view (created with code) to the first view (in storyboard) with some constraints so that the second view sits below the first view:
class ViewController: UIViewController{
#IBOutlet weak var view1: UIView!
override func viewDidLoad(){
super.viewDidLoad()
setUp()
}
func setUp(){
var view2 = UIView()
view2.setTranslatesAutoresizingMaskIntoConstraints(false)
view2.frame = CGRectMake(10,10,10,10)
self.view.addSubview(view2)
self.view.addConstraint(NSLayoutConstraint(item: view1, attribute: NSLayoutAttribute.TopMargin, relatedBy: NSLayoutRelation.Equal, toItem: view2, attribute: NSLayoutAttribute.Bottom, multiplier: 1, constant: 10))
}
}
The problem is that I got an error saying When added to a view, the constraint's items must be descendants of that view. Is it bad practice to have some views in storyboard and others in code?
You have a few problems.
First, the error means that view1 and view2 are not both part of self.view's view hierarchy. This is probably because view1 hasn't been decoded from the storyboard yet. Try moving this code from viewDidLoad() to awakeFromNib(), which is when they're guaranteed to have been loaded.
Second, you're trying to set the view's frame:
view2.frame = CGRectMake(10,10,10,10)
This frame will get overwritten by the layout engine making it pointless. Delete this line.
If you're going to use auto-layout, you need to unambiguously specify both the size (height & width) and position (x & y) of the view. The constraint you added only specifies the y-origin, so you also need to add more constraints (probably 3 more) to specify the x-origin, the width, and the height, which are currently undefined.
Is it bad practice to have some views in storyboard and others in code?
No, it's common.
I have a project I'm working on that needs two buttons with some data that will take up exactly half the width of the UITableViewCell that they are in.
In the past when I have wanted to do this I usually set a constraint that the button will be equal widths to its superview and give it a multiplier of 0.5.
For some reason however inside the UITableViewCell I can't get the Storyboards to give me this option. The "Equal Widths" constraint in the GUI is grayed out.
I resolved to just do it programmatically so in the custom cell I tied the following code. I've tried putting the cellInit() method below being called in the awakeFromNib and that gave an error. I've tried also just calling it on cellForRowAtIndexPath when the cell is loaded, and got the same error.
import UIKit
class PollCell: UITableViewCell {
#IBOutlet weak var option1: UIButton!
#IBOutlet weak var option2: UIButton!
override func awakeFromNib() {
super.awakeFromNib()
//cellInit() //Commented out because causes error
}
func cellInit(){
option1.addConstraint(NSLayoutConstraint(item: option1, attribute: .Width, relatedBy: .Equal, toItem: self, attribute: .Width, multiplier: 0.5, constant: 0))
}
}
This is the error that I am getting:
'NSInternalInconsistencyException', reason: 'Impossible to set up layout with view hierarchy unprepared for constraint.
What I'm trying to achieve is pretty standard, so I assume this isn't anything to crazy and I'm probably doing something the wrong way. Either way I assume plenty of newcomers like myself will run into this. Thanks in advance!
In the comments, we discussed that you leverage contentView.frame.maxX
Alternatively, you can use AutoLayout: Make sure you setTranslatesAutoresizingMaskIntoConstraints(false)
Assign tags (optional). You only havetwo buttons but for more than two, I would use tags so you don't need to manually type UIButton for every button.
addConstraint(NSLayoutConstraint(item: self.viewWithTag(1) as UIButton, attribute: .Right, relatedBy: .Equal, toItem: self, attribute: .CenterX, multiplier: 0.33, constant: 0))
addConstraint(NSLayoutConstraint(item: self.viewWithTag(2) as UIButton, attribute: .Right, relatedBy: .Equal, toItem: self, attribute: .CenterX, multiplier: 0.66, constant: 0))
OR VFL using a Dictionary:
for button in buttonsDictionary.keys {
self.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("H:|-[\(button1)]-[\(button2)]|", options: .allZeros, metrics: nil, views: buttonsDictionary))
}
call cell.updateConstraints() in your cellForRowAtIndexPath in TableView.
You can learn more in the link below: They have an example of two side by side buttons:
https://developer.apple.com/library/ios/documentation/UserExperience/Conceptual/AutolayoutPG/AutoLayoutinCode/AutoLayoutinCode.html#//apple_ref/doc/uid/TP40010853-CH11-SW1
I've encountered a similar problem like yours before. What I did was put a UIView in the cell first with its top, left, right, bottom constraints set to all 0, then place the button on top of the view. This way I get the 'equal width' option.
I've been going through the documentation and still seem to be on a sticking point.
I have a view controller object C_SelectPhoto. This has a container view. Inside the container view I want the childed view controller, C_SelectPhotoControllerView, to fit inside it. It will just be an array of photos. However, setting the frame and adding the child view controller is not working. If I move the x value of the desired child view controller, no effect happens.
To figure out what is going on I color coded everything. The container, below, is orange. The view the container expects, according to the storyboard is yellow. The view I actually want to fit in there is red.
Here is the storyboard:
Here is my controller code for C_SelectPhoto
class C_SelectPhoto:Controller
{
#IBOutlet weak var selectPhotoControllerView: UIView!
var _collectionViewController:C_SelectPhotoControllerView!
//TODO PERMISSION IS NEEDED BEFORE FETCHING
func initController()
{
_collectionViewController = Controller.STORYBOARD.instantiateViewControllerWithIdentifier("selectPhotoControllerView") as C_SelectPhotoControllerView
displayControllerViewController()
}
//show the photo selection
private func displayControllerViewController()
{
addChildViewController(_collectionViewController)
_collectionViewController.view.frame = CGRectMake(100, 0, 500, 500)
self.view.addSubview(_collectionViewController.view)
_collectionViewController.didMoveToParentViewController(self)
}
}
However the result is produces is below:
First, the yellow class shouldn't be added at all, I wanted only the red (the UICollectionViewController class). Second, I can tell the red class is being added to the wrong spot because its x value hasn't moved it over at all.
So my question is:
How can I add a UIContainerViewController, as a child to the main view controller, C_SelectPhoto, but have the UIContainerViewController frame FIT the container I have in the main view controller?
Thank you!!!
NOTE: The views I am trying to add are UICollectionViewControllers. When I add a UIViewController, the framing works just fine, but as you can see when adding the UICollectionViewControllers, the framing does NOT work, and they are getting added to random offsets and are not respecting my attempts to size them with frame assignments.
use following Extension for adding childViewController On View
extension UIViewController {
func configureChildViewController(childController: UIViewController, onView: UIView?) {
var holderView = self.view
if let onView = onView {
holderView = onView
}
addChildViewController(childController)
holderView.addSubview(childController.view)
constrainViewEqual(holderView, view: childController.view)
childController.didMoveToParentViewController(self)
childController.willMoveToParentViewController(self)
}
func constrainViewEqual(holderView: UIView, view: UIView) {
view.translatesAutoresizingMaskIntoConstraints = false
//pin 100 points from the top of the super
let pinTop = NSLayoutConstraint(item: view, attribute: .Top, relatedBy: .Equal,
toItem: holderView, attribute: .Top, multiplier: 1.0, constant: 0)
let pinBottom = NSLayoutConstraint(item: view, attribute: .Bottom, relatedBy: .Equal,
toItem: holderView, attribute: .Bottom, multiplier: 1.0, constant: 0)
let pinLeft = NSLayoutConstraint(item: view, attribute: .Left, relatedBy: .Equal,
toItem: holderView, attribute: .Left, multiplier: 1.0, constant: 0)
let pinRight = NSLayoutConstraint(item: view, attribute: .Right, relatedBy: .Equal,
toItem: holderView, attribute: .Right, multiplier: 1.0, constant: 0)
holderView.addConstraints([pinTop, pinBottom, pinLeft, pinRight])
}}
Updated for Swift 5+
Just one line in your view controller to add child view controller.
Super scalable methods in the extension if you want to add it on any custom view.
public extension UIViewController {
/// Adds child view controller to the parent.
///
/// - Parameter child: Child view controller.
func add(_ child: UIViewController) {
addChild(child)
view.addSubview(child.view)
child.didMove(toParent: self)
}
/// It removes the child view controller from the parent.
func remove() {
guard parent != nil else {
return
}
willMove(toParent: nil)
removeFromParent()
view.removeFromSuperview()
}
}
How to use:
Adding: In the view controller where you want to add the child view controller.
// let yourChildViewController = Load fro the storyboard or XIB
add(yourChildViewController)
Removing:
yourChildViewController.remove()
If you want the red controller to be the child controller, delete the yellow one, and control-drag from the container to the red controller. There's no need to add it in code, or do any resizing. The red controller will be set to the same size as the container in the storyboard.