Adding full programamticly custom view to ViewController. AutoLayout problems - ios

I really can't understand how to use layout when create custom view.
Main problem is in here:
rect.heightAnchor.constraint(equalTo: heightAnchor, multiplier: 0.5),
rect.widthAnchor.constraint(equalTo: widthAnchor, multiplier: 1),
If I use multiplier it's work correctly. But if if I use constant:
rect.widthAnchor.constraint(equalToConstant: self.frame.width)
it's doesn't working.
How can I use any superView frame sizes for my custom view autoLayout constraint?
class MainView: UIView {
lazy var rect: UIView = {
let view = UIView()
view.backgroundColor = .green
view.translatesAutoresizingMaskIntoConstraints = false
return view
}()
override init(frame: CGRect) {
super.init(frame: frame)
backgroundColor = .red
layout()
}
func layout() {
addSubview(rect)
self.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
rect.heightAnchor.constraint(equalTo: heightAnchor, multiplier: 0.5),
rect.widthAnchor.constraint(equalTo: widthAnchor, multiplier: 1),
rect.centerYAnchor.constraint(equalTo: centerYAnchor),
rect.centerXAnchor.constraint(equalTo: centerXAnchor)
])
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
class ViewController: UIViewController {
let mainView = MainView(frame: .zero)
override func loadView() {
self.view = mainView
}

If I use multiplier it's work correctly. But if if I use constant:
rect.widthAnchor.constraint(equalToConstant: self.frame.width)
This is bound to happen, In your viewController you instantiate your mainView as
let mainView = MainView(frame: .zero) This will trigger init of MainView with frame as zero
You call layout inside init
override init(frame: CGRect) {
super.init(frame: frame)
backgroundColor = .red
layout()
}
so you essentially call layout when MainView frame is Zero At this point if you set rect.widthAnchor.constraint(equalToConstant: self.frame.width) then what you are essentially saying is rect.widthAnchor.constraint(equalToConstant: 0)
hence width is set to zero
Please read the statement carefully, you are setting your rect width anchor to a constant which is 0 in this case, so even when in future stages of UI cycle your MainView gets proper width, your rect's width will always be zero because you set it to constant 0 value.
But I set mainView.frame = view.frame before self.view= mainView
Unfortunately for your rect this statement comes little late :) layout was called in init of mainView and hence rects width anchor was already set to a constant 0. Now though you update mainView.frame, your rect will never bother to update its width, why will it? It has its constraints already resolved to a constant value which is obviously '0' in this case :)
rect.widthAnchor.constraint(equalTo: widthAnchor, multiplier: 1),Why this works?
Here you say your rect's width anchor to be equal to width anchor of your MainView, you are not setting rect's width anchor to a constant rather, you are setting it to match the width anchor of MainView, so when MainView's frame finally sets to a proper value, your rect will also updates its width to match MainViews width.
Quite frankly multiplier: 1 makes no sense in this case, as you are trying to match the width of your rect to match the width of your MainView, if you want your rect's width to vary proportionally to width of MainView (like 50% of MainView width etc etc) only then multiplier makes sense
Autolayout constraints are nothing more than mathematical equations, understanding/visualising them properly is the key to get perfect UI with constraint
Hope this helps

How can I use any superView frame sizes for my custom view autoLayout constraint?
Simply remove multiplayer.
NSLayoutConstraint.activate([
rect.heightAnchor.constraint(equalTo: heightAnchor),
rect.widthAnchor.constraint(equalTo: widthAnchor),
rect.centerYAnchor.constraint(equalTo: centerYAnchor),
rect.centerXAnchor.constraint(equalTo: centerXAnchor)
])

Related

Autolayout without intrinsic size

Can anyone tell me how Autolayout knows the size of a UIView which does not have an intrinsic size and only one dimension has been constrained?
For example, I have a plain view containing a set of subviews. The view is constrained to the leading and trailing margins of the parent and centered vertically. Autolayout is able to ask the view for its height for a given width but I don't know how. I need to layout the subviews by hand but I cannot see which function/property I need to implement so Autolayout can tell me the width and ask for the corresponding height.
Any one know?
As a general rule, the more complex the layout, the more reason to use auto-layout.
However, if you really want to "hand layout" your subviews and calculate the intrinsic size, you can do it by overriding intrinsicContentSize.
So, for example, if you calculate your subviews sizes / positions in layoutSubviews(), you should also calculate the height there, call invalidateIntrinsicContentSize(), and return your calculated height.
Here's a quick example. We add two subviews, one above the other. We give the top view a 4:1 ratio and the bottom view a 3:1 ratio.
class MyIntrinsicView: UIView {
var myHeight: CGFloat = 0
let view1: UIView = {
let v = UIView()
v.backgroundColor = .red
return v
}()
let view2: UIView = {
let v = UIView()
v.backgroundColor = .green
return v
}()
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
func commonInit() -> Void {
addSubview(view1)
addSubview(view2)
backgroundColor = .blue
}
override var intrinsicContentSize: CGSize {
var sz = super.intrinsicContentSize
sz.height = myHeight
return sz
}
override func layoutSubviews() {
super.layoutSubviews()
// let's say view 1 needs a 4:1 ratio and
// view 2 needs a 3:1 ratio
view1.frame = CGRect(x: 0, y: 0, width: bounds.width, height: bounds.width / 4.0)
view2.frame = CGRect(x: 0, y: view1.frame.maxY, width: bounds.width, height: bounds.width / 3.0)
myHeight = view1.frame.height + view2.frame.height
invalidateIntrinsicContentSize()
}
}
Using this example view controller, where we give the view 60-pts leading and trailing, and only centerY constraints:
class QuickExampleViewController: UIViewController {
let testView = MyIntrinsicView()
override func viewDidLoad() {
super.viewDidLoad()
testView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(testView)
let g = view.safeAreaLayoutGuide
NSLayoutConstraint.activate([
testView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 60.0),
testView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -60.0),
testView.centerYAnchor.constraint(equalTo: g.centerYAnchor),
])
}
}
We get this output:

Height contraint in storyboard for a custom view with dynamic height

Starting swift here... How to set a constraint height to a custom view that wrap content or in other words that have a dynamic height ?
In Android we use wrap content or match parent for height. Match parent equivalent for ios is putting top, bottom, left, right constraint to 0 (if I understand well) but what about wrap content ?
Most of the time a custom view height can be dynamic. When I drag a view from the storyboard that extends my custom view, I'a asked for a height constraint... Txs for help !
edited: Why someone put a -1 without giving any reason, is this such a stupid question ?!
First step is making sure you have configured your custom #IBDesignable view correctly.
Here is a simple example, with a UITextField and a "helper" UILabel.
The text view is set to non-scrolling -- which allows it to auto-size its own height based on the text. It will grow / shrink as you type and add / delete text.
The text view and label are added to a vertical UIStackView to make it really, really easy to layout:
#IBDesignable
class MyCustomView: UIView {
let theTextView: UITextView = {
let v = UITextView()
v.translatesAutoresizingMaskIntoConstraints = false
v.isScrollEnabled = false
return v
}()
let helperLabel: UILabel = {
let v = UILabel()
v.translatesAutoresizingMaskIntoConstraints = false
v.textAlignment = .center
v.textColor = .white
v.backgroundColor = .blue
v.text = "Helper Text"
return v
}()
let theStackView: UIStackView = {
let v = UIStackView()
v.translatesAutoresizingMaskIntoConstraints = false
v.axis = .vertical
v.alignment = .fill
v.distribution = .fill
v.spacing = 8
return v
}()
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
commonInit()
}
func commonInit() -> Void {
// add the stack view as a subview
addSubview(theStackView)
// constrain the stack view top / bottom / leading / trailing
// with 8-pts "padding" on each side
NSLayoutConstraint.activate([
theStackView.topAnchor.constraint(equalTo: topAnchor, constant: 8.0),
theStackView.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -8.0),
theStackView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 8.0),
theStackView.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -8.0),
])
// add the text view and label to the stack view
theStackView.addArrangedSubview(theTextView)
theStackView.addArrangedSubview(helperLabel)
}
}
Now, in a new view controller in Storyboard, add a normal UIView and give it a background color (so we can see it). Add Leading and Trailing constraints of 40, and add a Center Vertically constraint. It should look similar to this:
and Storyboard will tell you it needs a constraint:
With the view selected, go to the Identity Inspector and change the Class to MyCustomClass. If you have **Automatically Refresh Views` turned on, it should change to this:
It is now centered vertically, and uses its own height (determined by the intrinsic heights of the text view and the label, embedded in the stack view). No more Needs constraints for: Y position or height error message, without needing to set any additional constraints.
When you run the app (and type some text into the text view), you'll get this:

Left-aligning vertical UILabel in Swift

I'm working inside a UICollectionViewCell, trying to get a rotated UILabel to stick to the left of the label at a fixed width. This is what I have been able achieve:
As you can see, the label's dimensions seem relative to the length of the text and disabling auto-resizing has no effect. I would like to constrain the label to ~80 and occupy the full height of the cell, enough for the font with some spacing. The entire code for the UICollectionViewCell:
import Foundation
import UIKit
class DayOfWeekCell: UICollectionViewCell {
let categoryLabel = UILabel()
override init(frame: CGRect) {
super.init(frame: frame)
backgroundColor = .red
categoryLabel.transform = CGAffineTransform.init(rotationAngle: -CGFloat.pi/2)
categoryLabel.textColor = UIColor.white
categoryLabel.font = UIFont.systemFont(ofSize: 25)
categoryLabel.translatesAutoresizingMaskIntoConstraints = false
addSubview(categoryLabel)
categoryLabel.backgroundColor = .blue
categoryLabel.leadingAnchor.constraint(equalTo: self.leadingAnchor, constant: 0).isActive = true
categoryLabel.topAnchor.constraint(equalTo: self.topAnchor, constant: 0).isActive = true
categoryLabel.bottomAnchor.constraint(equalTo: self.bottomAnchor, constant: 0).isActive = true
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
The ViewController sizeForItem returns the view width with 180 height. It does not play with anything of the cell, only setting the label's text.
I am still relatively new to iOS but have spent the past hour tinkering with this and just cannot get it to play nice! SnapKit is imported but I have had no success with it either. Is there some autosizing going on I'm not aware of? Any help would be greatly appreciated!
I guess there is an issue with the constraints applied using anchors. As all the bottom anchor is also applied, the anchor point is different for different texts. I managed get a decent alignment by applying translating the position again by doing
let transform = CGAffineTransform(rotationAngle: -.pi / 2).translatedBy(x: -25, y: -50)
categoryLabel.transform = transform
Constraints applied to the category label was aligned to leftAnchor, topAnchor and with a width of 100.

Dynamically size UIStackView to the width of the UIViews inside

I am trying to create a UIStackView with three UIViews inside. The UIViews will have a circle with text over / in it.
I would like not to set the StackView to a static number, i would like it to be able to get smaller/grow based on the device the user is using.
Right now, the StackView is being added to the view, and the UIViews are being added to that. The colors are being displayed, but the rounded circles are not and the StackView height is not equal to the leftui's width.
Basically, I need three circles of equal height and width....is there a better way for this?
Here is my code.
#IBOutlet var stack: UIStackView!
override func viewWillLayoutSubviews() {
//let stack = UIStackView()
let leftui = UIView()
let middleui = UIView()
let rightui = UIView()
stack.addArrangedSubview(leftui)
stack.addArrangedSubview(middleui)
stack.addArrangedSubview(rightui)
leftui.backgroundColor = UIColor.red
middleui.backgroundColor = UIColor.blue
rightui.backgroundColor = UIColor.brown
leftui.bounds.size.height = leftui.bounds.width //needs these to new equal
middleui.bounds.size.height = middleui.bounds.width //needs these to new equal
rightui.bounds.size.height = rightui.bounds.width //needs these to new equal
leftui.layer.cornerRadius = leftui.bounds.size.width / 2
middleui.layer.cornerRadius = middleui.bounds.size.width / 2
rightui.layer.cornerRadius = rightui.bounds.size.width / 2
print(leftui.bounds.size.width) //prints 0.0
leftui.clipsToBounds = true
middleui.clipsToBounds = true
rightui.clipsToBounds = true
stack.sizeToFit()
stack.layoutIfNeeded()
view.addSubview(stack)
}
Here is what I was looking for.
This is from the android version of the application.
I think that in order for UIStackView to work its arrangedSubviews have to use autolayout - Check first answer here: Is it necessary to use autolayout to use stackview
This is how you could solve this:
Add a new class for your circular views, these do not do much other than set its layer.cornerRadius to half of their width, so that if height and width are the same they will be circular.
class CircularView: UIView {
override func layoutSubviews() {
super.layoutSubviews()
clipsToBounds = true
layer.cornerRadius = bounds.midX
}
}
You add a widthConstraint with which you will be able to size the elements in the stack view
var widthConstraint: NSLayoutConstraint!
You can then create the UIStackView, I used your code mostly to do this:
override func viewDidLoad() {
super.viewDidLoad()
let leftui = CircularView()
let middleui = CircularView()
let rightui = CircularView()
leftui.translatesAutoresizingMaskIntoConstraints = false
middleui.translatesAutoresizingMaskIntoConstraints = false
rightui.translatesAutoresizingMaskIntoConstraints = false
leftui.backgroundColor = UIColor.red
middleui.backgroundColor = UIColor.blue
rightui.backgroundColor = UIColor.brown
let stack = UIStackView(arrangedSubviews: [leftui, middleui, rightui])
stack.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(stack)
widthConstraint = leftui.widthAnchor.constraint(equalToConstant: 100)
NSLayoutConstraint.activate([
widthConstraint,
stack.centerXAnchor.constraint(equalTo: view.centerXAnchor),
stack.centerYAnchor.constraint(equalTo: view.centerYAnchor),
leftui.heightAnchor.constraint(equalTo: leftui.widthAnchor, multiplier: 1.0),
middleui.widthAnchor.constraint(equalTo: leftui.widthAnchor, multiplier: 1.0),
middleui.heightAnchor.constraint(equalTo: leftui.widthAnchor, multiplier: 1.0),
rightui.widthAnchor.constraint(equalTo: leftui.widthAnchor, multiplier: 1.0),
rightui.heightAnchor.constraint(equalTo: leftui.widthAnchor, multiplier: 1.0)
])
}
Given the constraints set here, circles will have a width/height of 100 and stack view is centred in the view.
Next if you want to do something when view rotates you could implement something like this in your viewController
override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
coordinator.animate(alongsideTransition: { _ in
if size.width > size.height {
self.widthConstraint.constant = 150
} else {
self.widthConstraint.constant = 100
}
}, completion: nil)
}
It would animate to circles of width/height of 150 in landscape. You can then play with these values to get desired outcome.
To design this, follow the below steps.
create a custom view. in the custom view put all the subviews like
cost title label, price label and the color UIImageView
Now create three object of the custom view with proper data.
Get the device screen width divide by 3 gives each custom view
width, also set the view height as per your requirement and provide
frame for the created custom view
Now add the three views to the StackView.
Hope this will help to design, if you need any more help please comment.

How do I get UIView width and height before adding to subview?

I'm building a UIPageViewController that has a variable number of pages based on the height of views in an array of views.
I have a class called a BlockView that looks like this:
final class BlockView: UIView {
init(viewModel: BlockViewModel) {
super.init(frame: .zero)
let primaryLabel = UILabel()
primaryLabel.text = viewModel.labelText
addSubview(primaryLabel)
constrain(primaryLabel) {
$0.top == $0.superview!.top + 8
$0.bottom == $0.superview!.bottom - 8
$0.left == $0.superview!.left + 8
$0.right == $0.superview!.right - 8
}
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
What I'd like to be able to do is loop through my array of BlockViews and run print(blockView.frame) and see frames that aren't zero.
Now I know I'm setting my frame to .zero inside of the BlockView.init. That's because I'd like the view to size itself based on its labels.
Is there a function I need to run to achieve this?
Thanks
Try sizeThatFits(_:) to calculate it without putting it to the superview. The only parameter to the method is the CGSize that represents boundaries in which it should be displayed. E.g., if you know the width of the superview (e.g., 340 points) and you want to know how much it will take in height:
let expectedSize = view.sizeThatFits(CGSize(width: 340, height: .greatestFiniteMagnitude))
However, your BlockView does not seem to have a proper constraints set yet. You initialize it with super.init(frame: .zero) - thus it has size 0,0.
And your constraints does not change that, e.g.:
constrain(primaryLabel) {
$0.centerY == $0.superview!.centerY
$0.left == $0.superview!.left + 8
}
This looks like you set the center in Y axis of the label to the center of the block view, and the left anchor of the label to the left anchor of the view. If the blockView would have the size already, that would position the label properly. But right now the size of the block view is not affected by the size of labels at all. I guess you would want to constrain the labels to the left, right, top and bottom anchors of the blockView, so that when you try to calculate the size of the blockView, the autolayout will have to first calculate the size of the labels and based on this the size of the blockView itself.
A possible solution (I am using anchor based autolayout syntax) that you can try to put to initializer of the BlockView:
primaryLabel.leftAnchor.constraint(equalTo: self.leftAnchor, constant: 8).isActive = true
primaryLabel.topAchor.constraint(equalTo: self.topAnchor, constant: 8).isActive = true
primaryLabel.rightAnchor.constraint(equalTo: self.rightAnchor, constant: -8).isActive = true
primaryLabel.bottomAnchor.constraint(equalTo: secondaryLabel.topAnchor, constant: -8).isActive = true
secondaryLabel.leftAnchor.constraint(equalTo: self.leftAnchor, constant: 8).isActive = true
secondaryLabel.rightAnchor.constraint(equalTo: self.rightAnchor, constant: -8).isActive = true
secondaryLabel.bottomAnchor.constraint(equalTo: self.bottomAnchor, constant: -8).isActive = true

Resources