how to set height constraint in IBDesignable file? - ios

I need to make an IBDesignable to make a custom navigation bar file that will adjust the height of the view based on the iPhone type. if the iPhone has top notch like iPhone X,XR, then the height contraint will be 88, otherwise for iPhone 8 that does not has top notch, the height will be 64.
I need to set the height contraint, not the layer height. here is the code I use but it fails to update the height contraint
import UIKit
#IBDesignable
class CustomParentNavigationBarView: UIView {
override func awakeFromNib() {
super.awakeFromNib()
self.setHeight()
}
func setHeight() {
let deviceHasTopNotch = checkHasTopNotchOrNot()
var heightConstraint = NSLayoutConstraint()
if deviceHasTopNotch {
heightConstraint = NSLayoutConstraint(item: self, attribute: NSLayoutConstraint.Attribute.height, relatedBy: NSLayoutConstraint.Relation.equal, toItem: nil, attribute: NSLayoutConstraint.Attribute.notAnAttribute, multiplier: 1, constant: 88)
} else {
heightConstraint = NSLayoutConstraint(item: self, attribute: NSLayoutConstraint.Attribute.height, relatedBy: NSLayoutConstraint.Relation.equal, toItem: nil, attribute: NSLayoutConstraint.Attribute.notAnAttribute, multiplier: 1, constant: 64)
}
heightConstraint.isActive = true
self.addConstraint(heightConstraint)
}
func checkHasTopNotchOrNot() -> Bool {
if #available(iOS 11.0, tvOS 11.0, *) {
// with notch: 44.0 on iPhone X, XS, XS Max, XR.
// without notch: 20.0 on iPhone 8 on iOS 12+.
return UIApplication.shared.delegate?.window??.safeAreaInsets.top ?? 0 > 20
}
return false
}
}
the result should be something like this (red view), the height of the red view should change according to iPhone type, either 88 or 64
for the initial value, I set the autolayout of the view in storyboard like this

There are two problems that I can see.
You are not activating the constraints. Add this line
heightConstraint.isActive = true
You are calling SetHeight multiple times. Each time, a constraint is added. This will lead to constraint conflicts and poor behavior. Instead, just create the constraint once and store it as a member.

Related

Make UITextView height dynamic, issue with constraint multiplier

I have a UICollectionView which contains five cells. In there, I have a UIImageView, two UILabels and a UITextView. I want to change the height of the textview based on the text it contains, so afterwards I can set the height of the entire cell based on the height of the UITextView and the labels above them. Let me demonstrate with a screenshot.
So, as you can tell, the red background shows the height of the UITextView is not right. I set up the UITextView like this:
let commentTextView: UITextView = {
let textView = UITextView()
textView.translatesAutoresizingMaskIntoConstraints = false
textView.text = "This is just some text to act as a description"
textView.textContainerInset = UIEdgeInsets(top: 0, left: -4, bottom: 0, right: 0)
textView.font = UIFont.systemFont(ofSize: 16.0)
textView.textColor = UIColor.darkGray
textView.isEditable = false
textView.isSelectable = false
textView.backgroundColor = UIColor.red
textView.frame.size.width = (UIScreen.main.bounds.width - 16 - 16 - 60 - 8)
let numberOfLines = (textView.contentSize.height / textView.font!.lineHeight)
var textViewHeight = (textView.font?.lineHeight)! * floor(numberOfLines)
textView.frame.size.height = textViewHeight
return textView
}()
This does not create the wrong height, I think. I think the problem can be found in my constraints, which has a height constraint (if I delete it, the UITextView disappears). If I change the multiplier (currently set at 0.3), I have different heights, but I want this to be dynamically. So in my opinion, I would need to set a dynamic variable inside the multiplier, but I have no idea how to compose it. Could anyone help? Here are my constraints:
// top constraints
addConstraints([NSLayoutConstraint(item: commentTextView, attribute: .top, relatedBy: .equal, toItem: workoutLabel, attribute: .bottom, multiplier: 1, constant: 2)])
// left constraint
addConstraints([NSLayoutConstraint(item: commentTextView, attribute: .left, relatedBy: .equal, toItem: profilePictureImageView, attribute: .right, multiplier: 1, constant: 16)])
// right constraint
addConstraints([NSLayoutConstraint(item: commentTextView, attribute: .right, relatedBy: .equal, toItem: self.commentTextView.superview, attribute: .right, multiplier: 1, constant: -16)])
// height constraint
addConstraints([NSLayoutConstraint(item: commentTextView, attribute: .height, relatedBy: .equal, toItem: self, attribute: .height, multiplier: 0.3, constant: 1)])
Cheers guys!
You could consider another way about dynamic height of UITextView. It's known about intrinsic content size. You could implement dynamic height with that tech.
Originally answered here.
Don't change or give any frame to your UITextView. Just give it leading, trailing, top & bottom constraints. Then if your cell is capable of having automatic size then you don't need to calculate anything for your text view.
When using Auto-Layout for dynamically sizing cells, you don't really need to implement sizeThatFits(...). If the constraints are setup correctly, then you only need to disable the scrolling of the UITextView.
From code:
yourTextView.scrollEnabled = false
From IB:
Select your Text View and open Attributes inspector, then
Now if you are facing problems with making your cell to have dynamic size then please look at this answer.
As mentioned yesterday, I had the textview adapt its size based on the answer of #nayem - thank you!
However, I faced another problem with making the cell height dynamic. I have looked into different solutions, got nothing to work, except when I calculate the height myself and set this as the height to be used. This works on all devices in Simulator, just wondering if this is the right approach. Code below, constraints are set on top, left and right.
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
let dummyText = "Just a long text to see what will happen to the cell. Will it adapt or not? Let's find out!"
let rectWidth = view.frame.width - 32 - 60 - 16
let rect = NSString(string: dummyText).boundingRect(with:CGSize(width: rectWidth, height: 1000), options: NSStringDrawingOptions.usesFontLeading.union(NSStringDrawingOptions.usesLineFragmentOrigin), attributes: [NSAttributedStringKey.font: UIFont.systemFont(ofSize: 16)], context: nil)
let variableHeight = 16 + 20 + 2 + 20 + 2 + 16 + rect.height
return CGSize(width: view.frame.width, height: variableHeight)
}
Is this the best way to do this? Will this cause problems later on?

Setting Constraint programmatically like in storyboard

I need to set constraint to have a view equal height with multiplier 0.8
I use this code
override func willTransition(to presentationStyle: MSMessagesAppPresentationStyle) {
if(presentationStyle == .compact){
let videoController = recordingController.videoController
let containerView = videoController?.containerView
self.addConstraint(value: 0.1, name: "ViewAltezza",videoView: (videoController?.mainView)!, containerView: containerView!)
print("Compact")
}else if(presentationStyle == .expanded){
let videoController = recordingController.videoController
let containerView = videoController?.containerView
self.addConstraint(value: 0.1, name: "ViewAltezza",videoView: (videoController?.mainView)!, containerView: containerView!)
}
print("Expand")
}
}
func addConstraint(value : CGFloat, name: String, videoView: UIView, containerView: UIView){
videoView.translatesAutoresizingMaskIntoConstraints = false
for constraint in videoView.constraints{
if(constraint.identifier == name){
print("Ecco la constraint \(constraint)")
videoView.removeConstraint(constraint)
let heightConstraint = NSLayoutConstraint(item: containerView , attribute: .height, relatedBy: .equal, toItem: videoView, attribute: .height, multiplier: value, constant: 0.0)
heightConstraint.identifier = name
heightConstraint.priority = UILayoutPriority(rawValue: 1000)
print("Aggiungo questa constraint \(heightConstraint)")
videoView.addConstraint(heightConstraint)
}
}
}
The code seems correct, and the debug shows that the initial constraint and the created constraint are the same... is there any errors ? or just I am forgetting something
I need to do this because in iMessage extension when I create a viewController using present(_viewControllerFromStoryboard . . . ), it seems like the view can't auto resize like in MSMessagesAppViewController when the user expand or compact the iMessage app; so if there is any way to avoiding this and let the viewController auto fix the presentationStyle, it will be easier for me to fix the issue I am experimenting.

How do I add constraints to a subview loaded from a nib file?

I'm trying to load a sub view on to every single page of my app from a nib file. Right now I'm using a somewhat unusual approach to loading this sub view in that I am doing it through an extension of UIStoryboard (probably not relevant to my problem, but I'm not sure). So this is how the code looks when I load the nib file:
extension UIStoryboard {
public func appendCustomView(to viewController: UIViewController) {
if let myCustomSubview = Bundle.main.loadNibNamed("MyCustomSubview", owner: nil, options: nil)![0] as? MyCustomSubview {
viewController.view.addSubview(myCustomSubview)
}
}
}
This code does what it's supposed to do and adds "MyCustomSubview" to the view controller (I won't go in to detail on exactly how this method gets called because it works so it doesn't seem important). The problem is I can't for the life of me figure out how to add constraints that effect the size of myCustomSubview. I have tried putting code in the function I showed above as well as in the MyCustomSubview swift file to add constraints but no matter what I do the subview never changes.
Ideally the constraints would pin "MyCustomSubview" to the bottom of the ViewController, with width set to the size of the screen and a hard coded height constraint.
Here are the two main methods I tried (with about 100 minor variations for each) that did NOT work:
Method 1 - Add constraint directly from "appendCustomView"
public func appendCustomView(to viewController: UIViewController) {
if let myCustomSubview = Bundle.main.loadNibNamed("MyCustomSubview", owner: nil, options: nil)![0] as? MyCustomSubview {
let top = NSLayoutConstraint(item: myCustomSubview, attribute: .top, relatedBy: .equal
, toItem: viewController.view, attribute: .top, multiplier: 1, constant: 50.0)
viewController.view.addSubview(myCustomSubview)
viewController.view.addConstraint(top)
}
}
Method 2 - Add constraint outlets and setter method in MyCustomSubview
class MyCustomSubview: UIView {
#IBOutlet weak var widthConstraint: NSLayoutConstraint!
#IBOutlet weak var heightConstraint: NSLayoutConstraint!
func setConstraints(){
self.widthConstraint.constant = UIScreen.main.bounds.size.width
self.heightConstraint.constant = 20
}
}
And call setter method in "appendCustomView"
public func appendCustomView(to viewController: UIViewController) {
if let myCustomSubview = Bundle.main.loadNibNamed("MyCustomSubview", owner: nil, options: nil)![0] as? MyCustomSubview {
myCustomSubview.setConstraints()
viewController.view.addSubview(myCustomSubview)
}
}
(*note: the actual constraints of these examples are irrelevant and I wasn't trying to meet the specs I mentioned above, I was just trying to make any sort of change to the view to know that the constraints were updating. They weren't.)
Edit : Changed "MyCustomNib" to "MyCustomSubview" for clarity.
When you add constraints onto a view from a Nib, you have to call yourView.translatesAutoresizingMaskIntoConstraints = false, and you also need to make sure that you have all 4 (unless it's a label or a few other view types which only need 2) constraints in place:
Here's some sample code that makes a view fill it's parent view:
parentView.addSubview(yourView)
yourView.translatesAutoresizingMaskIntoConstraints = false
yourView.topAnchor.constraint(equalTo: parentView.topAnchor).isActive = true
yourView.leadingAnchor.constraint(equalTo: parentView.leadingAnchor).isActive = true
yourView.bottomAnchor.constraint(equalTo: parentView.bottomAnchor).isActive = true
yourView.trailingAnchor.constraint(equalTo: parentView.trailingAnchor).isActive = true
Edit: I've actually come around to perferring this method of adding NSLayoutConstraints, even though the results are the same
NSLayoutConstraint.activate([
yourView.topAnchor.constraint(equalTo: parentView.topAnchor),
yourView.leadingAnchor.constraint(equalTo: parentView.leadingAnchor),
yourView.bottomAnchor.constraint(equalTo: parentView.bottomAnchor),
yourView.trailingAnchor.constraint(equalTo: parentView.trailingAnchor),
])
For anyone who comes across this in the future, this is the solution I came up with by tweaking this answer a little bit
Add a setConstraints(withRelationshipTo) method in the swift class that corresponds to the nib file:
class MyCustomSubview: UIView {
func setConstraints(withRelationshipTo view: UIView){
self.translatesAutoresizingMaskIntoConstraints = false
// Replace with your own custom constraints
self.heightAnchor.constraint(equalToConstant: 40.0).isActive = true
self.leadingAnchor.constraint(equalTo: view.leadingAnchor).isActive = true
self.bottomAnchor.constraint(equalTo: view.bottomAnchor).isActive = true
self.trailingAnchor.constraint(equalTo: view.trailingAnchor).isActive = true
}
}
Then call the setConstraints method after you add the nib file to the view (probably in viewWillAppear or viewDidLoad of a view controller )
class MyViewController: UIViewController {
override func viewWillAppear(_ animated: Bool){
super.viewWillAppear(animated)
if let myCustomSubview = Bundle.main.loadNibNamed("MyCustomSubview", owner: nil, options: nil)![0] as? MyCustomSubview {
let view = self.view // Added for clarity
view.addSubview(myCustomSubview)
myCustomSubview.setConstraints(withRelationshipTo: view)
}
}
}
You can use this extension for anywhere you're going to add a subview to a existing UIView.
extension UIView {
func setConstraintsFor(contentView: UIView, left: Bool = true, top: Bool = true, right: Bool = true, bottom: Bool = true) {
contentView.translatesAutoresizingMaskIntoConstraints = false
self.addSubview(contentView)
var constraints : [NSLayoutConstraint] = []
if left {
let constraintLeft = NSLayoutConstraint(item: contentView, attribute: .left, relatedBy: .equal, toItem: self, attribute: .left, multiplier: 1, constant: 0)
constraints.append(constraintLeft)
}
if top {
let constraintTop = NSLayoutConstraint(item: contentView, attribute: .top, relatedBy: .equal, toItem: self, attribute: .top, multiplier: 1, constant: 0)
constraints.append(constraintTop)
}
if right {
let constraintRight = NSLayoutConstraint(item: contentView, attribute: .right, relatedBy: .equal, toItem: self, attribute: .right, multiplier: 1, constant: 0)
constraints.append(constraintRight)
}
if bottom {
let constraintBottom = NSLayoutConstraint(item: contentView, attribute: .bottom, relatedBy: .equal, toItem: self, attribute: .bottom, multiplier: 1, constant: 0)
constraints.append(constraintBottom)
}
self.addConstraints(constraints)
}
}
You can call this method like this:
containerView.setConstraintsFor(contentView: subView!, top: false)
This will add subView to the containerView and constraint to all sides except top side.
You can modify this method to pass left, top, right, bottom Constant value if you want.

UIViews overlapping when adding multiple views dynamically to a tableViewCell in swift

I want to add some n views inside a table cell (lets say each view is a row). Based on the designerExpertiseList count, i have created a view for each row and added 1 image view and 1 label.
But when i scroll, the data for cells is not correct. If i long press on a cell, i can see a different view overlapped with the one visible now. Please check the attached screenshots
1st time when the view is loaded : http://i.stack.imgur.com/M8itL.png
After i scroll down, scroll up again and long press: http://i.stack.imgur.com/AuTG0.png
And when i scroll up, the data which was correct the first time for few cell, even that is getting messed up. I even tried to add these dynamic views only once, on First Render.
There are the global declarations:
let rowHeight:CGFloat = 20.0
let imageWidth:CGFloat = 15.0
let imageHeight:CGFloat = 15.0
let labelX:CGFloat = 30.0
let labelHeight:CGFloat = 20.0
var firstRender:[Bool] = [Bool]()
code inside tableView cellForRowAtIndexPath method:
self.designer = AppController.getElementFromDesignersList(indexPath.section)
cell.designerName.text = designer.getFirstName() + " " + designer.getLastName()
cell.location.text = designer.getAddress()
// Add more ROWS of expertise if exist! Getting only 1st expertise now, if it exists
let expertiseList = self.designer.getExpertiseList()
if self.firstRender[indexPath.section] {
var i:Int = 0
for e in expertiseList {
let v = UIView(frame: CGRectMake(0, CGFloat(i)*rowHeight, cell.frame.width, rowHeight))
v.backgroundColor = UIColor.yellowColor()
let im = UIImageView(frame: CGRectMake(0, CGFloat(i)*imageHeight, imageWidth, imageHeight ))
//print("expertise image path: ", e.getImagePath())
im.af_setImageWithURL(
NSURL(string: e.getImagePath())!,
placeholderImage: UIImage(named: "default_galary_demo2")!
)
im.translatesAutoresizingMaskIntoConstraints = false
v.addSubview(im)
// Adding constraints
NSLayoutConstraint(item: im, attribute: .CenterY, relatedBy: .Equal, toItem: v, attribute: .CenterY, multiplier: 1, constant: 0).active = true
NSLayoutConstraint(item: im, attribute: NSLayoutAttribute.Leading, relatedBy: NSLayoutRelation.Equal, toItem: v, attribute: NSLayoutAttribute.LeadingMargin, multiplier: 1.0, constant: 0.0).active = true
im.widthAnchor.constraintEqualToAnchor(nil, constant: imageWidth).active = true
im.heightAnchor.constraintEqualToAnchor(nil, constant: imageHeight).active = true
// cell.frame.width - im.frame.width - 50
let label = UILabel(frame: CGRectMake(0, CGFloat(i)*labelHeight, UIScreen.mainScreen().bounds.width - imageWidth, labelHeight))
label.font = UIFont(name: "OpenSans", size: 12)
print("expertise dump: ", dump(e.getExpertiseValuesList()))
//print("expertise str: ", e.getExpertiseValuesList().map({"\($0.getName())"}).joinWithSeparator(","))
label.text = e.getExpertiseValuesList().map({"\($0.getName())"}).joinWithSeparator(",")
//label.translatesAutoresizingMaskIntoConstraints = false
v.addSubview(label)
NSLayoutConstraint(item: label, attribute: .CenterY, relatedBy: .Equal, toItem: v, attribute: .CenterY, multiplier: 1, constant: 0).active = true
NSLayoutConstraint(item: label, attribute: NSLayoutAttribute.Leading, relatedBy: NSLayoutRelation.Equal, toItem: im, attribute: NSLayoutAttribute.LeadingMargin, multiplier: 1.0, constant: 10.0).active = true
cell.designerExpertiseView.addSubview(v)
i += 1
}
self.firstRender[indexPath.section] = false
I don't think it correct to render cell only when if self.firstRender[indexPath.section] because when user scroll table view, the cells out of screen will be reused to show others different indexPaths.
Another thingļ¼Œ you create constraints, but not use them
You could use UIStackViews to handle the constraints for you. You would:
Greatly reduce the boilerplate-constraint code in your class;
Simplify adding subviews;
Allow for Adaptive Layouts.
Add a Vertical UIStackViewFor into your UITableViewCell. For more complex layouts, you can either embed more UIStackViews into the Vertical stack view or mix AutoLayout and UIStackViews.
I've quickly tried to recreate your layout and I've added a UIStackView. This implementation uses AutoLayout for some fixed components (like the profile picture) and the stack view to handle UILabels added programatically.
I hope this helps!
As it seems, adding views programmatically to cell using .addSubView() won't go well with reusable cell. It happens since cells are reused when go out of view, but when become visible again, the subviews are being added again.
The solution/workaround I used was to add another placeholder view from storyboard. And i removed the view from superview using .removeFromSuperview() when not needed.
I know this method is not good
Ex: Lets say I put 10 views in a cell ( assuming 10 is maximum views i need), but many of them might not be needed based on the data i get from server. So i will remove most of them from the view.
I am still looking for a better/actual solution for this issue.

textView height equal to content size without viewDidLayoutSubviews

I'm creating a viewController which contain 2 textViews a title and a fullText. At the moment i've created 2 textViews in the interface builder which is placed below each other and then created following code to change the height to equal to the content. However the issue is that it seem to be delayed, which gives a bad user experience. By delay i mean that it takes 0.5 or 1 sec before it resize? here is my code:
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
setHeightToContent(self.titleText!)
setHeightToContent(self.fullText!)
scrollView?.contentSize = CGSizeMake(self.view.frame.width, self.fullText!.frame.origin.y + self.fullText!.frame.height)
println(self.fullText!.frame.origin.y + self.fullText!.frame.height)
}
func setHeightToContent(theTextView: UITextView) {
let contentSize = theTextView.sizeThatFits(theTextView.bounds.size)
var frame = theTextView.frame
frame.size.height = contentSize.height
theTextView.frame = frame
var aspectRatioTextViewConstraint = NSLayoutConstraint(item: theTextView, attribute: .Height, relatedBy: .Equal, toItem: theTextView, attribute: .Width, multiplier: theTextView.bounds.height/theTextView.bounds.width, constant: 1)
theTextView.addConstraint(aspectRatioTextViewConstraint)
}
Make each of the text views self-sizing in accordance with its own content, and use constraints so that the scroll view's contentSize is configured automatically based on its content.

Resources