Left-aligning vertical UILabel in Swift - ios

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.

Related

Adding full programamticly custom view to ViewController. AutoLayout problems

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)
])

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:

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

UICollectionViewCell layout with SnapKit

I have a simple UICollectionViewCell which is full width and I'm using SnapKit to layout its subviews. I'm using SnapKit for my other view and it's working great but not within the collectionViewCell. This is the layout I'm trying to achieve:
collectionViewCell layout
The code for the collectionViewCell is:
lazy var imageView: UIImageView = {
let img = UIImageView(frame: .zero)
img.contentMode = UIViewContentMode.scaleAspectFit
img.backgroundColor = UIColor.lightGray
return img
}()
override init(frame: CGRect) {
super.init(frame: frame)
let imageWidth = CGFloat(frame.width * 0.80)
let imageHeight = CGFloat(imageWidth/1.77)
backgroundColor = UIColor.darkGray
imageView.frame.size.width = imageWidth
imageView.frame.size.height = imageHeight
contentView.addSubview(imageView)
imageView.snp.makeConstraints { (make) in
make.top.equalTo(20)
//make.right.equalTo(-20)
//make.right.equalTo(contentView).offset(-20)
//make.right.equalTo(contentView.snp.right).offset(-20)
//make.right.equalTo(contentView.snp.rightMargin).offset(-20)
//make.right.equalToSuperview().offset(-20)
}
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
Without applying any constraints the imageView is displayed top left in the cell but applying any constraints makes the image disappear. Inspecting the collectionViewCell in debug view shows the imageView and constraints but the 'Size and horizontal position are ambiguous for UIImageView'.
I have also tried setting contentView.translatesAutoresizingMaskIntoConstraints = false amongst other things but the same results.
I'm using all code and no storyboard UI or layouts.
Thanks for any help!
You can't use both auto layout (SnapKit) and manual layout (set frame). Usually, using auto layout will cause manual layout to fail. However, your code of the constraints in snp closure is not complete. Whether using auto layout or manual layout, finally the view should get the position and size.
So, you can try like this:
imageView.snp.makeConstraints { make in
// set size
make.size.equalTo(CGSize(width: imageWidth, height: imageHeight))
// set position
make.top.equalTo(20)
make.centerX.equalTo(contentView)
}
I don't know much about SnapKit but it looks like you're missing a constraint. You need height, width, x and y. Works nicely with IOS 9 constraints.
let imageView: UIImageView = {
let view = UIImageView()
view.translatesAutoresizingMaskIntoConstraints = false
view.contentMode = .scaleAspectFit
view.backgroundColor = .lightGray
return view
}()
override init(frame: CGRect) {
super.init(frame: frame)
setupViews()
}
func setupViews() {
self.addSubview(imageView)
imageView.topAnchor.constraint(equalTo: self.topAnchor, constant: 10).isActive = true
imageView.rightAnchor.constraint(equalTo: self.rightAnchor, constant: -10).isActive = true
imageView.widthAnchor.constraint(equalTo: self.widthAnchor, multiplier: 2/3, constant: -10).isActive = true
imageView.heightAnchor.constraint(equalTo: self.heightAnchor, multiplier: 1/2, constant: -10).isActive = true
}

Constraints not updating with device orientation change

I have a color slider in a tableView cell that extends from the label to the trailingAnchor of the cell. The issue I am having is when the device orientation changes the color slider does not update its constraints and no longer extends the full length of the cell. Below is my code to set up the constraints on the color slider. Below are images to show the issue I am having. If my phone is already in landscape orientation when I present this scene the color slider extends the full length of the cell as desired. However, if I switch to landscape when already viewing this scene the slider appears as below. Here is the full code if that helps.
func configureColorSlider() {
let colorSlider = ColorSlider()
let xCell = colorCell.contentView.bounds.width
let yCell = colorCell.contentView.bounds.height
colorSlider.frame = CGRect(x: xCell / 4, y: yCell / 4, width: 200, height: 24)
colorSlider.orientation = .horizontal
colorSlider.addTarget(self, action: #selector(ConfigureTimestampTableViewController.changedColor(_:)), for: .valueChanged)
colorCell.contentView.addSubview(colorSlider)
colorSlider.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([colorSlider.leadingAnchor.constraint(equalTo: colorLabel.trailingAnchor, constant: 8),
colorSlider.trailingAnchor.constraint(equalTo: colorCell.contentView.trailingAnchor, constant: -8),
colorSlider.topAnchor.constraint(equalTo: colorCell.contentView.topAnchor, constant: 8),
colorSlider.bottomAnchor.constraint(equalTo: colorCell.contentView.bottomAnchor, constant: -8) ])
}
CALayers will not be resized in response to their parent UIView being resized, whether by auto layout or manually.
You just need to add code to your ColorSlider that updates the layer when the size/position of the ColorSilider changes. Luckily, there is a method on UIView that is specifically for this.
override func layoutSubviews() {
super.layoutSubviews()
gradientLayer.frame = bounds
}

Resources